diff --git a/apps/electron-backend/src/app/api/main.preload.ts b/apps/electron-backend/src/app/api/main.preload.ts index c22da1104..b07746125 100644 --- a/apps/electron-backend/src/app/api/main.preload.ts +++ b/apps/electron-backend/src/app/api/main.preload.ts @@ -396,22 +396,18 @@ const electronApi = { dbDeletePlaylist: (playlistId: string, operationId?: string) => ipcRenderer.invoke('DB_DELETE_PLAYLIST', playlistId, operationId), dbDeleteXtreamContent: (playlistId: string, operationId?: string) => - ipcRenderer.invoke( - 'DB_DELETE_XTREAM_CONTENT', - playlistId, - operationId - ), + ipcRenderer.invoke('DB_DELETE_XTREAM_CONTENT', playlistId, operationId), dbRestoreXtreamUserData: ( playlistId: string, - favoritedXtreamIds: number[], - recentlyViewedXtreamIds: { xtreamId: number; viewedAt: string }[], + favorites: unknown[], + recentlyViewed: unknown[], operationId?: string ) => ipcRenderer.invoke( 'DB_RESTORE_XTREAM_USER_DATA', playlistId, - favoritedXtreamIds, - recentlyViewedXtreamIds, + favorites, + recentlyViewed, operationId ), dbHasCategories: (playlistId: string, type: string) => @@ -500,7 +496,8 @@ const electronApi = { dbGetFavorites: (playlistId: string) => ipcRenderer.invoke('DB_GET_FAVORITES', playlistId), dbGetGlobalFavorites: () => ipcRenderer.invoke('DB_GET_GLOBAL_FAVORITES'), - dbGetAllGlobalFavorites: () => ipcRenderer.invoke('DB_GET_ALL_GLOBAL_FAVORITES'), + dbGetAllGlobalFavorites: () => + ipcRenderer.invoke('DB_GET_ALL_GLOBAL_FAVORITES'), dbReorderGlobalFavorites: ( updates: { content_id: number; position: number }[] ) => ipcRenderer.invoke('DB_REORDER_GLOBAL_FAVORITES', updates), @@ -562,6 +559,8 @@ const electronApi = { ), dbGetAllPlaybackPositions: (playlistId: string) => ipcRenderer.invoke('DB_GET_ALL_PLAYBACK_POSITIONS', playlistId), + dbClearAllPlaybackPositions: (playlistId: string) => + ipcRenderer.invoke('DB_CLEAR_ALL_PLAYBACK_POSITIONS', playlistId), dbClearPlaybackPosition: ( playlistId: string, contentXtreamId: number, diff --git a/apps/electron-backend/src/app/database/operations/favorites.operations.ts b/apps/electron-backend/src/app/database/operations/favorites.operations.ts index cc8465ac3..ac0ba974d 100644 --- a/apps/electron-backend/src/app/database/operations/favorites.operations.ts +++ b/apps/electron-backend/src/app/database/operations/favorites.operations.ts @@ -58,10 +58,7 @@ export async function isFavorite( return result[0].count > 0; } -export async function getFavorites( - db: AppDatabase, - playlistId: string -) { +export async function getFavorites(db: AppDatabase, playlistId: string) { return db .select({ id: schema.content.id, @@ -73,6 +70,7 @@ export async function getFavorites( xtream_id: schema.content.xtreamId, type: schema.content.type, added_at: schema.favorites.addedAt, + position: schema.favorites.position, }) .from(schema.favorites) .innerJoin( @@ -80,7 +78,10 @@ export async function getFavorites( eq(schema.favorites.contentId, schema.content.id) ) .where(eq(schema.favorites.playlistId, playlistId)) - .orderBy(desc(schema.favorites.addedAt)); + .orderBy( + asc(schema.favorites.position), + desc(schema.favorites.addedAt) + ); } export async function getGlobalFavorites(db: AppDatabase) { diff --git a/apps/electron-backend/src/app/database/operations/playback-position.operations.ts b/apps/electron-backend/src/app/database/operations/playback-position.operations.ts index 91a861aa4..3668a2c01 100644 --- a/apps/electron-backend/src/app/database/operations/playback-position.operations.ts +++ b/apps/electron-backend/src/app/database/operations/playback-position.operations.ts @@ -2,12 +2,7 @@ import { and, desc, eq, sql } from 'drizzle-orm'; import * as schema from 'database-schema'; import type { AppDatabase } from '../database.types'; -type PlaylistType = - | 'xtream' - | 'stalker' - | 'm3u-file' - | 'm3u-text' - | 'm3u-url'; +type PlaylistType = 'xtream' | 'stalker' | 'm3u-file' | 'm3u-text' | 'm3u-url'; type PlaybackPositionPayload = { contentXtreamId: number; @@ -98,10 +93,7 @@ export async function getPlaybackPosition( .where( and( eq(schema.playbackPositions.playlistId, playlistId), - eq( - schema.playbackPositions.contentXtreamId, - contentXtreamId - ), + eq(schema.playbackPositions.contentXtreamId, contentXtreamId), eq(schema.playbackPositions.contentType, contentType) ) ) @@ -150,6 +142,17 @@ export async function getAllPlaybackPositions( .where(eq(schema.playbackPositions.playlistId, playlistId)); } +export async function clearAllPlaybackPositions( + db: AppDatabase, + playlistId: string +): Promise<{ success: boolean }> { + await db + .delete(schema.playbackPositions) + .where(eq(schema.playbackPositions.playlistId, playlistId)); + + return { success: true }; +} + export async function clearPlaybackPosition( db: AppDatabase, playlistId: string, @@ -161,10 +164,7 @@ export async function clearPlaybackPosition( .where( and( eq(schema.playbackPositions.playlistId, playlistId), - eq( - schema.playbackPositions.contentXtreamId, - contentXtreamId - ), + eq(schema.playbackPositions.contentXtreamId, contentXtreamId), eq(schema.playbackPositions.contentType, contentType) ) ); diff --git a/apps/electron-backend/src/app/database/operations/xtream.operations.ts b/apps/electron-backend/src/app/database/operations/xtream.operations.ts index 747d8d2b4..7b51ec8ee 100644 --- a/apps/electron-backend/src/app/database/operations/xtream.operations.ts +++ b/apps/electron-backend/src/app/database/operations/xtream.operations.ts @@ -1,5 +1,10 @@ import { and, eq, inArray } from 'drizzle-orm'; import * as schema from 'database-schema'; +import type { + XtreamBackupFavoriteItem, + XtreamBackupHiddenCategory, + XtreamBackupRecentlyViewedItem, +} from 'shared-interfaces'; import type { AppDatabase } from '../database.types'; import { checkpointOperation, @@ -10,15 +15,28 @@ import { const DEFAULT_BATCH_SIZE = 100; +type ContentIdentity = { + id: number; + xtreamId: number; + contentType: XtreamBackupFavoriteItem['contentType']; +}; + +function toContentIdentityKey( + contentType: XtreamBackupFavoriteItem['contentType'], + xtreamId: number +): string { + return `${contentType}:${xtreamId}`; +} + export async function deleteXtreamContent( db: AppDatabase, playlistId: string, control?: OperationControl ): Promise<{ success: boolean; - favoritedXtreamIds: number[]; - recentlyViewedXtreamIds: { xtreamId: number; viewedAt: string }[]; - hiddenCategories: { xtreamId: number; type: string }[]; + favorites: XtreamBackupFavoriteItem[]; + recentlyViewed: XtreamBackupRecentlyViewedItem[]; + hiddenCategories: XtreamBackupHiddenCategory[]; }> { const categories = await db .select({ @@ -35,16 +53,19 @@ export async function deleteXtreamContent( .filter((category) => category.hidden) .map((category) => ({ xtreamId: category.xtreamId, - type: category.type, + categoryType: category.type, })); - let favoritedXtreamIds: number[] = []; - let recentlyViewedXtreamIds: { xtreamId: number; viewedAt: string }[] = []; + let favorites: XtreamBackupFavoriteItem[] = []; + let recentlyViewed: XtreamBackupRecentlyViewedItem[] = []; if (categoryIds.length > 0) { - const favorites = await db + const favoritedContent = await db .select({ xtreamId: schema.content.xtreamId, + contentType: schema.content.type, + addedAt: schema.favorites.addedAt, + position: schema.favorites.position, }) .from(schema.favorites) .innerJoin( @@ -58,11 +79,17 @@ export async function deleteXtreamContent( ) ); - favoritedXtreamIds = favorites.map((favorite) => favorite.xtreamId); + favorites = favoritedContent.map((favorite) => ({ + xtreamId: favorite.xtreamId, + contentType: favorite.contentType, + addedAt: favorite.addedAt ?? undefined, + position: favorite.position, + })); - const recentlyViewed = await db + const recentlyViewedContent = await db .select({ xtreamId: schema.content.xtreamId, + contentType: schema.content.type, viewedAt: schema.recentlyViewed.viewedAt, }) .from(schema.recentlyViewed) @@ -77,8 +104,9 @@ export async function deleteXtreamContent( ) ); - recentlyViewedXtreamIds = recentlyViewed.map((item) => ({ + recentlyViewed = recentlyViewedContent.map((item) => ({ xtreamId: item.xtreamId, + contentType: item.contentType, viewedAt: item.viewedAt || new Date().toISOString(), })); @@ -127,119 +155,164 @@ export async function deleteXtreamContent( return { success: true, - favoritedXtreamIds, - recentlyViewedXtreamIds, + favorites, + recentlyViewed, hiddenCategories, }; } +async function getContentIdentityMap( + db: AppDatabase, + playlistId: string, + identities: Array<{ + contentType: XtreamBackupFavoriteItem['contentType']; + xtreamId: number; + }> +): Promise> { + const xtreamIds = Array.from( + new Set(identities.map((item) => item.xtreamId)) + ); + + if (xtreamIds.length === 0) { + return new Map(); + } + + const content = await db + .select({ + id: schema.content.id, + xtreamId: schema.content.xtreamId, + contentType: schema.content.type, + }) + .from(schema.content) + .innerJoin( + schema.categories, + eq(schema.content.categoryId, schema.categories.id) + ) + .where( + and( + eq(schema.categories.playlistId, playlistId), + inArray(schema.content.xtreamId, xtreamIds) + ) + ); + + return new Map( + content.map((item: ContentIdentity) => [ + toContentIdentityKey(item.contentType, item.xtreamId), + item.id, + ]) + ); +} + export async function restoreXtreamUserData( db: AppDatabase, playlistId: string, - favoritedXtreamIds: number[], - recentlyViewedXtreamIds: { xtreamId: number; viewedAt: string }[], + favorites: XtreamBackupFavoriteItem[], + recentlyViewed: XtreamBackupRecentlyViewedItem[], control?: OperationControl ): Promise<{ success: boolean }> { - if (favoritedXtreamIds.length > 0) { - const content = await db - .select({ - id: schema.content.id, - xtreamId: schema.content.xtreamId, - }) - .from(schema.content) - .innerJoin( - schema.categories, - eq(schema.content.categoryId, schema.categories.id) - ) - .where( - and( - eq(schema.categories.playlistId, playlistId), - inArray(schema.content.xtreamId, favoritedXtreamIds) - ) + await checkpointOperation(control); + await db + .delete(schema.favorites) + .where(eq(schema.favorites.playlistId, playlistId)); + + await checkpointOperation(control); + await db + .delete(schema.recentlyViewed) + .where(eq(schema.recentlyViewed.playlistId, playlistId)); + + const contentByIdentity = await getContentIdentityMap(db, playlistId, [ + ...favorites.map((item) => ({ + contentType: item.contentType, + xtreamId: item.xtreamId, + })), + ...recentlyViewed.map((item) => ({ + contentType: item.contentType, + xtreamId: item.xtreamId, + })), + ]); + + const favoriteValues = favorites + .map((item, index) => { + const contentId = contentByIdentity.get( + toContentIdentityKey(item.contentType, item.xtreamId) ); - let currentFavorites = 0; - const totalFavorites = content.length; + if (!contentId) { + return null; + } - for (const chunk of chunkValues(content, DEFAULT_BATCH_SIZE)) { - await checkpointOperation(control); - await db.insert(schema.favorites).values( - chunk.map((item) => ({ - contentId: item.id, - playlistId, - addedAt: new Date().toISOString(), - })) - ); - currentFavorites += chunk.length; - await reportOperationProgress(control, { - phase: 'restoring-favorites', - current: currentFavorites, - total: totalFavorites, - increment: chunk.length, - }); - } - } - - if (recentlyViewedXtreamIds.length > 0) { - const xtreamIds = recentlyViewedXtreamIds.map((item) => item.xtreamId); - const content = await db - .select({ - id: schema.content.id, - xtreamId: schema.content.xtreamId, - }) - .from(schema.content) - .innerJoin( - schema.categories, - eq(schema.content.categoryId, schema.categories.id) - ) - .where( - and( - eq(schema.categories.playlistId, playlistId), - inArray(schema.content.xtreamId, xtreamIds) - ) - ); - - const xtreamIdToContentId = new Map( - content.map((item) => [item.xtreamId, item.id]) + return { + contentId, + playlistId, + addedAt: item.addedAt ?? new Date().toISOString(), + position: item.position ?? index, + }; + }) + .filter( + ( + value + ): value is { + contentId: number; + playlistId: string; + addedAt: string; + position: number | null; + } => value !== null ); - const values = recentlyViewedXtreamIds - .map((item) => { - const contentId = xtreamIdToContentId.get(item.xtreamId); - if (!contentId) { - return null; - } + let restoredFavorites = 0; + const totalFavorites = favoriteValues.length; - return { - contentId, - playlistId, - viewedAt: item.viewedAt, - }; - }) - .filter( - ( - value - ): value is { - contentId: number; - playlistId: string; - viewedAt: string; - } => value !== null + for (const chunk of chunkValues(favoriteValues, DEFAULT_BATCH_SIZE)) { + await checkpointOperation(control); + await db.insert(schema.favorites).values(chunk); + restoredFavorites += chunk.length; + await reportOperationProgress(control, { + phase: 'restoring-favorites', + current: restoredFavorites, + total: totalFavorites, + increment: chunk.length, + }); + } + + const recentlyViewedValues = recentlyViewed + .map((item) => { + const contentId = contentByIdentity.get( + toContentIdentityKey(item.contentType, item.xtreamId) ); - let currentRecentlyViewed = 0; - const totalRecentlyViewed = values.length; + if (!contentId) { + return null; + } - for (const chunk of chunkValues(values, DEFAULT_BATCH_SIZE)) { - await checkpointOperation(control); - await db.insert(schema.recentlyViewed).values(chunk); - currentRecentlyViewed += chunk.length; - await reportOperationProgress(control, { - phase: 'restoring-recently-viewed', - current: currentRecentlyViewed, - total: totalRecentlyViewed, - increment: chunk.length, - }); - } + return { + contentId, + playlistId, + viewedAt: item.viewedAt, + }; + }) + .filter( + ( + value + ): value is { + contentId: number; + playlistId: string; + viewedAt: string; + } => value !== null + ); + + let restoredRecentlyViewed = 0; + const totalRecentlyViewed = recentlyViewedValues.length; + + for (const chunk of chunkValues(recentlyViewedValues, DEFAULT_BATCH_SIZE)) { + await checkpointOperation(control); + await db.insert(schema.recentlyViewed).values(chunk); + restoredRecentlyViewed += chunk.length; + await reportOperationProgress(control, { + phase: 'restoring-recently-viewed', + current: restoredRecentlyViewed, + total: totalRecentlyViewed, + increment: chunk.length, + }); } return { success: true }; diff --git a/apps/electron-backend/src/app/events/database/playback-position.events.ts b/apps/electron-backend/src/app/events/database/playback-position.events.ts index 592a535da..ae007c9d6 100644 --- a/apps/electron-backend/src/app/events/database/playback-position.events.ts +++ b/apps/electron-backend/src/app/events/database/playback-position.events.ts @@ -42,8 +42,12 @@ handleWorkerRequest( }) ); +handleWorkerRequest('DB_GET_ALL_PLAYBACK_POSITIONS', (playlistId: string) => ({ + playlistId, +})); + handleWorkerRequest( - 'DB_GET_ALL_PLAYBACK_POSITIONS', + 'DB_CLEAR_ALL_PLAYBACK_POSITIONS', (playlistId: string) => ({ playlistId, }) diff --git a/apps/electron-backend/src/app/events/database/xtream.events.ts b/apps/electron-backend/src/app/events/database/xtream.events.ts index bfec7e43c..e1933ee17 100644 --- a/apps/electron-backend/src/app/events/database/xtream.events.ts +++ b/apps/electron-backend/src/app/events/database/xtream.events.ts @@ -30,8 +30,8 @@ ipcMain.handle( async ( event, playlistId: string, - favoritedXtreamIds: number[], - recentlyViewedXtreamIds: { xtreamId: number; viewedAt: string }[], + favorites: unknown[], + recentlyViewed: unknown[], operationId?: string ) => { try { @@ -40,8 +40,8 @@ ipcMain.handle( 'DB_RESTORE_XTREAM_USER_DATA', { playlistId, - favoritedXtreamIds, - recentlyViewedXtreamIds, + favorites, + recentlyViewed, operationId, } ); diff --git a/apps/electron-backend/src/app/workers/database-worker.types.ts b/apps/electron-backend/src/app/workers/database-worker.types.ts index 5967e5495..c3d9d7aaa 100644 --- a/apps/electron-backend/src/app/workers/database-worker.types.ts +++ b/apps/electron-backend/src/app/workers/database-worker.types.ts @@ -43,6 +43,7 @@ export const DB_WORKER_OPERATIONS = [ 'DB_GET_SERIES_PLAYBACK_POSITIONS', 'DB_GET_RECENT_PLAYBACK_POSITIONS', 'DB_GET_ALL_PLAYBACK_POSITIONS', + 'DB_CLEAR_ALL_PLAYBACK_POSITIONS', 'DB_CLEAR_PLAYBACK_POSITION', ] as const; diff --git a/apps/electron-backend/src/app/workers/database.worker.ts b/apps/electron-backend/src/app/workers/database.worker.ts index 794119716..2f42eb8a5 100644 --- a/apps/electron-backend/src/app/workers/database.worker.ts +++ b/apps/electron-backend/src/app/workers/database.worker.ts @@ -3,6 +3,10 @@ import { getWorkerDatabase, } from './database.worker-connection'; import { parentPort } from 'worker_threads'; +import type { + XtreamBackupFavoriteItem, + XtreamBackupRecentlyViewedItem, +} from 'shared-interfaces'; import type { DbOperationEvent, DbWorkerIncomingMessage, @@ -40,6 +44,7 @@ import { searchContent, } from '../database/operations/content.operations'; import { + clearAllPlaybackPositions, clearPlaybackPosition, getAllPlaybackPositions, getPlaybackPosition, @@ -167,9 +172,7 @@ function createOperationController(config: { const { operationId, operation, playlistId, requestId } = config; const cancellable = config.cancellable ?? true; const activeState = - operationId && cancellable - ? { cancelled: false } - : null; + operationId && cancellable ? { cancelled: false } : null; if (operationId && activeState) { activeOperations.set(operationId, activeState); @@ -232,8 +235,7 @@ function createOperationController(config: { emitError: (error, event) => { send('error', { ...event, - error: - error instanceof Error ? error.message : String(error), + error: error instanceof Error ? error.message : String(error), }); }, cleanup: () => { @@ -614,8 +616,8 @@ async function executeRequest(message: DbWorkerRequestMessage) { case 'DB_RESTORE_XTREAM_USER_DATA': { const payload = message.payload as { playlistId: string; - favoritedXtreamIds: number[]; - recentlyViewedXtreamIds: { xtreamId: number; viewedAt: string }[]; + favorites: XtreamBackupFavoriteItem[]; + recentlyViewed: XtreamBackupRecentlyViewedItem[]; operationId?: string; }; @@ -628,8 +630,8 @@ async function executeRequest(message: DbWorkerRequestMessage) { }, async (controller) => { const totalItems = - payload.favoritedXtreamIds.length + - payload.recentlyViewedXtreamIds.length; + payload.favorites.length + + payload.recentlyViewed.length; controller.emitStarted({ phase: DB_OPERATION_PHASES.RESTORING_FAVORITES, current: 0, @@ -639,8 +641,8 @@ async function executeRequest(message: DbWorkerRequestMessage) { const result = await restoreXtreamUserData( db, payload.playlistId, - payload.favoritedXtreamIds, - payload.recentlyViewedXtreamIds, + payload.favorites, + payload.recentlyViewed, controller.control ); @@ -740,7 +742,12 @@ async function executeRequest(message: DbWorkerRequestMessage) { episodeNumber?: number; positionSeconds: number; durationSeconds?: number; - playlistType?: 'xtream' | 'stalker' | 'm3u-file' | 'm3u-text' | 'm3u-url'; + playlistType?: + | 'xtream' + | 'stalker' + | 'm3u-file' + | 'm3u-text' + | 'm3u-url'; }; }; return savePlaybackPosition(db, payload.playlistId, payload.data); @@ -789,6 +796,11 @@ async function executeRequest(message: DbWorkerRequestMessage) { return getAllPlaybackPositions(db, payload.playlistId); } + case 'DB_CLEAR_ALL_PLAYBACK_POSITIONS': { + const payload = message.payload as { playlistId: string }; + return clearAllPlaybackPositions(db, payload.playlistId); + } + case 'DB_CLEAR_PLAYBACK_POSITION': { const payload = message.payload as { playlistId: string; @@ -823,7 +835,11 @@ parentPort.on('message', async (message: DbWorkerIncomingMessage) => { result, }); } catch (error) { - console.error(loggerLabel, `Error handling ${message.operation}:`, error); + console.error( + loggerLabel, + `Error handling ${message.operation}:`, + error + ); postMessage({ type: 'response', requestId: message.requestId, diff --git a/apps/web/src/typings.d.ts b/apps/web/src/typings.d.ts index b7549d1bc..884ff303c 100644 --- a/apps/web/src/typings.d.ts +++ b/apps/web/src/typings.d.ts @@ -14,6 +14,9 @@ import { PlaylistRefreshEvent, PlaylistRefreshPayload, PortalDebugEvent, + XtreamBackupFavoriteItem, + XtreamBackupHiddenCategory, + XtreamBackupRecentlyViewedItem, XtreamCategory, } from 'shared-interfaces'; import { @@ -81,9 +84,7 @@ declare global { headers?: Record ) => Promise; autoUpdatePlaylists: (playlists: Playlist[]) => Promise; - fetchEpg: ( - urls: string[] - ) => Promise<{ + fetchEpg: (urls: string[]) => Promise<{ success: boolean; message?: string; skipped?: string[]; @@ -171,23 +172,14 @@ declare global { operationId?: string ) => Promise<{ success: boolean; - favoritedXtreamIds: number[]; - recentlyViewedXtreamIds: { - xtreamId: number; - viewedAt: string; - }[]; - hiddenCategories: { - xtreamId: number; - type: string; - }[]; + favorites: XtreamBackupFavoriteItem[]; + recentlyViewed: XtreamBackupRecentlyViewedItem[]; + hiddenCategories: XtreamBackupHiddenCategory[]; }>; dbRestoreXtreamUserData: ( playlistId: string, - favoritedXtreamIds: number[], - recentlyViewedXtreamIds: { - xtreamId: number; - viewedAt: string; - }[], + favorites: XtreamBackupFavoriteItem[], + recentlyViewed: XtreamBackupRecentlyViewedItem[], operationId?: string ) => Promise<{ success: boolean }>; dbHasCategories: ( @@ -381,6 +373,9 @@ declare global { dbGetAllPlaybackPositions: ( playlistId: string ) => Promise; + dbClearAllPlaybackPositions: ( + playlistId: string + ) => Promise<{ success: boolean }>; dbClearPlaybackPosition: ( playlistId: string, contentXtreamId: number, diff --git a/global.d.ts b/global.d.ts index 7fc139412..7b43cdba5 100644 --- a/global.d.ts +++ b/global.d.ts @@ -1,5 +1,11 @@ import 'jest-extended'; import { ExternalPlayerSession } from './libs/shared/interfaces/src/lib/external-player-session.interface'; +import { PlaybackPositionData } from './libs/shared/interfaces/src/lib/playback-position.interface'; +import { + XtreamBackupFavoriteItem, + XtreamBackupHiddenCategory, + XtreamBackupRecentlyViewedItem, +} from './libs/shared/interfaces/src/lib/playlist-backup.interface'; import { PlaylistRefreshEvent, PlaylistRefreshPayload, @@ -156,23 +162,14 @@ declare global { operationId?: string ) => Promise<{ success: boolean; - favoritedXtreamIds: number[]; - recentlyViewedXtreamIds: { - xtreamId: number; - viewedAt: string; - }[]; - hiddenCategories: { - xtreamId: number; - type: string; - }[]; + favorites: XtreamBackupFavoriteItem[]; + recentlyViewed: XtreamBackupRecentlyViewedItem[]; + hiddenCategories: XtreamBackupHiddenCategory[]; }>; dbRestoreXtreamUserData: ( playlistId: string, - favoritedXtreamIds: number[], - recentlyViewedXtreamIds: { - xtreamId: number; - viewedAt: string; - }[], + favorites: XtreamBackupFavoriteItem[], + recentlyViewed: XtreamBackupRecentlyViewedItem[], operationId?: string ) => Promise<{ success: boolean }>; dbHasCategories: ( @@ -346,8 +343,13 @@ declare global { dbGetRecentPlaybackPositions: ( playlistId: string, limit?: number - ) => Promise; - dbGetAllPlaybackPositions: (playlistId: string) => Promise; + ) => Promise; + dbGetAllPlaybackPositions: ( + playlistId: string + ) => Promise; + dbClearAllPlaybackPositions: ( + playlistId: string + ) => Promise<{ success: boolean }>; dbClearPlaybackPosition: ( playlistId: string, contentXtreamId: number,