diff --git a/extension/background.js b/extension/background.js index 9f44a08..44dfe5a 100644 --- a/extension/background.js +++ b/extension/background.js @@ -1,6 +1,7 @@ import { EVENTS, PROTOCOL_VERSION, OFFICIAL_SERVER_URL, OFFICIAL_SERVER_TOKEN, EPISODE_LOBBY_TIMEOUT, FORCE_SYNC_TIMEOUT } from './shared/constants.js'; import { generateUsername } from './shared/names.js'; import { loadLocale, getMessage, getSystemLanguage } from './i18n.js'; +import { sameEpisode } from './episode-utils.js'; // --- Uninstall URL Initialization --- chrome.runtime.onInstalled.addListener((details) => { @@ -191,25 +192,7 @@ let forceSyncTimeout = null; let episodeLobby = null; // { expectedTitle, initiatorPeerId, readyPeers: [], createdAt } let episodeLobbyTimeout = null; -// --- Episode Title Extraction (synced with content.js) --- -function extractEpisodeId(title) { - if (!title || typeof title !== 'string') return null; - const se = title.match(/S(?:eason\s*)?(\d+)[\s\-\.]*E(?:pisode\s*)?(\d+)/i); - if (se) return `S${String(se[1]).padStart(2, '0')}E${String(se[2]).padStart(2, '0')}`; - const ep = title.match(/(?:Episode|Folge|Ep\.?|#)\s*(\d+)/i); - if (ep) return `EP${String(ep[1]).padStart(3, '0')}`; - return null; -} - -function sameEpisode(titleA, titleB) { - if (!titleA && !titleB) return true; // Both unknown → assume same (backward compat) - if (!titleA || !titleB) return false; // One unknown, one known → different - const idA = extractEpisodeId(titleA); - const idB = extractEpisodeId(titleB); - if (idA && idB) return idA === idB; // Both have parseable IDs → compare IDs - if (idA || idB) return false; // One has ID, other doesn't → different - return titleA === titleB; // Neither has ID → exact string match -} +// --- Episode Title Extraction (synced with content.js via episode-utils.js) --- // --- Storage Utils --- diff --git a/extension/content.js b/extension/content.js index b91c6c2..ce1118b 100644 --- a/extension/content.js +++ b/extension/content.js @@ -290,28 +290,27 @@ // Extract a canonical episode identifier from a title string. // Handles: S01E01, S1E1, S01 - E01, Season 1 Episode 1, "Folge 5", "Episode 5", "Ep. 5", "#5" // Returns null if no episode pattern found. + // --- SHARED_EPISODE_UTILS_INJECT_START --- + // This block is automatically replaced by /scripts/build-extension.js function extractEpisodeId(title) { if (!title || typeof title !== 'string') return null; - // S01E01 patterns (with any non-alphanumeric separator between season and E) const se = title.match(/S(?:eason\s*)?(\d+)[^a-zA-Z0-9]*E(?:pisode\s*)?(\d+)/i); if (se) return `S${String(se[1]).padStart(2, '0')}E${String(se[2]).padStart(2, '0')}`; - // "Episode X", "Folge X", "Ep. X", "#X" const ep = title.match(/(?:Episode|Folge|Ep\.?|#)\s*(\d+)/i); if (ep) return `EP${String(ep[1]).padStart(3, '0')}`; return null; } - // Returns true if two titles likely refer to the same episode. - // Strict: both must have IDs and match, OR neither has IDs and exact match. function sameEpisode(titleA, titleB) { - if (!titleA && !titleB) return true; // Both unknown → assume same (backward compat) - if (!titleA || !titleB) return false; // One unknown, one known → different + if (!titleA && !titleB) return true; + if (!titleA || !titleB) return false; const idA = extractEpisodeId(titleA); const idB = extractEpisodeId(titleB); - if (idA && idB) return idA === idB; // Both have parseable IDs → compare IDs - if (idA || idB) return false; // One has ID, other doesn't → different - return titleA === titleB; // Neither has ID → exact string match + if (idA && idB) return idA === idB; + if (idA || idB) return false; + return titleA === titleB; } + // --- SHARED_EPISODE_UTILS_INJECT_END --- // Returns true only when we are CERTAIN the episodes differ. // Permissive: only blocks if BOTH titles have parseable IDs AND they differ. diff --git a/extension/episode-utils.js b/extension/episode-utils.js new file mode 100644 index 0000000..ab68b07 --- /dev/null +++ b/extension/episode-utils.js @@ -0,0 +1,24 @@ +/** + * KoalaSync Episode Title Utilities + * Single source of truth — synced to content.js by build-extension.js. + * Keep in sync with the injection block in content.js! + */ + +export function extractEpisodeId(title) { + if (!title || typeof title !== 'string') return null; + const se = title.match(/S(?:eason\s*)?(\d+)[^a-zA-Z0-9]*E(?:pisode\s*)?(\d+)/i); + if (se) return `S${String(se[1]).padStart(2, '0')}E${String(se[2]).padStart(2, '0')}`; + const ep = title.match(/(?:Episode|Folge|Ep\.?|#)\s*(\d+)/i); + if (ep) return `EP${String(ep[1]).padStart(3, '0')}`; + return null; +} + +export function sameEpisode(titleA, titleB) { + if (!titleA && !titleB) return true; + if (!titleA || !titleB) return false; + const idA = extractEpisodeId(titleA); + const idB = extractEpisodeId(titleB); + if (idA && idB) return idA === idB; + if (idA || idB) return false; + return titleA === titleB; +} diff --git a/scripts/build-extension.js b/scripts/build-extension.js index ffcb971..4f69299 100644 --- a/scripts/build-extension.js +++ b/scripts/build-extension.js @@ -97,6 +97,25 @@ function copyExtensionFiles(targetDir, browserName) { console.warn('⚠️ WARNING: Heartbeat markers not found in content.js'); } + // 3. Inject Episode Utils + const euStart = '// --- SHARED_EPISODE_UTILS_INJECT_START ---'; + const euEnd = '// --- SHARED_EPISODE_UTILS_INJECT_END ---'; + const euPattern = new RegExp(euStart.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '[\\s\\S]+?' + euEnd.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')); + const euPath = path.join(rootDir, 'extension', 'episode-utils.js'); + if (fs.existsSync(euPath)) { + const euContent = fs.readFileSync(euPath, 'utf8'); + const stripped = euContent + .replace(/^\/\*\*[\s\S]*?\*\/\s*/m, '') + .replace(/export function /g, 'function ') + .trim(); + const euRep = `${euStart}\n // This block is automatically updated by /scripts/build-extension.js\n${stripped.split('\n').map(l => ' ' + l).join('\n')}\n ${euEnd}`; + if (euPattern.test(content)) { + content = content.replace(euPattern, euRep); + } else { + console.warn('⚠ WARNING: Episode utils markers not found in content.js'); + } + } + fs.writeFileSync(destPath, content); console.log('✓ Injected shared constants into content.js'); } else if (item === 'background.js') {