diff --git a/src/app/services/database.service.ts b/src/app/services/database.service.ts index 98257fd20..fb19b09c3 100644 --- a/src/app/services/database.service.ts +++ b/src/app/services/database.service.ts @@ -1,5 +1,34 @@ import { Injectable } from '@angular/core'; import Database from '@tauri-apps/plugin-sql'; +import { PlaylistMeta } from '../shared/playlist-meta.type'; + +export interface XCategoryFromDb { + id: number; + name: string; + playlist_id: string; + type: 'movies' | 'live' | 'series'; + xtream_id: number; +} + +export interface XtreamContent { + id: number; + category_id: number; + title: string; + rating: string; + added: string; + poster_url: string; + xtream_id: number; + type: string; +} + +export interface XtreamPlaylist { + id: string; + name: string; + serverUrl: string; + username: string; + password: string; + type: string; +} @Injectable({ providedIn: 'root', @@ -128,4 +157,215 @@ export class DatabaseService { return false; } } + + async hasXtreamCategories( + playlistId: string, + type: 'live' | 'movies' | 'series' + ): Promise { + const db = await this.getConnection(); + const result = await db.select( + 'SELECT * FROM categories WHERE playlist_id = ? AND type = ?', + [playlistId, type] + ); + return result.length > 0; + } + + async getXtreamCategories( + playlistId: string, + type: 'live' | 'movies' | 'series' + ): Promise { + const db = await this.getConnection(); + return await db.select( + 'SELECT * FROM categories WHERE playlist_id = ? AND type = ?', + [playlistId, type] + ); + } + + async saveXtreamCategories( + playlistId: string, + categories: any[], + type: 'live' | 'movies' | 'series' + ): Promise { + const db = await this.getConnection(); + for (const category of categories) { + await db.execute( + 'INSERT INTO categories (playlist_id, name, type, xtream_id) VALUES (?, ?, ?, ?)', + [playlistId, category.category_name, type, category.category_id] + ); + } + } + + async hasXtreamContent( + playlistId: string, + type: 'live' | 'movie' | 'series' + ): Promise { + const db = await this.getConnection(); + const result = await db.select( + `SELECT c.* FROM content c + JOIN categories cat ON c.category_id = cat.id + WHERE cat.playlist_id = ? AND c.type = ? + ORDER BY c.added`, + [playlistId, type] + ); + return (result as any[]).length > 0; + } + + async getXtreamContent( + playlistId: string, + type: 'live' | 'movie' | 'series' + ): Promise { + const db = await this.getConnection(); + return await db.select( + `SELECT + c.id, c.category_id, c.title, c.rating, + c.added, c.poster_url, c.xtream_id, c.type + FROM content c + INNER JOIN categories cat ON c.category_id = cat.id + WHERE cat.playlist_id = ? AND c.type = ? + ORDER BY c.added DESC`, + [playlistId, type] + ); + } + + async saveXtreamContent( + playlistId: string, + streams: any[], + type: 'live' | 'movie' | 'series', + onProgress?: (count: number) => void + ): Promise { + const db = await this.getConnection(); + const dbType = + type === 'series' ? 'series' : type === 'movie' ? 'movies' : 'live'; + + const categories = await db.select<{ id: number; xtream_id: number }[]>( + 'SELECT id, xtream_id FROM categories WHERE playlist_id = ? AND type = ?', + [playlistId, dbType] + ); + + const categoryMap = new Map( + categories.map((c) => [parseInt(c.xtream_id.toString()), c.id]) + ); + + const bulkInsertData = this.prepareBulkInsertData( + streams, + type, + categoryMap + ); + return await this.executeBulkInsert(db, bulkInsertData, onProgress); + } + + async searchXtreamContent( + playlistId: string, + searchTerm: string, + types: string[] + ): Promise { + const db = await this.getConnection(); + const placeholders = types.map(() => '?').join(','); + return await db.select( + `SELECT c.* FROM content c + JOIN categories cat ON c.category_id = cat.id + WHERE (c.title LIKE ?) + AND cat.playlist_id = ? + AND c.type IN (${placeholders}) + LIMIT 50`, + [`%${searchTerm}%`, playlistId, ...types] + ); + } + + async getPlaylistById(playlistId: string): Promise { + const db = await this.getConnection(); + const results = await db.select( + 'SELECT * FROM playlists WHERE id = ?', + [playlistId] + ); + return results[0] || null; + } + + async createPlaylist(playlist: PlaylistMeta): Promise { + const db = await this.getConnection(); + await db.execute( + 'INSERT INTO playlists (id, name, serverUrl, username, password, type) VALUES (?, ?, ?, ?, ?, ?)', + [ + playlist._id, + playlist.title, + playlist.serverUrl, + playlist.username, + playlist.password, + 'xtream', + ] + ); + } + + private prepareBulkInsertData( + streams: any[], + type: string, + categoryMap: Map + ): any[] { + return streams + .map((stream) => { + const streamCategoryId = + type === 'series' + ? parseInt(stream.category_id || '0') + : parseInt(stream.category_id); + + const categoryId = categoryMap.get(streamCategoryId); + if (!categoryId) return null; + + const title = + type === 'series' + ? stream.title || + stream.name || + `Unknown Series ${stream.series_id}` + : stream.name || + stream.title || + `Unknown Stream ${stream.stream_id}`; + + return [ + categoryId, + title, + stream.rating || stream.rating_imdb || '', + type === 'series' + ? stream.last_modified || '' + : stream.added || '', + stream.stream_icon || stream.poster || stream.cover || '', + type === 'series' + ? parseInt(stream.series_id || '0') + : parseInt(stream.stream_id || '0'), + type, + ]; + }) + .filter((data) => data !== null); + } + + private async executeBulkInsert( + db: Database, + data: any[], + onProgress?: (count: number) => void + ): Promise { + const CHUNK_SIZE = 100; + let totalInserted = 0; + + for (let i = 0; i < data.length; i += CHUNK_SIZE) { + const chunk = data.slice(i, i + CHUNK_SIZE); + const placeholders = chunk + .map(() => '(?, ?, ?, ?, ?, ?, ?)') + .join(', '); + const query = ` + INSERT INTO content ( + category_id, title, rating, added, + poster_url, xtream_id, type + ) VALUES ${placeholders} + `; + + try { + await db.execute(query, chunk.flat()); + totalInserted += chunk.length; + onProgress?.(totalInserted); + } catch (err) { + console.error('Error in bulk insert chunk:', err); + } + } + + return totalInserted; + } } diff --git a/src/app/xtream-tauri/services/xtream-api.service.ts b/src/app/xtream-tauri/services/xtream-api.service.ts new file mode 100644 index 000000000..d03ffda93 --- /dev/null +++ b/src/app/xtream-tauri/services/xtream-api.service.ts @@ -0,0 +1,112 @@ +import { Injectable, inject } from '@angular/core'; +import { XtreamCodeActions } from '../../../../shared/xtream-code-actions'; +import { DataService } from '../../services/data.service'; + +export interface XtreamCredentials { + url: string; + username: string; + password: string; +} + +@Injectable({ + providedIn: 'root', +}) +export class XtreamApiService { + private dataService = inject(DataService); + + private buildUrl(baseUrl: string): string { + return `${baseUrl}/player_api.php`; + } + + async getCategories( + credentials: XtreamCredentials, + type: 'live' | 'vod' | 'series' + ) { + const action = this.getCategoryAction(type); + return await this.dataService.fetchData( + this.buildUrl(credentials.url), + { + action, + username: credentials.username, + password: credentials.password, + } + ); + } + + async getStreams( + credentials: XtreamCredentials, + type: 'live' | 'vod' | 'series' + ) { + const action = this.getStreamAction(type); + return await this.dataService.fetchData( + this.buildUrl(credentials.url), + { + action, + username: credentials.username, + password: credentials.password, + } + ); + } + + async getVodDetails(credentials: XtreamCredentials, vodId: string) { + return await this.dataService.fetchData( + this.buildUrl(credentials.url), + { + action: XtreamCodeActions.GetVodInfo, + username: credentials.username, + password: credentials.password, + vod_id: vodId, + } + ); + } + + async getSerialDetails(credentials: XtreamCredentials, serialId: string) { + return await this.dataService.fetchData( + this.buildUrl(credentials.url), + { + action: XtreamCodeActions.GetSeriesInfo, + username: credentials.username, + password: credentials.password, + series_id: serialId, + } + ); + } + + async getEpgData(credentials: XtreamCredentials, streamId: number) { + return await this.dataService.fetchData( + this.buildUrl(credentials.url), + { + action: 'get_short_epg', + username: credentials.username, + password: credentials.password, + stream_id: streamId, + } + ); + } + + private getCategoryAction( + type: 'live' | 'vod' | 'series' + ): XtreamCodeActions { + switch (type) { + case 'live': + return XtreamCodeActions.GetLiveCategories; + case 'vod': + return XtreamCodeActions.GetVodCategories; + case 'series': + return XtreamCodeActions.GetSeriesCategories; + } + } + + private getStreamAction( + type: 'live' | 'vod' | 'series' + ): XtreamCodeActions { + switch (type) { + case 'live': + return XtreamCodeActions.GetLiveStreams; + case 'vod': + return XtreamCodeActions.GetVodStreams; + case 'series': + return XtreamCodeActions.GetSeries; + } + } +} diff --git a/src/app/xtream-tauri/with-favorites.feature.ts b/src/app/xtream-tauri/with-favorites.feature.ts new file mode 100644 index 000000000..bec41359f --- /dev/null +++ b/src/app/xtream-tauri/with-favorites.feature.ts @@ -0,0 +1,84 @@ +import { inject } from '@angular/core'; +import { + patchState, + signalStoreFeature, + withMethods, + withState, +} from '@ngrx/signals'; +import { DatabaseService } from '../services/database.service'; +import { FavoritesService } from './services/favorites.service'; + +export const withFavorites = function () { + return signalStoreFeature( + withState({ + isFavorite: false, + }), + withMethods( + ( + store, + dbService = inject(DatabaseService), + favoritesService = inject(FavoritesService) + ) => ({ + async toggleFavorite(xtreamId: number, playlistId: string) { + if (!xtreamId || !playlistId) return; + + const db = await dbService.getConnection(); + + const content: any = await db.select( + 'SELECT content.id FROM content ' + + 'INNER JOIN categories ON content.category_id = categories.id ' + + 'WHERE content.xtream_id = ? AND categories.playlist_id = ?', + [xtreamId, playlistId] + ); + + if (!content || content.length === 0) { + console.error('Content not found in database'); + return; + } + + const contentId = content[0].id; + const isFavorite = await favoritesService.isFavorite( + xtreamId, + playlistId + ); + + if (isFavorite) { + await favoritesService.removeFromFavorites( + contentId, + playlistId + ); + } else { + await favoritesService.addToFavorites({ + content_id: contentId, + playlist_id: playlistId, + }); + } + + patchState(store, { isFavorite: !isFavorite }); + }, + + async checkFavoriteStatus( + xtreamId: number, + playlistId: string + ) { + if (!xtreamId || !playlistId) { + patchState(store, { isFavorite: false }); + return; + } + + if (!xtreamId) { + patchState(store, { isFavorite: false }); + return; + } + + const isFavorite = await favoritesService.isFavorite( + xtreamId, + playlistId + ); + + patchState(store, { isFavorite }); + }, + }) + ) + ); +}; diff --git a/src/app/xtream-tauri/xtream.store.ts b/src/app/xtream-tauri/xtream.store.ts index 01e839a1b..4dc3d91d9 100644 --- a/src/app/xtream-tauri/xtream.store.ts +++ b/src/app/xtream-tauri/xtream.store.ts @@ -10,41 +10,13 @@ import { import { rxMethod } from '@ngrx/signals/rxjs-interop'; import { Store } from '@ngrx/store'; import { EMPTY, from, lastValueFrom, pipe, switchMap, tap } from 'rxjs'; -import { XtreamCategory } from '../../../shared/xtream-category.interface'; import { XtreamCodeActions } from '../../../shared/xtream-code-actions'; -import { XtreamLiveStream } from '../../../shared/xtream-live-stream.interface'; -import { XtreamSerieItem } from '../../../shared/xtream-serie-item.interface'; -import { XtreamVodStream } from '../../../shared/xtream-vod-stream.interface'; import { DataService } from '../services/data.service'; import { DatabaseService } from '../services/database.service'; import { selectActivePlaylist } from '../state/selectors'; -import { EpgItem } from '../xtream/epg-item.interface'; -import { FavoritesService } from './services/favorites.service'; +import { withFavorites } from './with-favorites.feature'; import { withRecentItems } from './with-recent-items'; - -type XtreamState = { - isLoadingCategories: boolean; - isLoadingContent: boolean; - isImporting: boolean; - liveCategories: XtreamCategory[]; - vodCategories: XtreamCategory[]; - serialCategories: XtreamCategory[]; - liveStreams: XtreamLiveStream[]; - vodStreams: XtreamVodStream[]; - serialStreams: XtreamSerieItem[]; - page: number; - limit: number; - selectedCategoryId: number | null; - searchResults: any[]; - selectedContentType: 'live' | 'vod' | 'series'; - selectedItem: any | null; - importCount: number; - itemsToImport: number; - currentPlaylist: any | null; - isFavorite: boolean; - epgItems: EpgItem[]; - hideExternalInfoDialog: boolean; -}; +import { XtreamState } from './xtream-state'; const initialState: XtreamState = { isLoadingCategories: false, @@ -65,20 +37,11 @@ const initialState: XtreamState = { selectedItem: null, importCount: 0, currentPlaylist: null, - isFavorite: false, epgItems: [], hideExternalInfoDialog: localStorage.getItem('hideExternalInfoDialog') === 'true', }; -interface XCategoryFromDb { - id: number; - name: string; - playlist_id: string; // uuid - type: 'movies' | 'live' | 'series'; - xtream_id: number; -} - /** to decode epg */ function b64DecodeUnicode(str: string) { return decodeURIComponent( @@ -93,6 +56,7 @@ function b64DecodeUnicode(str: string) { export const XtreamStore = signalStore( withState(initialState), withRecentItems(), + withFavorites(), withComputed((store) => { return { getCategoriesBySelectedType: computed(() => { @@ -198,231 +162,8 @@ export const XtreamStore = signalStore( route = inject(ActivatedRoute), dataService = inject(DataService), dbService = inject(DatabaseService), - oldStore = inject(Store), - favoritesService = inject(FavoritesService) + oldStore = inject(Store) ) => { - const checkLocalData = async ( - type: 'live' | 'movies' | 'series' - ) => { - const playlistId = route.snapshot.params.id; - console.log('Checking local data...', playlistId, type); - const db = await dbService.getConnection(); - const result = await db.select( - 'SELECT * FROM categories WHERE playlist_id = ? AND type = ?', - [playlistId, type] - ); - console.log(`Local data for ${type}:`, result); - return result.length > 0; - }; - - const saveCategoriesToDb = async ( - categories: XtreamCategory[], - type: 'live' | 'movies' | 'series' - ) => { - const playlistId = route.snapshot.params.id; - const db = await dbService.getConnection(); - for (const category of categories) { - await db.execute( - 'INSERT INTO categories (playlist_id, name, type, xtream_id) VALUES (?, ?, ?, ?)', - [ - playlistId, - category.category_name, - type, - category.category_id, - ] - ); - } - }; - - const getLocalCategories = async ( - type: 'live' | 'movies' | 'series' - ) => { - const playlistId = route.snapshot.params.id; - - const db = await dbService.getConnection(); - return await db.select( - 'SELECT * FROM categories WHERE playlist_id = ? AND type = ?', - [playlistId, type] - ); - }; - - const checkLocalContent = async ( - type: 'live' | 'movie' | 'series' - ) => { - const playlistId = route.snapshot.params.id; - console.log('Checking local content...'); - const db = await dbService.getConnection(); - const result = await db.select( - `SELECT c.* FROM content c - JOIN categories cat ON c.category_id = cat.id - WHERE cat.playlist_id = ? AND c.type = ? - ORDER BY c.added`, - [playlistId, type] - ); - return (result as any[]).length > 0; - }; - - const saveContentToDb = async ( - streams: any[], - type: 'live' | 'movie' | 'series' - ) => { - const playlistId = route.snapshot.params.id; - - console.log('Saving content to db...', { - playlistId, - streamCount: streams.length, - type, - sampleStream: streams[0], - }); - - patchState(store, { - itemsToImport: store.itemsToImport() + streams.length, - }); - - try { - const db = await dbService.getConnection(); - const dbType = - type === 'series' - ? 'series' - : type === 'movie' - ? 'movies' - : 'live'; - - const categories: any[] = await db.select( - 'SELECT id, xtream_id FROM categories WHERE playlist_id = ? AND type = ?', - [playlistId, dbType] - ); - - console.log('Found categories in DB:', { - type: dbType, - count: categories.length, - categories: categories, - }); - - const categoryMap = new Map( - categories.map((c) => [parseInt(c.xtream_id), c.id]) - ); - - // Prepare bulk insert data - const bulkInsertData = streams - .map((stream) => { - const streamCategoryId = - type === 'series' - ? parseInt(stream.category_id || '0') - : parseInt(stream.category_id); - - const categoryId = - categoryMap.get(streamCategoryId); - if (!categoryId) return null; - - const title = - type === 'series' - ? stream.title || - stream.name || - `Unknown Series ${stream.series_id}` - : stream.name || - stream.title || - `Unknown Stream ${stream.stream_id}`; - - return [ - categoryId, - title, - stream.rating || stream.rating_imdb || '', - type === 'series' - ? stream.last_modified || '' - : stream.added || '', - stream.stream_icon || - stream.poster || - stream.cover || - '', - type === 'series' - ? parseInt(stream.series_id || '0') - : parseInt(stream.stream_id || '0'), - type, - ]; - }) - .filter((data) => data !== null); - - if (bulkInsertData.length > 0) { - // Process in chunks to avoid SQLite variable limit - // Each row has 7 variables, so we'll insert 100 rows at a time (700 variables) - const CHUNK_SIZE = 100; - const chunks = []; - - for ( - let i = 0; - i < bulkInsertData.length; - i += CHUNK_SIZE - ) { - chunks.push( - bulkInsertData.slice(i, i + CHUNK_SIZE) - ); - } - - let totalInserted = 0; - - for (const chunk of chunks) { - try { - // Construct the bulk insert query for this chunk - const placeholders = chunk - .map(() => '(?, ?, ?, ?, ?, ?, ?)') - .join(', '); - - const query = ` - INSERT INTO content ( - category_id, title, rating, added, - poster_url, xtream_id, type - ) VALUES ${placeholders} - `; - - // Flatten the chunk array for the bulk insert - const flattenedValues = chunk.flat(); - - await db.execute(query, flattenedValues); - totalInserted += chunk.length; - - console.log( - `Successfully inserted chunk of ${chunk.length} streams. Total: ${totalInserted}/${bulkInsertData.length}` - ); - } catch (err) { - console.error( - 'Error in bulk insert chunk:', - err - ); - } - } - - patchState(store, { - importCount: store.importCount() + totalInserted, - }); - - console.log( - `Completed bulk insert: ${totalInserted} of ${bulkInsertData.length} streams inserted` - ); - } - } catch (err) { - console.error('Error in saveContentToDb:', err); - } - }; - - const getLocalContent = async ( - type: 'live' | 'movie' | 'series' - ) => { - const playlistId = route.snapshot.params.id; - console.log('Getting local content...'); - const db = await dbService.getConnection(); - return await db.select( - `SELECT - c.id, c.category_id, c.title, c.rating, - c.added, c.poster_url, c.xtream_id, c.type - FROM content c - INNER JOIN categories cat ON c.category_id = cat.id - WHERE cat.playlist_id = ? AND c.type = ? - ORDER BY c.added DESC`, - [playlistId, type] - ); - }; - const fetchCategories = ( action: XtreamCodeActions, stateKey: keyof Pick< @@ -439,51 +180,45 @@ export const XtreamStore = signalStore( password: store.currentPlaylist().password, }; - return from(checkLocalData(type)).pipe( + return from( + dbService.hasXtreamCategories( + route.snapshot.params.id, + type + ) + ).pipe( switchMap(async (exists) => { try { if (exists) { const localData = - await getLocalCategories(type); + await dbService.getXtreamCategories( + route.snapshot.params.id, + type + ); patchState(store, { [stateKey]: localData, isLoadingCategories: false, }); return localData; } - console.log( - 'Fetching remote categories:', - store.currentPlaylist() - ); + const remoteData = await dataService.fetchData( `${store.currentPlaylist().serverUrl}/player_api.php`, queryParams ); if (remoteData && Array.isArray(remoteData)) { - console.log( - `Got remote ${type} categories:`, - remoteData.length + await dbService.saveXtreamCategories( + route.snapshot.params.id, + remoteData, + type ); - await saveCategoriesToDb(remoteData, type); - } else { - console.error( - 'Invalid remote data received:', - remoteData - ); - patchState(store, { - isLoadingCategories: false, - }); - return []; } - const localData: any = - await getLocalCategories(type); - console.log( - `Loaded ${type} categories from DB:`, - localData.length - ); - + const localData = + await dbService.getXtreamCategories( + route.snapshot.params.id, + type + ); patchState(store, { [stateKey]: localData, isLoadingCategories: false, @@ -518,11 +253,17 @@ export const XtreamStore = signalStore( password: store.currentPlaylist().password, }; - return from(checkLocalContent(type)).pipe( + return from( + dbService.hasXtreamContent(route.snapshot.params.id, type) + ).pipe( switchMap(async (exists) => { try { if (exists) { - const localData = await getLocalContent(type); + const localData = + await dbService.getXtreamContent( + route.snapshot.params.id, + type + ); patchState(store, { [stateKey]: localData, isLoadingContent: false, @@ -536,11 +277,28 @@ export const XtreamStore = signalStore( ); if (remoteData && Array.isArray(remoteData)) { - await saveContentToDb(remoteData, type); + patchState(store, { + itemsToImport: + store.itemsToImport() + + remoteData.length, + }); + const insertedCount = + await dbService.saveXtreamContent( + route.snapshot.params.id, + remoteData, + type, + (count) => + patchState(store, { + importCount: count, + }) + ); + console.log(`Inserted ${insertedCount} items`); } - const localData = await getLocalContent(type); - + const localData = await dbService.getXtreamContent( + route.snapshot.params.id, + type + ); patchState(store, { [stateKey]: localData, isLoadingContent: false, @@ -560,60 +318,31 @@ export const XtreamStore = signalStore( searchTerm: string, types: string[] ) => { - const db = await dbService.getConnection(); - const placeholders = types.map(() => '?').join(','); - const playlistId = route.snapshot.params.id; - const results: any[] = await db.select( - `SELECT c.* FROM content c - JOIN categories cat ON c.category_id = cat.id - WHERE (c.title LIKE ?) - AND cat.playlist_id = ? - AND c.type IN (${placeholders}) - LIMIT 50`, - [`%${searchTerm}%`, playlistId, ...types] + const results = await dbService.searchXtreamContent( + route.snapshot.params.id, + searchTerm, + types ); - patchState(store, { searchResults: results }); }; + const fetchXtreamPlaylist = rxMethod( pipe(() => { const playlistId = route.snapshot.params.id; if (!playlistId) return EMPTY; - return from(dbService.getConnection()).pipe( - switchMap((db) => - from( - db.select( - 'SELECT * FROM playlists WHERE id = ?', - [playlistId] - ) - ) - ), - switchMap(async (res) => { - if (res[0]) { - patchState(store, { currentPlaylist: res[0] }); + return from(dbService.getPlaylistById(playlistId)).pipe( + switchMap(async (playlist) => { + if (playlist) { + patchState(store, { + currentPlaylist: playlist, + }); } else { const playlist = oldStore.selectSignal( selectActivePlaylist )(); - console.log( - 'Fetched playlist from default store:', - playlist - ); if (playlist) { - const db = await dbService.getConnection(); - // Insert into DB - await db.execute( - 'INSERT INTO playlists (id, name, serverUrl, username, password, type) VALUES (?, ?, ?, ?, ?, ?)', - [ - playlist._id, - playlist.title, - playlist.serverUrl, - playlist.username, - playlist.password, - 'xtream', - ] - ); + await dbService.createPlaylist(playlist); patchState(store, { currentPlaylist: { ...playlist, @@ -907,69 +636,6 @@ export const XtreamStore = signalStore( return []; } }, - async toggleFavorite() { - const item = store.selectedItem(); - const playlist = store.currentPlaylist(); - - if (!item || !playlist) return; - - const db = await dbService.getConnection(); - const xtreamId = item.stream_id || item.series_id; - - const content: any = await db.select( - 'SELECT id FROM content WHERE xtream_id = ?', - [xtreamId] - ); - - if (!content || content.length === 0) { - console.error('Content not found in database'); - return; - } - - const contentId = content[0].id; - const isFavorite = await favoritesService.isFavorite( - xtreamId, - playlist.id - ); - - if (isFavorite) { - await favoritesService.removeFromFavorites( - contentId, - playlist.id - ); - } else { - await favoritesService.addToFavorites({ - content_id: contentId, - playlist_id: playlist.id, - }); - } - - patchState(store, { isFavorite: !isFavorite }); - }, - - async checkFavoriteStatus() { - const item = store.selectedItem(); - const playlist = store.currentPlaylist(); - - if (!item || !playlist) { - patchState(store, { isFavorite: false }); - return; - } - - const xtreamId = item.stream_id || item.series_id; - - if (!xtreamId) { - patchState(store, { isFavorite: false }); - return; - } - - const isFavorite = await favoritesService.isFavorite( - xtreamId, - playlist.id - ); - - patchState(store, { isFavorite }); - }, setHideExternalInfoDialog(hideExternalInfoDialog: boolean) { localStorage.setItem( 'hideExternalInfoDialog',