import { notificationService } from '../notificationService'; import { mmkvStorage } from '../mmkvStorage'; import { logger } from '../../utils/logger'; import type { StreamingContent } from './types'; export interface CatalogLibraryState { LEGACY_LIBRARY_KEY: string; RECENT_CONTENT_KEY: string; MAX_RECENT_ITEMS: number; library: Record; recentContent: StreamingContent[]; librarySubscribers: Array<(items: StreamingContent[]) => void>; libraryAddListeners: Array<(item: StreamingContent) => void>; libraryRemoveListeners: Array<(type: string, id: string) => void>; initPromise: Promise; isInitialized: boolean; } export function createLibraryKey(type: string, id: string): string { return `${type}:${id}`; } export async function initializeCatalogState(state: CatalogLibraryState): Promise { logger.log('[CatalogService] Starting initialization...'); try { logger.log('[CatalogService] Step 1: Initializing scope...'); await initializeScope(); logger.log('[CatalogService] Step 2: Loading library...'); await loadLibrary(state); logger.log('[CatalogService] Step 3: Loading recent content...'); await loadRecentContent(state); state.isInitialized = true; logger.log( `[CatalogService] Initialization completed successfully. Library contains ${Object.keys(state.library).length} items.` ); } catch (error) { logger.error('[CatalogService] Initialization failed:', error); state.isInitialized = true; } } export async function ensureCatalogInitialized(state: CatalogLibraryState): Promise { logger.log(`[CatalogService] ensureInitialized() called. isInitialized: ${state.isInitialized}`); try { await state.initPromise; logger.log( `[CatalogService] ensureInitialized() completed. Library ready with ${Object.keys(state.library).length} items.` ); } catch (error) { logger.error('[CatalogService] Error waiting for initialization:', error); } } async function initializeScope(): Promise { try { const currentScope = await mmkvStorage.getItem('@user:current'); if (!currentScope) { await mmkvStorage.setItem('@user:current', 'local'); logger.log('[CatalogService] Initialized @user:current scope to "local"'); return; } logger.log(`[CatalogService] Using existing scope: "${currentScope}"`); } catch (error) { logger.error('[CatalogService] Failed to initialize scope:', error); } } async function loadLibrary(state: CatalogLibraryState): Promise { try { const scope = (await mmkvStorage.getItem('@user:current')) || 'local'; const scopedKey = `@user:${scope}:stremio-library`; let storedLibrary = await mmkvStorage.getItem(scopedKey); if (!storedLibrary) { storedLibrary = await mmkvStorage.getItem(state.LEGACY_LIBRARY_KEY); if (storedLibrary) { await mmkvStorage.setItem(scopedKey, storedLibrary); } } if (storedLibrary) { const parsedLibrary = JSON.parse(storedLibrary); logger.log( `[CatalogService] Raw library data type: ${Array.isArray(parsedLibrary) ? 'ARRAY' : 'OBJECT'}, keys: ${JSON.stringify(Object.keys(parsedLibrary).slice(0, 5))}` ); if (Array.isArray(parsedLibrary)) { logger.log('[CatalogService] WARNING: Library is stored as ARRAY format. Converting to OBJECT format.'); const libraryObject: Record = {}; for (const item of parsedLibrary) { libraryObject[createLibraryKey(item.type, item.id)] = item; } state.library = libraryObject; logger.log(`[CatalogService] Converted ${parsedLibrary.length} items from array to object format`); const normalizedLibrary = JSON.stringify(state.library); await mmkvStorage.setItem(scopedKey, normalizedLibrary); await mmkvStorage.setItem(state.LEGACY_LIBRARY_KEY, normalizedLibrary); logger.log('[CatalogService] Re-saved library in correct format'); } else { state.library = parsedLibrary; } logger.log( `[CatalogService] Library loaded successfully with ${Object.keys(state.library).length} items from scope: ${scope}` ); } else { logger.log(`[CatalogService] No library data found for scope: ${scope}`); state.library = {}; } await mmkvStorage.setItem('@user:current', scope); } catch (error: any) { logger.error('Failed to load library:', error); state.library = {}; } } async function saveLibrary(state: CatalogLibraryState): Promise { if (state.isInitialized) { await ensureCatalogInitialized(state); } try { const itemCount = Object.keys(state.library).length; const scope = (await mmkvStorage.getItem('@user:current')) || 'local'; const scopedKey = `@user:${scope}:stremio-library`; const libraryData = JSON.stringify(state.library); logger.log(`[CatalogService] Saving library with ${itemCount} items to scope: "${scope}" (key: ${scopedKey})`); await mmkvStorage.setItem(scopedKey, libraryData); await mmkvStorage.setItem(state.LEGACY_LIBRARY_KEY, libraryData); logger.log(`[CatalogService] Library saved successfully with ${itemCount} items`); } catch (error: any) { logger.error('Failed to save library:', error); logger.error( `[CatalogService] Library save failed details - scope: ${(await mmkvStorage.getItem('@user:current')) || 'unknown'}, itemCount: ${Object.keys(state.library).length}` ); } } async function loadRecentContent(state: CatalogLibraryState): Promise { try { const storedRecentContent = await mmkvStorage.getItem(state.RECENT_CONTENT_KEY); if (storedRecentContent) { state.recentContent = JSON.parse(storedRecentContent); } } catch (error: any) { logger.error('Failed to load recent content:', error); } } async function saveRecentContent(state: CatalogLibraryState): Promise { try { await mmkvStorage.setItem(state.RECENT_CONTENT_KEY, JSON.stringify(state.recentContent)); } catch (error: any) { logger.error('Failed to save recent content:', error); } } function notifyLibrarySubscribers(state: CatalogLibraryState): void { const items = Object.values(state.library); state.librarySubscribers.forEach(callback => callback(items)); } export async function getLibraryItems(state: CatalogLibraryState): Promise { if (!state.isInitialized) { await ensureCatalogInitialized(state); } return Object.values(state.library); } export function subscribeToLibraryUpdates( state: CatalogLibraryState, callback: (items: StreamingContent[]) => void ): () => void { state.librarySubscribers.push(callback); Promise.resolve().then(() => { getLibraryItems(state).then(items => { if (state.librarySubscribers.includes(callback)) { callback(items); } }); }); return () => { const index = state.librarySubscribers.indexOf(callback); if (index > -1) { state.librarySubscribers.splice(index, 1); } }; } export function onLibraryAdd( state: CatalogLibraryState, listener: (item: StreamingContent) => void ): () => void { state.libraryAddListeners.push(listener); return () => { state.libraryAddListeners = state.libraryAddListeners.filter(currentListener => currentListener !== listener); }; } export function onLibraryRemove( state: CatalogLibraryState, listener: (type: string, id: string) => void ): () => void { state.libraryRemoveListeners.push(listener); return () => { state.libraryRemoveListeners = state.libraryRemoveListeners.filter( currentListener => currentListener !== listener ); }; } export async function addToLibrary(state: CatalogLibraryState, content: StreamingContent): Promise { logger.log(`[CatalogService] addToLibrary() called for: ${content.type}:${content.id} (${content.name})`); await ensureCatalogInitialized(state); const key = createLibraryKey(content.type, content.id); const itemCountBefore = Object.keys(state.library).length; logger.log(`[CatalogService] Adding to library with key: "${key}". Current library keys: [${Object.keys(state.library).length}] items`); state.library[key] = { ...content, addedToLibraryAt: Date.now(), }; const itemCountAfter = Object.keys(state.library).length; logger.log( `[CatalogService] Library updated: ${itemCountBefore} -> ${itemCountAfter} items. New library keys: [${Object.keys(state.library).slice(0, 5).join(', ')}${Object.keys(state.library).length > 5 ? '...' : ''}]` ); await saveLibrary(state); logger.log(`[CatalogService] addToLibrary() completed for: ${content.type}:${content.id}`); notifyLibrarySubscribers(state); try { state.libraryAddListeners.forEach(listener => listener(content)); } catch {} if (content.type === 'series') { try { await notificationService.updateNotificationsForSeries(content.id); console.log(`[CatalogService] Auto-setup notifications for series: ${content.name}`); } catch (error) { console.error(`[CatalogService] Failed to setup notifications for ${content.name}:`, error); } } } export async function removeFromLibrary( state: CatalogLibraryState, type: string, id: string ): Promise { logger.log(`[CatalogService] removeFromLibrary() called for: ${type}:${id}`); await ensureCatalogInitialized(state); const key = createLibraryKey(type, id); const itemCountBefore = Object.keys(state.library).length; const itemExisted = key in state.library; logger.log( `[CatalogService] Removing key: "${key}". Currently library has ${itemCountBefore} items with keys: [${Object.keys(state.library).slice(0, 5).join(', ')}${Object.keys(state.library).length > 5 ? '...' : ''}]` ); delete state.library[key]; const itemCountAfter = Object.keys(state.library).length; logger.log(`[CatalogService] Library updated: ${itemCountBefore} -> ${itemCountAfter} items (existed: ${itemExisted})`); await saveLibrary(state); logger.log(`[CatalogService] removeFromLibrary() completed for: ${type}:${id}`); notifyLibrarySubscribers(state); try { state.libraryRemoveListeners.forEach(listener => listener(type, id)); } catch {} if (type === 'series') { try { const scheduledNotifications = notificationService.getScheduledNotifications(); const seriesToCancel = scheduledNotifications.filter(notification => notification.seriesId === id); for (const notification of seriesToCancel) { await notificationService.cancelNotification(notification.id); } console.log(`[CatalogService] Cancelled ${seriesToCancel.length} notifications for removed series: ${id}`); } catch (error) { console.error(`[CatalogService] Failed to cancel notifications for removed series ${id}:`, error); } } } export function addToRecentContent(state: CatalogLibraryState, content: StreamingContent): void { state.recentContent = state.recentContent.filter(item => !(item.id === content.id && item.type === content.type)); state.recentContent.unshift(content); if (state.recentContent.length > state.MAX_RECENT_ITEMS) { state.recentContent = state.recentContent.slice(0, state.MAX_RECENT_ITEMS); } void saveRecentContent(state); } export function getRecentContent(state: CatalogLibraryState): StreamingContent[] { return state.recentContent; }