mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-09-19 05:16:27 -07:00
fix(stats): keep same-title anime and YouTube records separate
- Scope title identity, aliases, AniList matching, and sync by media kind - Migrate legacy schemas while preserving IDs, history, and assignments
This commit is contained in:
@@ -571,6 +571,8 @@ function ensureSubtitleLineEventIndex(db: DatabaseSync): void {
|
||||
}
|
||||
|
||||
export function getOrCreateAnimeRecord(db: DatabaseSync, input: AnimeRecordInput): number {
|
||||
const mediaKind = input.mediaKind ?? 'anime';
|
||||
const anilistId = mediaKind === 'anime' ? input.anilistId : null;
|
||||
const seasonScope = normalizeSeasonScope(input.seasonScope);
|
||||
const identityTitle = buildSeasonScopedAnimeTitle(input.parsedTitle, seasonScope);
|
||||
const canonicalTitle =
|
||||
@@ -582,17 +584,23 @@ export function getOrCreateAnimeRecord(db: DatabaseSync, input: AnimeRecordInput
|
||||
}
|
||||
|
||||
const byAnilistId =
|
||||
input.anilistId !== null
|
||||
? (db.prepare('SELECT anime_id FROM imm_anime WHERE anilist_id = ?').get(input.anilistId) as {
|
||||
anilistId !== null
|
||||
? (db
|
||||
.prepare("SELECT anime_id FROM imm_anime WHERE anilist_id = ? AND media_kind = 'anime'")
|
||||
.get(anilistId) as {
|
||||
anime_id: number;
|
||||
} | null)
|
||||
: null;
|
||||
const byNormalizedTitle = db
|
||||
.prepare('SELECT anime_id FROM imm_anime WHERE normalized_title_key = ?')
|
||||
.get(normalizedTitleKey) as { anime_id: number } | null;
|
||||
.prepare('SELECT anime_id FROM imm_anime WHERE normalized_title_key = ? AND media_kind = ?')
|
||||
.get(normalizedTitleKey, mediaKind) as { anime_id: number } | null;
|
||||
const byTitleAlias = db
|
||||
.prepare('SELECT anime_id FROM imm_anime_title_aliases WHERE normalized_title_key = ?')
|
||||
.get(normalizedTitleKey) as { anime_id: number } | null;
|
||||
.prepare(
|
||||
`SELECT a.anime_id FROM imm_anime_title_aliases AS alias
|
||||
JOIN imm_anime AS a ON a.anime_id = alias.anime_id
|
||||
WHERE alias.normalized_title_key = ? AND a.media_kind = ?`,
|
||||
)
|
||||
.get(normalizedTitleKey, mediaKind) as { anime_id: number } | null;
|
||||
const existing = byAnilistId ?? byNormalizedTitle ?? byTitleAlias;
|
||||
if (existing?.anime_id) {
|
||||
// An alias remembers an intentionally merged-away spelling. Reusing it
|
||||
@@ -604,7 +612,7 @@ export function getOrCreateAnimeRecord(db: DatabaseSync, input: AnimeRecordInput
|
||||
SET
|
||||
media_kind = COALESCE(?, media_kind),
|
||||
canonical_title = COALESCE(NULLIF(?, ''), canonical_title),
|
||||
anilist_id = COALESCE(?, anilist_id),
|
||||
anilist_id = CASE WHEN ? = 'youtube' THEN NULL ELSE COALESCE(?, anilist_id) END,
|
||||
title_romaji = COALESCE(?, title_romaji),
|
||||
title_english = COALESCE(?, title_english),
|
||||
title_native = COALESCE(?, title_native),
|
||||
@@ -615,7 +623,8 @@ export function getOrCreateAnimeRecord(db: DatabaseSync, input: AnimeRecordInput
|
||||
).run(
|
||||
byAnilistId || byNormalizedTitle ? (input.mediaKind ?? null) : null,
|
||||
canonicalTitleUpdate,
|
||||
input.anilistId,
|
||||
mediaKind,
|
||||
anilistId,
|
||||
input.titleRomaji,
|
||||
input.titleEnglish,
|
||||
input.titleNative,
|
||||
@@ -648,7 +657,7 @@ export function getOrCreateAnimeRecord(db: DatabaseSync, input: AnimeRecordInput
|
||||
input.mediaKind ?? 'anime',
|
||||
normalizedTitleKey,
|
||||
canonicalTitle,
|
||||
input.anilistId,
|
||||
anilistId,
|
||||
input.titleRomaji,
|
||||
input.titleEnglish,
|
||||
input.titleNative,
|
||||
@@ -882,13 +891,61 @@ function migrateLegacyAnimeMetadata(db: DatabaseSync): void {
|
||||
}
|
||||
}
|
||||
|
||||
// SQLite cannot drop a table-level UNIQUE constraint. Rebuild with IDs intact
|
||||
// and foreign keys disabled so dependent history and manual assignments survive.
|
||||
function migrateAnimeTitleUniqueness(db: DatabaseSync): void {
|
||||
const schema = db.prepare("SELECT sql FROM sqlite_master WHERE name = 'imm_anime'").get() as {
|
||||
sql: string;
|
||||
};
|
||||
if (/normalized_title_key TEXT NOT NULL UNIQUE/i.test(schema.sql)) {
|
||||
const foreignKeys = db.prepare('PRAGMA foreign_keys').get() as { foreign_keys: number };
|
||||
const sequence = db
|
||||
.prepare("SELECT seq FROM sqlite_sequence WHERE name = 'imm_anime'")
|
||||
.get() as { seq: number } | null;
|
||||
db.exec('PRAGMA foreign_keys = OFF');
|
||||
try {
|
||||
db.exec('BEGIN IMMEDIATE');
|
||||
db.exec(
|
||||
schema.sql
|
||||
.replace(
|
||||
/CREATE TABLE (?:IF NOT EXISTS )?["`]?imm_anime["`]?/i,
|
||||
'CREATE TABLE imm_anime_new',
|
||||
)
|
||||
.replace(
|
||||
/normalized_title_key TEXT NOT NULL UNIQUE/i,
|
||||
'normalized_title_key TEXT NOT NULL',
|
||||
),
|
||||
);
|
||||
db.exec(`INSERT INTO imm_anime_new SELECT * FROM imm_anime;
|
||||
DROP TABLE imm_anime;
|
||||
ALTER TABLE imm_anime_new RENAME TO imm_anime;`);
|
||||
if (sequence) {
|
||||
db.prepare("UPDATE sqlite_sequence SET seq = MAX(seq, ?) WHERE name = 'imm_anime'").run(
|
||||
sequence.seq,
|
||||
);
|
||||
}
|
||||
db.exec('COMMIT');
|
||||
} catch (error) {
|
||||
db.exec('ROLLBACK');
|
||||
throw error;
|
||||
} finally {
|
||||
db.exec(`PRAGMA foreign_keys = ${foreignKeys.foreign_keys}`);
|
||||
}
|
||||
}
|
||||
db.exec(`CREATE UNIQUE INDEX IF NOT EXISTS idx_anime_kind_title
|
||||
ON imm_anime(media_kind, normalized_title_key)`);
|
||||
}
|
||||
|
||||
// Older builds can create channel rows with the default anime kind even after
|
||||
// the schema upgrade. Repair classification on every startup without moving videos.
|
||||
function classifyYoutubeChannels(db: DatabaseSync): void {
|
||||
db.exec(`
|
||||
UPDATE imm_anime
|
||||
SET media_kind = 'youtube'
|
||||
SET media_kind = 'youtube', anilist_id = NULL
|
||||
WHERE media_kind = 'anime'
|
||||
AND NOT EXISTS (SELECT 1 FROM imm_anime AS channel
|
||||
WHERE channel.media_kind = 'youtube'
|
||||
AND channel.normalized_title_key = imm_anime.normalized_title_key)
|
||||
AND (
|
||||
normalized_title_key LIKE 'youtube channel %'
|
||||
OR CASE WHEN json_valid(metadata_json)
|
||||
@@ -932,7 +989,7 @@ export function ensureSchema(db: DatabaseSync): void {
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS imm_anime(
|
||||
anime_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
normalized_title_key TEXT NOT NULL UNIQUE,
|
||||
normalized_title_key TEXT NOT NULL,
|
||||
canonical_title TEXT NOT NULL,
|
||||
anilist_id INTEGER UNIQUE,
|
||||
title_romaji TEXT,
|
||||
@@ -951,7 +1008,6 @@ export function ensureSchema(db: DatabaseSync): void {
|
||||
'media_kind',
|
||||
"TEXT NOT NULL DEFAULT 'anime' CHECK(media_kind IN ('anime', 'youtube'))",
|
||||
);
|
||||
classifyYoutubeChannels(db);
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS imm_videos(
|
||||
video_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
@@ -1495,6 +1551,8 @@ export function ensureSchema(db: DatabaseSync): void {
|
||||
);
|
||||
}
|
||||
|
||||
migrateAnimeTitleUniqueness(db);
|
||||
classifyYoutubeChannels(db);
|
||||
migrateSessionEventTimestampsToText(db);
|
||||
|
||||
ensureLexicalDailyRollupTables(db);
|
||||
|
||||
Reference in New Issue
Block a user