887 lines
26 KiB
TypeScript
887 lines
26 KiB
TypeScript
import {
|
|
closeWorkerDatabase,
|
|
getWorkerDatabase,
|
|
} from './database.worker-connection';
|
|
import { parentPort } from 'worker_threads';
|
|
import type {
|
|
XtreamBackupFavoriteItem,
|
|
XtreamBackupRecentlyViewedItem,
|
|
} from 'shared-interfaces';
|
|
import type {
|
|
DbOperationEvent,
|
|
DbWorkerIncomingMessage,
|
|
DbWorkerMessage,
|
|
DbWorkerRequestMessage,
|
|
} from './database-worker.types';
|
|
import {
|
|
DB_OPERATION_NAMES,
|
|
DB_OPERATION_PHASES,
|
|
} from './database-worker.types';
|
|
import {
|
|
getAllCategories,
|
|
getCategories,
|
|
hasCategories,
|
|
saveCategories,
|
|
updateCategoryVisibility,
|
|
} from '../database/operations/category.operations';
|
|
import {
|
|
addFavorite,
|
|
getAllGlobalFavorites,
|
|
getFavorites,
|
|
getGlobalFavorites,
|
|
isFavorite,
|
|
removeFavorite,
|
|
reorderGlobalFavorites,
|
|
} from '../database/operations/favorites.operations';
|
|
import {
|
|
clearXtreamImportCache,
|
|
getContent,
|
|
getContentByXtreamId,
|
|
getGlobalRecentlyAdded,
|
|
globalSearch,
|
|
hasContent,
|
|
saveContent,
|
|
searchContent,
|
|
} from '../database/operations/content.operations';
|
|
import { setContentBackdropIfMissing } from '../database/operations/content-backdrop.operations';
|
|
import {
|
|
clearAllPlaybackPositions,
|
|
clearPlaybackPosition,
|
|
getAllPlaybackPositions,
|
|
getPlaybackPosition,
|
|
getRecentPlaybackPositions,
|
|
getSeriesPlaybackPositions,
|
|
savePlaybackPosition,
|
|
} from '../database/operations/playback-position.operations';
|
|
import {
|
|
createPlaylist,
|
|
deleteAllPlaylists,
|
|
deletePlaylist,
|
|
getAppPlaylist,
|
|
getAppPlaylists,
|
|
getAppState,
|
|
getPlaylist,
|
|
setAppState,
|
|
updatePlaylist,
|
|
upsertAppPlaylist,
|
|
upsertAppPlaylists,
|
|
} from '../database/operations/playlist.operations';
|
|
import {
|
|
addRecentItem,
|
|
clearPlaylistRecentItems,
|
|
clearRecentlyViewed,
|
|
getRecentItems,
|
|
getRecentlyViewed,
|
|
removeRecentItem,
|
|
} from '../database/operations/recently-viewed.operations';
|
|
import {
|
|
deleteXtreamContent,
|
|
restoreXtreamUserData,
|
|
} from '../database/operations/xtream.operations';
|
|
|
|
const loggerLabel = '[DB Worker]';
|
|
const batchDelayMs = Number.parseInt(
|
|
process.env['IPTVNATOR_DB_WORKER_BATCH_DELAY_MS'] ?? '0',
|
|
10
|
|
);
|
|
|
|
type ActiveOperationState = {
|
|
cancelled: boolean;
|
|
};
|
|
|
|
type OperationController = {
|
|
control: {
|
|
checkpoint: () => Promise<void>;
|
|
onProgress: (progress: {
|
|
phase: string;
|
|
current?: number;
|
|
total?: number;
|
|
increment?: number;
|
|
}) => Promise<void>;
|
|
};
|
|
emitStarted: (event: Partial<DbOperationEvent> & { phase: string }) => void;
|
|
emitCompleted: (
|
|
event?: Partial<DbOperationEvent> & { phase?: string }
|
|
) => void;
|
|
emitCancelled: (
|
|
event?: Partial<DbOperationEvent> & { phase?: string }
|
|
) => void;
|
|
emitError: (
|
|
error: unknown,
|
|
event?: Partial<DbOperationEvent> & { phase?: string }
|
|
) => void;
|
|
cleanup: () => void;
|
|
};
|
|
|
|
const activeOperations = new Map<string, ActiveOperationState>();
|
|
|
|
if (!parentPort) {
|
|
throw new Error('Database worker must be started with a parent port');
|
|
}
|
|
|
|
function createAbortError(message: string): Error {
|
|
const error = new Error(message);
|
|
error.name = 'AbortError';
|
|
return error;
|
|
}
|
|
|
|
function isAbortError(error: unknown): boolean {
|
|
return error instanceof Error && error.name === 'AbortError';
|
|
}
|
|
|
|
function serializeError(error: unknown) {
|
|
if (error instanceof Error) {
|
|
return {
|
|
name: error.name,
|
|
message: error.message,
|
|
stack: error.stack,
|
|
};
|
|
}
|
|
|
|
return {
|
|
message: String(error),
|
|
};
|
|
}
|
|
|
|
function postMessage(message: DbWorkerMessage): void {
|
|
parentPort?.postMessage(message);
|
|
}
|
|
|
|
function postEvent(requestId: string, event: DbOperationEvent): void {
|
|
postMessage({
|
|
type: 'event',
|
|
requestId,
|
|
event,
|
|
});
|
|
}
|
|
|
|
async function pauseBetweenBatches(): Promise<void> {
|
|
if (batchDelayMs <= 0) {
|
|
return;
|
|
}
|
|
|
|
await new Promise((resolve) => setTimeout(resolve, batchDelayMs));
|
|
}
|
|
|
|
function createOperationController(config: {
|
|
operationId?: string;
|
|
operation: string;
|
|
playlistId?: string;
|
|
requestId: string;
|
|
cancellable?: boolean;
|
|
}): OperationController {
|
|
const { operationId, operation, playlistId, requestId } = config;
|
|
const cancellable = config.cancellable ?? true;
|
|
const activeState =
|
|
operationId && cancellable ? { cancelled: false } : null;
|
|
|
|
if (operationId && activeState) {
|
|
activeOperations.set(operationId, activeState);
|
|
}
|
|
|
|
let lastEvent: Partial<DbOperationEvent> = {};
|
|
|
|
const send = (
|
|
status: DbOperationEvent['status'],
|
|
event: Partial<DbOperationEvent> = {}
|
|
): void => {
|
|
const mergedEvent: DbOperationEvent = {
|
|
operationId,
|
|
operation,
|
|
playlistId,
|
|
status,
|
|
phase: event.phase ?? lastEvent.phase,
|
|
current: event.current ?? lastEvent.current,
|
|
total: event.total ?? lastEvent.total,
|
|
increment: event.increment,
|
|
error: event.error,
|
|
};
|
|
|
|
lastEvent = {
|
|
phase: mergedEvent.phase,
|
|
current: mergedEvent.current,
|
|
total: mergedEvent.total,
|
|
};
|
|
postEvent(requestId, mergedEvent);
|
|
};
|
|
|
|
const checkpoint = async (): Promise<void> => {
|
|
if (activeState?.cancelled) {
|
|
throw createAbortError(`Operation "${operation}" was cancelled`);
|
|
}
|
|
|
|
await pauseBetweenBatches();
|
|
|
|
if (activeState?.cancelled) {
|
|
throw createAbortError(`Operation "${operation}" was cancelled`);
|
|
}
|
|
};
|
|
|
|
return {
|
|
control: {
|
|
checkpoint,
|
|
onProgress: async (progress) => {
|
|
send('progress', progress);
|
|
},
|
|
},
|
|
emitStarted: (event) => {
|
|
send('started', event);
|
|
},
|
|
emitCompleted: (event) => {
|
|
send('completed', event);
|
|
},
|
|
emitCancelled: (event) => {
|
|
send('cancelled', event);
|
|
},
|
|
emitError: (error, event) => {
|
|
send('error', {
|
|
...event,
|
|
error: error instanceof Error ? error.message : String(error),
|
|
});
|
|
},
|
|
cleanup: () => {
|
|
if (operationId) {
|
|
activeOperations.delete(operationId);
|
|
}
|
|
},
|
|
};
|
|
}
|
|
|
|
async function executeTrackedOperation<TResult>(
|
|
config: Parameters<typeof createOperationController>[0],
|
|
handler: (controller: OperationController) => Promise<TResult>
|
|
): Promise<TResult> {
|
|
const controller = createOperationController(config);
|
|
|
|
try {
|
|
return await handler(controller);
|
|
} catch (error) {
|
|
if (isAbortError(error)) {
|
|
controller.emitCancelled();
|
|
} else {
|
|
controller.emitError(error);
|
|
}
|
|
throw error;
|
|
} finally {
|
|
controller.cleanup();
|
|
}
|
|
}
|
|
|
|
async function executeRequest(message: DbWorkerRequestMessage) {
|
|
const db = await getWorkerDatabase();
|
|
|
|
switch (message.operation) {
|
|
case 'DB_HAS_CATEGORIES': {
|
|
const payload = message.payload as {
|
|
playlistId: string;
|
|
type: 'live' | 'movies' | 'series';
|
|
};
|
|
return hasCategories(db, payload.playlistId, payload.type);
|
|
}
|
|
|
|
case 'DB_GET_CATEGORIES': {
|
|
const payload = message.payload as {
|
|
playlistId: string;
|
|
type: 'live' | 'movies' | 'series';
|
|
};
|
|
return getCategories(db, payload.playlistId, payload.type);
|
|
}
|
|
|
|
case 'DB_SAVE_CATEGORIES': {
|
|
const payload = message.payload as {
|
|
playlistId: string;
|
|
categories: Array<{
|
|
category_name: string;
|
|
category_id: string | number;
|
|
}>;
|
|
type: 'live' | 'movies' | 'series';
|
|
hiddenCategoryXtreamIds?: number[];
|
|
};
|
|
return saveCategories(
|
|
db,
|
|
payload.playlistId,
|
|
payload.categories,
|
|
payload.type,
|
|
payload.hiddenCategoryXtreamIds
|
|
);
|
|
}
|
|
|
|
case 'DB_GET_ALL_CATEGORIES': {
|
|
const payload = message.payload as {
|
|
playlistId: string;
|
|
type: 'live' | 'movies' | 'series';
|
|
};
|
|
return getAllCategories(db, payload.playlistId, payload.type);
|
|
}
|
|
|
|
case 'DB_UPDATE_CATEGORY_VISIBILITY': {
|
|
const payload = message.payload as {
|
|
categoryIds: number[];
|
|
hidden: boolean;
|
|
};
|
|
return updateCategoryVisibility(
|
|
db,
|
|
payload.categoryIds,
|
|
payload.hidden
|
|
);
|
|
}
|
|
|
|
case 'DB_HAS_CONTENT': {
|
|
const payload = message.payload as {
|
|
playlistId: string;
|
|
type: 'live' | 'movie' | 'series';
|
|
};
|
|
return hasContent(db, payload.playlistId, payload.type);
|
|
}
|
|
|
|
case 'DB_GET_CONTENT': {
|
|
const payload = message.payload as {
|
|
playlistId: string;
|
|
type: 'live' | 'movie' | 'series';
|
|
};
|
|
return getContent(db, payload.playlistId, payload.type);
|
|
}
|
|
|
|
case 'DB_GET_GLOBAL_RECENTLY_ADDED': {
|
|
const payload = message.payload as {
|
|
kind?: 'all' | 'vod' | 'series';
|
|
limit?: number;
|
|
playlistType?:
|
|
| 'xtream'
|
|
| 'stalker'
|
|
| 'm3u-file'
|
|
| 'm3u-text'
|
|
| 'm3u-url';
|
|
};
|
|
return getGlobalRecentlyAdded(
|
|
db,
|
|
payload.kind,
|
|
payload.limit,
|
|
payload.playlistType
|
|
);
|
|
}
|
|
|
|
case 'DB_SAVE_CONTENT': {
|
|
const payload = message.payload as {
|
|
playlistId: string;
|
|
streams: Array<Record<string, unknown>>;
|
|
type: 'live' | 'movie' | 'series';
|
|
operationId?: string;
|
|
};
|
|
|
|
return executeTrackedOperation(
|
|
{
|
|
requestId: message.requestId,
|
|
operation: DB_OPERATION_NAMES.SAVE_CONTENT,
|
|
operationId: payload.operationId,
|
|
playlistId: payload.playlistId,
|
|
},
|
|
async (controller) => {
|
|
controller.emitStarted({
|
|
phase: DB_OPERATION_PHASES.PREPARING_CONTENT,
|
|
current: 0,
|
|
total: payload.streams.length,
|
|
});
|
|
|
|
const result = await saveContent(
|
|
db,
|
|
payload.playlistId,
|
|
payload.streams,
|
|
payload.type,
|
|
controller.control
|
|
);
|
|
|
|
controller.emitCompleted({
|
|
phase: DB_OPERATION_PHASES.SAVING_CONTENT,
|
|
current: result.count,
|
|
total: result.count,
|
|
});
|
|
|
|
return result;
|
|
}
|
|
);
|
|
}
|
|
|
|
case 'DB_CLEAR_XTREAM_IMPORT_CACHE': {
|
|
const payload = message.payload as {
|
|
playlistId: string;
|
|
type: 'live' | 'movie' | 'series';
|
|
};
|
|
|
|
return clearXtreamImportCache(db, payload.playlistId, payload.type);
|
|
}
|
|
|
|
case 'DB_GET_CONTENT_BY_XTREAM_ID': {
|
|
const payload = message.payload as {
|
|
xtreamId: number;
|
|
playlistId: string;
|
|
contentType?: 'live' | 'movie' | 'series';
|
|
};
|
|
return getContentByXtreamId(
|
|
db,
|
|
payload.xtreamId,
|
|
payload.playlistId,
|
|
payload.contentType
|
|
);
|
|
}
|
|
|
|
case 'DB_SET_CONTENT_BACKDROP_IF_MISSING': {
|
|
const payload = message.payload as {
|
|
contentId: number;
|
|
backdropUrl?: string;
|
|
};
|
|
return setContentBackdropIfMissing(
|
|
db,
|
|
payload.contentId,
|
|
payload.backdropUrl
|
|
);
|
|
}
|
|
|
|
case 'DB_SEARCH_CONTENT': {
|
|
const payload = message.payload as {
|
|
playlistId: string;
|
|
searchTerm: string;
|
|
types: string[];
|
|
excludeHidden?: boolean;
|
|
};
|
|
return searchContent(
|
|
db,
|
|
payload.playlistId,
|
|
payload.searchTerm,
|
|
payload.types,
|
|
payload.excludeHidden
|
|
);
|
|
}
|
|
|
|
case 'DB_GLOBAL_SEARCH': {
|
|
const payload = message.payload as {
|
|
searchTerm: string;
|
|
types: string[];
|
|
excludeHidden?: boolean;
|
|
};
|
|
return globalSearch(
|
|
db,
|
|
payload.searchTerm,
|
|
payload.types,
|
|
payload.excludeHidden
|
|
);
|
|
}
|
|
|
|
case 'DB_CREATE_PLAYLIST': {
|
|
return createPlaylist(
|
|
db,
|
|
message.payload as {
|
|
id: string;
|
|
name: string;
|
|
serverUrl?: string;
|
|
username?: string;
|
|
password?: string;
|
|
macAddress?: string;
|
|
url?: string;
|
|
type: string;
|
|
}
|
|
);
|
|
}
|
|
|
|
case 'DB_UPSERT_APP_PLAYLIST': {
|
|
return upsertAppPlaylist(
|
|
db,
|
|
message.payload as Record<string, unknown>
|
|
);
|
|
}
|
|
|
|
case 'DB_UPSERT_APP_PLAYLISTS': {
|
|
return upsertAppPlaylists(
|
|
db,
|
|
message.payload as Record<string, unknown>[]
|
|
);
|
|
}
|
|
|
|
case 'DB_GET_APP_PLAYLISTS':
|
|
return getAppPlaylists(db);
|
|
|
|
case 'DB_GET_APP_PLAYLIST': {
|
|
const payload = message.payload as { playlistId: string };
|
|
return getAppPlaylist(db, payload.playlistId);
|
|
}
|
|
|
|
case 'DB_GET_PLAYLIST': {
|
|
const payload = message.payload as { playlistId: string };
|
|
return getPlaylist(db, payload.playlistId);
|
|
}
|
|
|
|
case 'DB_UPDATE_PLAYLIST': {
|
|
const payload = message.payload as {
|
|
playlistId: string;
|
|
updates: {
|
|
name?: string;
|
|
username?: string;
|
|
password?: string;
|
|
serverUrl?: string;
|
|
lastUpdated?: string;
|
|
};
|
|
};
|
|
return updatePlaylist(db, payload.playlistId, payload.updates);
|
|
}
|
|
|
|
case 'DB_DELETE_PLAYLIST': {
|
|
const payload = message.payload as {
|
|
playlistId: string;
|
|
operationId?: string;
|
|
};
|
|
|
|
return executeTrackedOperation(
|
|
{
|
|
requestId: message.requestId,
|
|
operation: DB_OPERATION_NAMES.DELETE_PLAYLIST,
|
|
operationId: payload.operationId,
|
|
playlistId: payload.playlistId,
|
|
},
|
|
async (controller) => {
|
|
controller.emitStarted({
|
|
phase: DB_OPERATION_PHASES.DELETING_FAVORITES,
|
|
current: 0,
|
|
});
|
|
|
|
const result = await deletePlaylist(
|
|
db,
|
|
payload.playlistId,
|
|
controller.control
|
|
);
|
|
|
|
controller.emitCompleted({
|
|
phase: DB_OPERATION_PHASES.DELETING_PLAYLIST,
|
|
current: 1,
|
|
total: 1,
|
|
});
|
|
|
|
return result;
|
|
}
|
|
);
|
|
}
|
|
|
|
case 'DB_GET_APP_STATE': {
|
|
const payload = message.payload as { key: string };
|
|
return getAppState(db, payload.key);
|
|
}
|
|
|
|
case 'DB_SET_APP_STATE': {
|
|
const payload = message.payload as { key: string; value: string };
|
|
return setAppState(db, payload.key, payload.value);
|
|
}
|
|
|
|
case 'DB_DELETE_ALL_PLAYLISTS': {
|
|
const payload = message.payload as { operationId?: string };
|
|
|
|
return executeTrackedOperation(
|
|
{
|
|
requestId: message.requestId,
|
|
operation: DB_OPERATION_NAMES.DELETE_ALL_PLAYLISTS,
|
|
operationId: payload.operationId,
|
|
cancellable: false,
|
|
},
|
|
async (controller) => {
|
|
controller.emitStarted({
|
|
phase: DB_OPERATION_PHASES.DELETING_FAVORITES,
|
|
current: 0,
|
|
total: 7,
|
|
});
|
|
|
|
const result = await deleteAllPlaylists(
|
|
db,
|
|
controller.control
|
|
);
|
|
|
|
controller.emitCompleted({
|
|
phase: DB_OPERATION_PHASES.DELETING_PLAYLISTS,
|
|
current: 7,
|
|
total: 7,
|
|
});
|
|
|
|
return result;
|
|
}
|
|
);
|
|
}
|
|
|
|
case 'DB_DELETE_XTREAM_CONTENT': {
|
|
const payload = message.payload as {
|
|
playlistId: string;
|
|
operationId?: string;
|
|
};
|
|
|
|
return executeTrackedOperation(
|
|
{
|
|
requestId: message.requestId,
|
|
operation: DB_OPERATION_NAMES.DELETE_XTREAM_CONTENT,
|
|
operationId: payload.operationId,
|
|
playlistId: payload.playlistId,
|
|
},
|
|
async (controller) => {
|
|
controller.emitStarted({
|
|
phase: DB_OPERATION_PHASES.COLLECTING_USER_DATA,
|
|
current: 0,
|
|
});
|
|
|
|
const result = await deleteXtreamContent(
|
|
db,
|
|
payload.playlistId,
|
|
controller.control
|
|
);
|
|
|
|
controller.emitCompleted({
|
|
phase: DB_OPERATION_PHASES.DELETING_CATEGORIES,
|
|
});
|
|
|
|
return result;
|
|
}
|
|
);
|
|
}
|
|
|
|
case 'DB_RESTORE_XTREAM_USER_DATA': {
|
|
const payload = message.payload as {
|
|
playlistId: string;
|
|
favorites: XtreamBackupFavoriteItem[];
|
|
recentlyViewed: XtreamBackupRecentlyViewedItem[];
|
|
operationId?: string;
|
|
};
|
|
|
|
return executeTrackedOperation(
|
|
{
|
|
requestId: message.requestId,
|
|
operation: DB_OPERATION_NAMES.RESTORE_XTREAM_USER_DATA,
|
|
operationId: payload.operationId,
|
|
playlistId: payload.playlistId,
|
|
},
|
|
async (controller) => {
|
|
const totalItems =
|
|
payload.favorites.length +
|
|
payload.recentlyViewed.length;
|
|
controller.emitStarted({
|
|
phase: DB_OPERATION_PHASES.RESTORING_FAVORITES,
|
|
current: 0,
|
|
total: totalItems,
|
|
});
|
|
|
|
const result = await restoreXtreamUserData(
|
|
db,
|
|
payload.playlistId,
|
|
payload.favorites,
|
|
payload.recentlyViewed,
|
|
controller.control
|
|
);
|
|
|
|
controller.emitCompleted({
|
|
phase: DB_OPERATION_PHASES.RESTORING_RECENTLY_VIEWED,
|
|
current: totalItems,
|
|
total: totalItems,
|
|
});
|
|
|
|
return result;
|
|
}
|
|
);
|
|
}
|
|
|
|
case 'DB_ADD_FAVORITE': {
|
|
const payload = message.payload as {
|
|
contentId: number;
|
|
playlistId: string;
|
|
backdropUrl?: string;
|
|
};
|
|
return addFavorite(db, payload.contentId, payload.playlistId, {
|
|
backdropUrl: payload.backdropUrl,
|
|
});
|
|
}
|
|
|
|
case 'DB_REMOVE_FAVORITE': {
|
|
const payload = message.payload as {
|
|
contentId: number;
|
|
playlistId: string;
|
|
};
|
|
return removeFavorite(db, payload.contentId, payload.playlistId);
|
|
}
|
|
|
|
case 'DB_IS_FAVORITE': {
|
|
const payload = message.payload as {
|
|
contentId: number;
|
|
playlistId: string;
|
|
};
|
|
return isFavorite(db, payload.contentId, payload.playlistId);
|
|
}
|
|
|
|
case 'DB_GET_FAVORITES': {
|
|
const payload = message.payload as { playlistId: string };
|
|
return getFavorites(db, payload.playlistId);
|
|
}
|
|
|
|
case 'DB_GET_GLOBAL_FAVORITES':
|
|
return getGlobalFavorites(db);
|
|
|
|
case 'DB_GET_ALL_GLOBAL_FAVORITES':
|
|
return getAllGlobalFavorites(db);
|
|
|
|
case 'DB_REORDER_GLOBAL_FAVORITES': {
|
|
const payload = message.payload as {
|
|
updates: { content_id: number; position: number }[];
|
|
};
|
|
return reorderGlobalFavorites(db, payload.updates);
|
|
}
|
|
|
|
case 'DB_GET_RECENTLY_VIEWED':
|
|
return getRecentlyViewed(db);
|
|
|
|
case 'DB_CLEAR_RECENTLY_VIEWED':
|
|
return clearRecentlyViewed(db);
|
|
|
|
case 'DB_GET_RECENT_ITEMS': {
|
|
const payload = message.payload as { playlistId: string };
|
|
return getRecentItems(db, payload.playlistId);
|
|
}
|
|
|
|
case 'DB_ADD_RECENT_ITEM': {
|
|
const payload = message.payload as {
|
|
contentId: number;
|
|
playlistId: string;
|
|
backdropUrl?: string;
|
|
};
|
|
return addRecentItem(db, payload.contentId, payload.playlistId, {
|
|
backdropUrl: payload.backdropUrl,
|
|
});
|
|
}
|
|
|
|
case 'DB_CLEAR_PLAYLIST_RECENT_ITEMS': {
|
|
const payload = message.payload as { playlistId: string };
|
|
return clearPlaylistRecentItems(db, payload.playlistId);
|
|
}
|
|
|
|
case 'DB_REMOVE_RECENT_ITEM': {
|
|
const payload = message.payload as {
|
|
contentId: number;
|
|
playlistId: string;
|
|
};
|
|
return removeRecentItem(db, payload.contentId, payload.playlistId);
|
|
}
|
|
|
|
case 'DB_SAVE_PLAYBACK_POSITION': {
|
|
const payload = message.payload as {
|
|
playlistId: string;
|
|
data: {
|
|
contentXtreamId: number;
|
|
contentType: 'vod' | 'episode';
|
|
seriesXtreamId?: number;
|
|
seasonNumber?: number;
|
|
episodeNumber?: number;
|
|
positionSeconds: number;
|
|
durationSeconds?: number;
|
|
playlistType?:
|
|
| 'xtream'
|
|
| 'stalker'
|
|
| 'm3u-file'
|
|
| 'm3u-text'
|
|
| 'm3u-url';
|
|
};
|
|
};
|
|
return savePlaybackPosition(db, payload.playlistId, payload.data);
|
|
}
|
|
|
|
case 'DB_GET_PLAYBACK_POSITION': {
|
|
const payload = message.payload as {
|
|
playlistId: string;
|
|
contentXtreamId: number;
|
|
contentType: 'vod' | 'episode';
|
|
};
|
|
return getPlaybackPosition(
|
|
db,
|
|
payload.playlistId,
|
|
payload.contentXtreamId,
|
|
payload.contentType
|
|
);
|
|
}
|
|
|
|
case 'DB_GET_SERIES_PLAYBACK_POSITIONS': {
|
|
const payload = message.payload as {
|
|
playlistId: string;
|
|
seriesXtreamId: number;
|
|
};
|
|
return getSeriesPlaybackPositions(
|
|
db,
|
|
payload.playlistId,
|
|
payload.seriesXtreamId
|
|
);
|
|
}
|
|
|
|
case 'DB_GET_RECENT_PLAYBACK_POSITIONS': {
|
|
const payload = message.payload as {
|
|
playlistId: string;
|
|
limit?: number;
|
|
};
|
|
return getRecentPlaybackPositions(
|
|
db,
|
|
payload.playlistId,
|
|
payload.limit
|
|
);
|
|
}
|
|
|
|
case 'DB_GET_ALL_PLAYBACK_POSITIONS': {
|
|
const payload = message.payload as { playlistId: string };
|
|
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;
|
|
contentXtreamId: number;
|
|
contentType: 'vod' | 'episode';
|
|
};
|
|
return clearPlaybackPosition(
|
|
db,
|
|
payload.playlistId,
|
|
payload.contentXtreamId,
|
|
payload.contentType
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
parentPort.on('message', async (message: DbWorkerIncomingMessage) => {
|
|
if (message.type === 'cancel') {
|
|
const activeOperation = activeOperations.get(message.operationId);
|
|
if (activeOperation) {
|
|
activeOperation.cancelled = true;
|
|
}
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const result = await executeRequest(message);
|
|
postMessage({
|
|
type: 'response',
|
|
requestId: message.requestId,
|
|
success: true,
|
|
result,
|
|
});
|
|
} catch (error) {
|
|
console.error(
|
|
loggerLabel,
|
|
`Error handling ${message.operation}:`,
|
|
error
|
|
);
|
|
postMessage({
|
|
type: 'response',
|
|
requestId: message.requestId,
|
|
success: false,
|
|
error: serializeError(error),
|
|
});
|
|
}
|
|
});
|
|
|
|
process.on('exit', () => {
|
|
closeWorkerDatabase();
|
|
});
|
|
|
|
postMessage({ type: 'ready' });
|