mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-03-31 06:48:39 +00:00
329 lines
11 KiB
TypeScript
329 lines
11 KiB
TypeScript
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<string, StreamingContent>;
|
|
recentContent: StreamingContent[];
|
|
librarySubscribers: Array<(items: StreamingContent[]) => void>;
|
|
libraryAddListeners: Array<(item: StreamingContent) => void>;
|
|
libraryRemoveListeners: Array<(type: string, id: string) => void>;
|
|
initPromise: Promise<void>;
|
|
isInitialized: boolean;
|
|
}
|
|
|
|
export function createLibraryKey(type: string, id: string): string {
|
|
return `${type}:${id}`;
|
|
}
|
|
|
|
export async function initializeCatalogState(state: CatalogLibraryState): Promise<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<string, StreamingContent> = {};
|
|
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<StreamingContent[]> {
|
|
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<void> {
|
|
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<void> {
|
|
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;
|
|
}
|