mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-08-01 19:21:34 -07:00
refactor(stats): drop dead IPC handlers, unify stats types over HTTP (#164)
This commit is contained in:
@@ -0,0 +1,231 @@
|
||||
import type {
|
||||
AnimeDetailData,
|
||||
AnimeLibraryItem,
|
||||
AnimeWord,
|
||||
DailyRollup,
|
||||
EpisodeDetailData,
|
||||
EpisodesPerDay,
|
||||
KanjiDetailData,
|
||||
KanjiEntry,
|
||||
MediaDetailData,
|
||||
MediaLibraryItem,
|
||||
MonthlyRollup,
|
||||
NewAnimePerDay,
|
||||
OverviewData,
|
||||
SentenceSearchResult,
|
||||
SessionEvent,
|
||||
SessionSummary,
|
||||
SessionTimelinePoint,
|
||||
StatsAnkiNoteInfo,
|
||||
StatsCoverImagesData,
|
||||
StatsExcludedWord,
|
||||
StreakCalendarDay,
|
||||
TrendsDashboardData,
|
||||
VocabularyEntry,
|
||||
VocabularyOccurrenceEntry,
|
||||
WatchTimePerAnime,
|
||||
WordDetailData,
|
||||
} from './stats-wire';
|
||||
|
||||
export type StatsTrendRange = '7d' | '30d' | '90d' | '365d' | 'all';
|
||||
export type StatsTrendGroupBy = 'day' | 'month';
|
||||
export type StatsMineMode = 'word' | 'sentence' | 'audio';
|
||||
|
||||
export interface StatsSessionKnownWordsTimelinePoint {
|
||||
linesSeen: number;
|
||||
knownWordsSeen: number;
|
||||
totalWordsSeen: number;
|
||||
}
|
||||
|
||||
export interface StatsKnownWordsSummary {
|
||||
totalUniqueWords: number;
|
||||
knownWordCount: number;
|
||||
}
|
||||
|
||||
export interface StatsAnilistSearchResult {
|
||||
id: number;
|
||||
episodes: number | null;
|
||||
season: string | null;
|
||||
seasonYear: number | null;
|
||||
description: string | null;
|
||||
coverImage: { large: string | null; medium: string | null } | null;
|
||||
title: { romaji: string | null; english: string | null; native: string | null } | null;
|
||||
}
|
||||
|
||||
export interface StatsAnilistAssignment {
|
||||
anilistId: number;
|
||||
titleRomaji?: string | null;
|
||||
titleEnglish?: string | null;
|
||||
titleNative?: string | null;
|
||||
episodesTotal?: number | null;
|
||||
description?: string | null;
|
||||
coverUrl?: string | null;
|
||||
}
|
||||
|
||||
export interface StatsMineCardParams {
|
||||
sourcePath: string;
|
||||
startMs: number;
|
||||
endMs: number;
|
||||
sentence: string;
|
||||
word: string;
|
||||
secondaryText?: string | null;
|
||||
videoTitle: string;
|
||||
mode: StatsMineMode;
|
||||
}
|
||||
|
||||
export interface StatsMineCardResponse {
|
||||
noteId?: number;
|
||||
error?: string;
|
||||
errors?: string[];
|
||||
}
|
||||
|
||||
export interface StatsCoverImagesRequest {
|
||||
animeIds: number[];
|
||||
videoIds: number[];
|
||||
}
|
||||
|
||||
export interface StatsExcludedWordsRequest {
|
||||
words: StatsExcludedWord[];
|
||||
}
|
||||
|
||||
export interface StatsVideoWatchedRequest {
|
||||
watched: boolean;
|
||||
}
|
||||
|
||||
export interface StatsDeleteSessionsRequest {
|
||||
sessionIds: number[];
|
||||
}
|
||||
|
||||
export interface StatsAnkiNotesInfoRequest {
|
||||
noteIds: number[];
|
||||
}
|
||||
|
||||
export interface StatsOkResponse {
|
||||
ok: true;
|
||||
}
|
||||
|
||||
export interface StatsErrorResponse {
|
||||
error: string;
|
||||
}
|
||||
|
||||
export interface StatsAnkiBrowseResponse {
|
||||
result?: number[] | null;
|
||||
error?: string | null;
|
||||
}
|
||||
|
||||
export interface StatsJsonResponseMap {
|
||||
error: StatsErrorResponse | null;
|
||||
overview: OverviewData;
|
||||
dailyRollups: DailyRollup[];
|
||||
monthlyRollups: MonthlyRollup[];
|
||||
sessions: SessionSummary[];
|
||||
sessionTimeline: SessionTimelinePoint[];
|
||||
sessionEvents: SessionEvent[];
|
||||
sessionKnownWordsTimeline: StatsSessionKnownWordsTimelinePoint[];
|
||||
vocabulary: VocabularyEntry[];
|
||||
excludedWords: StatsExcludedWord[];
|
||||
setExcludedWords: StatsOkResponse;
|
||||
wordOccurrences: VocabularyOccurrenceEntry[];
|
||||
sentenceSearch: SentenceSearchResult[];
|
||||
kanji: KanjiEntry[];
|
||||
kanjiOccurrences: VocabularyOccurrenceEntry[];
|
||||
wordDetail: WordDetailData;
|
||||
kanjiDetail: KanjiDetailData;
|
||||
mediaLibrary: MediaLibraryItem[];
|
||||
mediaDetail: MediaDetailData;
|
||||
animeLibrary: AnimeLibraryItem[];
|
||||
animeDetail: AnimeDetailData;
|
||||
animeWords: AnimeWord[];
|
||||
animeRollups: DailyRollup[];
|
||||
setVideoWatched: StatsOkResponse;
|
||||
deleteSessions: StatsOkResponse;
|
||||
deleteSession: StatsOkResponse;
|
||||
deleteVideo: StatsOkResponse;
|
||||
anilistSearch: StatsAnilistSearchResult[];
|
||||
knownWords: string[];
|
||||
knownWordsSummary: StatsKnownWordsSummary;
|
||||
animeKnownWordsSummary: StatsKnownWordsSummary;
|
||||
mediaKnownWordsSummary: StatsKnownWordsSummary;
|
||||
reassignAnimeAnilist: StatsOkResponse;
|
||||
coverImages: StatsCoverImagesData;
|
||||
episodeDetail: EpisodeDetailData;
|
||||
ankiBrowse: StatsAnkiBrowseResponse;
|
||||
ankiNotesInfo: StatsAnkiNoteInfo[];
|
||||
mineCard: StatsMineCardResponse;
|
||||
streakCalendar: StreakCalendarDay[];
|
||||
episodesPerDay: EpisodesPerDay[];
|
||||
newAnimePerDay: NewAnimePerDay[];
|
||||
watchTimePerAnime: WatchTimePerAnime[];
|
||||
trendsDashboard: TrendsDashboardData;
|
||||
}
|
||||
|
||||
export function statsJson<Key extends keyof StatsJsonResponseMap>(
|
||||
_key: Key,
|
||||
payload: StatsJsonResponseMap[Key],
|
||||
): StatsJsonResponseMap[Key] {
|
||||
return payload;
|
||||
}
|
||||
|
||||
export interface StatsHttpClient {
|
||||
getOverview: () => Promise<OverviewData>;
|
||||
getDailyRollups: (limit?: number) => Promise<DailyRollup[]>;
|
||||
getMonthlyRollups: (limit?: number) => Promise<MonthlyRollup[]>;
|
||||
getSessions: (limit?: number) => Promise<SessionSummary[]>;
|
||||
getSessionTimeline: (id: number, limit?: number) => Promise<SessionTimelinePoint[]>;
|
||||
getSessionEvents: (id: number, limit?: number, eventTypes?: number[]) => Promise<SessionEvent[]>;
|
||||
getSessionKnownWordsTimeline: (id: number) => Promise<StatsSessionKnownWordsTimelinePoint[]>;
|
||||
getVocabulary: (limit?: number) => Promise<VocabularyEntry[]>;
|
||||
getExcludedWords: () => Promise<StatsExcludedWord[]>;
|
||||
setExcludedWords: (words: StatsExcludedWord[]) => Promise<void>;
|
||||
getWordOccurrences: (
|
||||
headword: string,
|
||||
word: string,
|
||||
reading: string,
|
||||
limit?: number,
|
||||
offset?: number,
|
||||
) => Promise<VocabularyOccurrenceEntry[]>;
|
||||
searchSentences: (
|
||||
query: string,
|
||||
limit?: number,
|
||||
searchByHeadword?: boolean,
|
||||
) => Promise<SentenceSearchResult[]>;
|
||||
getKanji: (limit?: number) => Promise<KanjiEntry[]>;
|
||||
getKanjiOccurrences: (
|
||||
kanji: string,
|
||||
limit?: number,
|
||||
offset?: number,
|
||||
) => Promise<VocabularyOccurrenceEntry[]>;
|
||||
getMediaLibrary: () => Promise<MediaLibraryItem[]>;
|
||||
getMediaDetail: (videoId: number) => Promise<MediaDetailData>;
|
||||
getAnimeLibrary: () => Promise<AnimeLibraryItem[]>;
|
||||
getAnimeDetail: (animeId: number) => Promise<AnimeDetailData>;
|
||||
getAnimeWords: (animeId: number, limit?: number) => Promise<AnimeWord[]>;
|
||||
getAnimeRollups: (animeId: number, limit?: number) => Promise<DailyRollup[]>;
|
||||
getAnimeCoverUrl: (animeId: number, retryToken?: number) => string;
|
||||
getCoverImages: (params: StatsCoverImagesRequest) => Promise<StatsCoverImagesData>;
|
||||
getStreakCalendar: (days?: number) => Promise<StreakCalendarDay[]>;
|
||||
getEpisodesPerDay: (limit?: number) => Promise<EpisodesPerDay[]>;
|
||||
getNewAnimePerDay: (limit?: number) => Promise<NewAnimePerDay[]>;
|
||||
getWatchTimePerAnime: (limit?: number) => Promise<WatchTimePerAnime[]>;
|
||||
getTrendsDashboard: (
|
||||
range: StatsTrendRange,
|
||||
groupBy: StatsTrendGroupBy,
|
||||
fillEmpty?: boolean,
|
||||
) => Promise<TrendsDashboardData>;
|
||||
getWordDetail: (wordId: number) => Promise<WordDetailData>;
|
||||
getKanjiDetail: (kanjiId: number) => Promise<KanjiDetailData>;
|
||||
getEpisodeDetail: (videoId: number) => Promise<EpisodeDetailData>;
|
||||
setVideoWatched: (videoId: number, watched: boolean) => Promise<void>;
|
||||
deleteSession: (sessionId: number) => Promise<void>;
|
||||
deleteSessions: (sessionIds: number[]) => Promise<void>;
|
||||
deleteVideo: (videoId: number) => Promise<void>;
|
||||
getKnownWords: () => Promise<string[]>;
|
||||
getKnownWordsSummary: () => Promise<StatsKnownWordsSummary>;
|
||||
getAnimeKnownWordsSummary: (animeId: number) => Promise<StatsKnownWordsSummary>;
|
||||
getMediaKnownWordsSummary: (videoId: number) => Promise<StatsKnownWordsSummary>;
|
||||
searchAnilist: (query: string) => Promise<StatsAnilistSearchResult[]>;
|
||||
reassignAnimeAnilist: (animeId: number, info: StatsAnilistAssignment) => Promise<void>;
|
||||
mineCard: (params: StatsMineCardParams) => Promise<StatsMineCardResponse>;
|
||||
ankiBrowse: (noteId: number) => Promise<void>;
|
||||
ankiNotesInfo: (noteIds: number[]) => Promise<StatsAnkiNoteInfo[]>;
|
||||
}
|
||||
@@ -0,0 +1,430 @@
|
||||
export interface SessionSummary {
|
||||
sessionId: number;
|
||||
canonicalTitle: string | null;
|
||||
videoId: number | null;
|
||||
animeId: number | null;
|
||||
animeTitle: string | null;
|
||||
startedAtMs: number;
|
||||
endedAtMs: number | null;
|
||||
totalWatchedMs: number;
|
||||
activeWatchedMs: number;
|
||||
linesSeen: number;
|
||||
tokensSeen: number;
|
||||
cardsMined: number;
|
||||
lookupCount: number;
|
||||
lookupHits: number;
|
||||
yomitanLookupCount: number;
|
||||
knownWordsSeen: number;
|
||||
knownWordRate: number;
|
||||
}
|
||||
|
||||
export interface DailyRollup {
|
||||
rollupDayOrMonth: number;
|
||||
videoId: number | null;
|
||||
totalSessions: number;
|
||||
totalActiveMin: number;
|
||||
totalLinesSeen: number;
|
||||
totalTokensSeen: number;
|
||||
totalCards: number;
|
||||
cardsPerHour: number | null;
|
||||
tokensPerMin: number | null;
|
||||
lookupHitRate: number | null;
|
||||
}
|
||||
|
||||
export type MonthlyRollup = DailyRollup;
|
||||
|
||||
export interface SessionTimelinePoint {
|
||||
sampleMs: number;
|
||||
totalWatchedMs: number;
|
||||
activeWatchedMs: number;
|
||||
linesSeen: number;
|
||||
tokensSeen: number;
|
||||
cardsMined: number;
|
||||
}
|
||||
|
||||
export interface SessionEvent {
|
||||
eventType: number;
|
||||
tsMs: number;
|
||||
payload: string | null;
|
||||
}
|
||||
|
||||
export interface AnkiNotePreview {
|
||||
word: string;
|
||||
sentence: string;
|
||||
translation: string;
|
||||
}
|
||||
|
||||
export interface StatsAnkiNoteInfo {
|
||||
noteId: number;
|
||||
fields: Record<string, { value: string }>;
|
||||
preview?: AnkiNotePreview;
|
||||
}
|
||||
|
||||
export interface VocabularyEntry {
|
||||
wordId: number;
|
||||
headword: string;
|
||||
word: string;
|
||||
reading: string;
|
||||
partOfSpeech: string | null;
|
||||
pos1: string | null;
|
||||
pos2: string | null;
|
||||
pos3: string | null;
|
||||
frequency: number;
|
||||
frequencyRank: number | null;
|
||||
animeCount: number;
|
||||
firstSeen: number;
|
||||
lastSeen: number;
|
||||
}
|
||||
|
||||
export interface StatsExcludedWord {
|
||||
headword: string;
|
||||
word: string;
|
||||
reading: string;
|
||||
}
|
||||
|
||||
export interface StatsCoverImage {
|
||||
contentType: string;
|
||||
dataUrl: string;
|
||||
}
|
||||
|
||||
export interface StatsCoverImagesData {
|
||||
anime: Record<number, StatsCoverImage | null>;
|
||||
media: Record<number, StatsCoverImage | null>;
|
||||
}
|
||||
|
||||
export interface KanjiEntry {
|
||||
kanjiId: number;
|
||||
kanji: string;
|
||||
frequency: number;
|
||||
firstSeen: number;
|
||||
lastSeen: number;
|
||||
}
|
||||
|
||||
export interface VocabularyOccurrenceEntry {
|
||||
animeId: number | null;
|
||||
animeTitle: string | null;
|
||||
videoId: number;
|
||||
videoTitle: string;
|
||||
sourcePath: string | null;
|
||||
secondaryText: string | null;
|
||||
sessionId: number;
|
||||
lineIndex: number;
|
||||
segmentStartMs: number | null;
|
||||
segmentEndMs: number | null;
|
||||
text: string;
|
||||
occurrenceCount: number;
|
||||
}
|
||||
|
||||
export interface SentenceSearchResult {
|
||||
animeId: number | null;
|
||||
animeTitle: string | null;
|
||||
videoId: number;
|
||||
videoTitle: string;
|
||||
sourcePath: string | null;
|
||||
secondaryText: string | null;
|
||||
sessionId: number;
|
||||
lineIndex: number;
|
||||
segmentStartMs: number | null;
|
||||
segmentEndMs: number | null;
|
||||
text: string;
|
||||
}
|
||||
|
||||
export interface OverviewData {
|
||||
sessions: SessionSummary[];
|
||||
rollups: DailyRollup[];
|
||||
hints: {
|
||||
totalSessions: number;
|
||||
activeSessions: number;
|
||||
episodesToday: number;
|
||||
activeAnimeCount: number;
|
||||
totalEpisodesWatched: number;
|
||||
totalAnimeCompleted: number;
|
||||
totalActiveMin: number;
|
||||
activeDays: number;
|
||||
totalCards?: number;
|
||||
totalTokensSeen: number;
|
||||
totalLookupCount: number;
|
||||
totalLookupHits: number;
|
||||
totalYomitanLookupCount: number;
|
||||
newWordsToday: number;
|
||||
newWordsThisWeek: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface MediaLibraryItem {
|
||||
videoId: number;
|
||||
canonicalTitle: string;
|
||||
totalSessions: number;
|
||||
totalActiveMs: number;
|
||||
totalCards: number;
|
||||
totalTokensSeen: number;
|
||||
lastWatchedMs: number;
|
||||
hasCoverArt: number;
|
||||
youtubeVideoId?: string | null;
|
||||
videoUrl?: string | null;
|
||||
videoTitle?: string | null;
|
||||
videoThumbnailUrl?: string | null;
|
||||
channelId?: string | null;
|
||||
channelName?: string | null;
|
||||
channelUrl?: string | null;
|
||||
channelThumbnailUrl?: string | null;
|
||||
uploaderId?: string | null;
|
||||
uploaderUrl?: string | null;
|
||||
description?: string | null;
|
||||
}
|
||||
|
||||
export interface MediaDetailData {
|
||||
detail: {
|
||||
videoId: number;
|
||||
canonicalTitle: string;
|
||||
animeId: number | null;
|
||||
totalSessions: number;
|
||||
totalActiveMs: number;
|
||||
totalCards: number;
|
||||
totalTokensSeen: number;
|
||||
totalLinesSeen: number;
|
||||
totalLookupCount: number;
|
||||
totalLookupHits: number;
|
||||
totalYomitanLookupCount: number;
|
||||
youtubeVideoId?: string | null;
|
||||
videoUrl?: string | null;
|
||||
videoTitle?: string | null;
|
||||
videoThumbnailUrl?: string | null;
|
||||
channelId?: string | null;
|
||||
channelName?: string | null;
|
||||
channelUrl?: string | null;
|
||||
channelThumbnailUrl?: string | null;
|
||||
uploaderId?: string | null;
|
||||
uploaderUrl?: string | null;
|
||||
description?: string | null;
|
||||
} | null;
|
||||
sessions: SessionSummary[];
|
||||
rollups: DailyRollup[];
|
||||
}
|
||||
|
||||
export const EventType = {
|
||||
SUBTITLE_LINE: 1,
|
||||
MEDIA_BUFFER: 2,
|
||||
LOOKUP: 3,
|
||||
CARD_MINED: 4,
|
||||
SEEK_FORWARD: 5,
|
||||
SEEK_BACKWARD: 6,
|
||||
PAUSE_START: 7,
|
||||
PAUSE_END: 8,
|
||||
YOMITAN_LOOKUP: 9,
|
||||
} as const;
|
||||
|
||||
export type EventType = (typeof EventType)[keyof typeof EventType];
|
||||
|
||||
export interface AnimeLibraryItem {
|
||||
animeId: number;
|
||||
canonicalTitle: string;
|
||||
anilistId: number | null;
|
||||
totalSessions: number;
|
||||
totalActiveMs: number;
|
||||
totalCards: number;
|
||||
totalTokensSeen: number;
|
||||
episodeCount: number;
|
||||
episodesTotal: number | null;
|
||||
lastWatchedMs: number;
|
||||
}
|
||||
|
||||
export interface AnilistEntry {
|
||||
anilistId: number;
|
||||
titleRomaji: string | null;
|
||||
titleEnglish: string | null;
|
||||
season: number | null;
|
||||
}
|
||||
|
||||
export interface AnimeDetailData {
|
||||
detail: {
|
||||
animeId: number;
|
||||
canonicalTitle: string;
|
||||
anilistId: number | null;
|
||||
titleRomaji: string | null;
|
||||
titleEnglish: string | null;
|
||||
titleNative: string | null;
|
||||
description: string | null;
|
||||
totalSessions: number;
|
||||
totalActiveMs: number;
|
||||
totalCards: number;
|
||||
totalTokensSeen: number;
|
||||
totalLinesSeen: number;
|
||||
totalLookupCount: number;
|
||||
totalLookupHits: number;
|
||||
totalYomitanLookupCount: number;
|
||||
episodeCount: number;
|
||||
lastWatchedMs: number;
|
||||
};
|
||||
episodes: AnimeEpisode[];
|
||||
anilistEntries: AnilistEntry[];
|
||||
}
|
||||
|
||||
export interface AnimeEpisode {
|
||||
videoId: number;
|
||||
episode: number | null;
|
||||
season: number | null;
|
||||
durationMs: number;
|
||||
endedMediaMs: number | null;
|
||||
watched: number;
|
||||
canonicalTitle: string;
|
||||
totalSessions: number;
|
||||
totalActiveMs: number;
|
||||
totalCards: number;
|
||||
totalTokensSeen: number;
|
||||
totalYomitanLookupCount: number;
|
||||
lastWatchedMs: number;
|
||||
}
|
||||
|
||||
export interface AnimeWord {
|
||||
wordId: number;
|
||||
headword: string;
|
||||
word: string;
|
||||
reading: string;
|
||||
partOfSpeech: string | null;
|
||||
frequency: number;
|
||||
}
|
||||
|
||||
export interface StreakCalendarDay {
|
||||
epochDay: number;
|
||||
totalActiveMin: number;
|
||||
}
|
||||
|
||||
export interface EpisodesPerDay {
|
||||
epochDay: number;
|
||||
episodeCount: number;
|
||||
}
|
||||
|
||||
export interface NewAnimePerDay {
|
||||
epochDay: number;
|
||||
newAnimeCount: number;
|
||||
}
|
||||
|
||||
export interface WatchTimePerAnime {
|
||||
epochDay: number;
|
||||
animeId: number;
|
||||
animeTitle: string;
|
||||
totalActiveMin: number;
|
||||
}
|
||||
|
||||
export interface TrendChartPoint {
|
||||
label: string;
|
||||
value: number;
|
||||
}
|
||||
|
||||
export interface TrendPerAnimePoint {
|
||||
epochDay: number;
|
||||
animeTitle: string;
|
||||
value: number;
|
||||
}
|
||||
|
||||
export interface LibrarySummaryRow {
|
||||
title: string;
|
||||
watchTimeMin: number;
|
||||
videos: number;
|
||||
sessions: number;
|
||||
cards: number;
|
||||
words: number;
|
||||
lookups: number;
|
||||
lookupsPerHundred: number | null;
|
||||
firstWatched: number;
|
||||
lastWatched: number;
|
||||
}
|
||||
|
||||
export interface TrendsDashboardData {
|
||||
activity: {
|
||||
watchTime: TrendChartPoint[];
|
||||
cards: TrendChartPoint[];
|
||||
words: TrendChartPoint[];
|
||||
sessions: TrendChartPoint[];
|
||||
};
|
||||
progress: {
|
||||
watchTime: TrendChartPoint[];
|
||||
sessions: TrendChartPoint[];
|
||||
words: TrendChartPoint[];
|
||||
newWords: TrendChartPoint[];
|
||||
cards: TrendChartPoint[];
|
||||
episodes: TrendChartPoint[];
|
||||
lookups: TrendChartPoint[];
|
||||
};
|
||||
ratios: {
|
||||
lookupsPerHundred: TrendChartPoint[];
|
||||
cardsPerHour: TrendChartPoint[];
|
||||
readingSpeed: TrendChartPoint[];
|
||||
};
|
||||
librarySummary: LibrarySummaryRow[];
|
||||
animeCumulative: {
|
||||
watchTime: TrendPerAnimePoint[];
|
||||
episodes: TrendPerAnimePoint[];
|
||||
cards: TrendPerAnimePoint[];
|
||||
words: TrendPerAnimePoint[];
|
||||
};
|
||||
patterns: {
|
||||
watchTimeByDayOfWeek: TrendChartPoint[];
|
||||
watchTimeByHour: TrendChartPoint[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface WordDetailData {
|
||||
detail: {
|
||||
wordId: number;
|
||||
headword: string;
|
||||
word: string;
|
||||
reading: string;
|
||||
partOfSpeech: string | null;
|
||||
pos1: string | null;
|
||||
pos2: string | null;
|
||||
pos3: string | null;
|
||||
frequency: number;
|
||||
firstSeen: number;
|
||||
lastSeen: number;
|
||||
};
|
||||
animeAppearances: Array<{
|
||||
animeId: number;
|
||||
animeTitle: string;
|
||||
occurrenceCount: number;
|
||||
}>;
|
||||
similarWords: Array<{
|
||||
wordId: number;
|
||||
headword: string;
|
||||
word: string;
|
||||
reading: string;
|
||||
frequency: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface EpisodeCardEvent {
|
||||
eventId: number;
|
||||
sessionId: number;
|
||||
tsMs: number;
|
||||
cardsDelta: number;
|
||||
noteIds: number[];
|
||||
}
|
||||
|
||||
export interface EpisodeDetailData {
|
||||
sessions: SessionSummary[];
|
||||
words: AnimeWord[];
|
||||
cardEvents: EpisodeCardEvent[];
|
||||
}
|
||||
|
||||
export interface KanjiDetailData {
|
||||
detail: {
|
||||
kanjiId: number;
|
||||
kanji: string;
|
||||
frequency: number;
|
||||
firstSeen: number;
|
||||
lastSeen: number;
|
||||
};
|
||||
animeAppearances: Array<{
|
||||
animeId: number;
|
||||
animeTitle: string;
|
||||
occurrenceCount: number;
|
||||
}>;
|
||||
words: Array<{
|
||||
wordId: number;
|
||||
headword: string;
|
||||
word: string;
|
||||
reading: string;
|
||||
frequency: number;
|
||||
}>;
|
||||
}
|
||||
Reference in New Issue
Block a user