Files
iptvnator/libs/services/src/lib/database-electron.service.ts
T
4gray 91cd650c1a feat: enhance EPG and portal features with error suppression and status resolution
- Added suppressErrorLog option to EPG API calls in withEpg feature.
- Introduced resolvePortalStatus function to streamline portal status checks in withPortal feature.
- Updated checkPortalStatus to return PortalStatusType instead of void.
- Enhanced XtreamStore to preserve cancelled content initialization block.
- Expanded XtreamState to include contentInitBlockReason for better state management.
- Added XtreamContentGateComponent to manage content initialization states.
- Implemented tests for XtreamContentGateComponent to ensure correct rendering based on contentInitBlockReason.
- Created PlaylistRefreshService to handle playlist refresh operations and events.
- Introduced new IPC commands for playlist refresh functionality.
- Added interfaces for playlist refresh events and payloads to improve type safety.
2026-04-04 13:40:34 +02:00

789 lines
22 KiB
TypeScript

/**
* Database service for Electron renderer process
* Communicates with the main process database via IPC
*/
import { Injectable } from '@angular/core';
import {
PlaylistMeta,
XtreamCategory,
} from 'shared-interfaces';
export interface XCategoryFromDb {
id: number;
name: string;
playlist_id: string;
type: 'movies' | 'live' | 'series';
xtream_id: number;
hidden: boolean;
}
export interface XtreamContent {
id: number;
category_id: number;
title: string;
rating: string;
added: string;
poster_url: string;
xtream_id: number;
type: string;
added_at?: string;
viewed_at?: string;
}
export interface XtreamPlaylist {
id: string;
name: string;
serverUrl: string;
username: string;
password: string;
type: string;
}
type XtreamDatabasePlaylistUpdate = {
name?: string;
username?: string;
password?: string;
serverUrl?: string;
lastUpdated?: string;
};
type XtreamContentStream =
| {
category_id: string | number;
rating?: string | number;
rating_imdb?: string;
last_modified?: string;
added?: string;
stream_icon?: string;
poster?: string;
cover?: string;
name?: string;
title?: string;
series_id?: string | number;
stream_id?: string | number;
}
| Record<string, unknown>;
export interface GlobalSearchResult extends XtreamContent {
playlist_id: string;
playlist_name: string;
}
export interface GlobalRecentItem extends XtreamContent {
playlist_id: string;
playlist_name: string;
viewed_at: string;
}
export interface GlobalFavoriteItem extends XtreamContent {
playlist_id: string;
playlist_name: string;
added_at: string;
}
export interface DbOperationEvent {
operationId?: string;
operation: string;
playlistId?: string;
status: 'started' | 'progress' | 'completed' | 'cancelled' | 'error';
phase?: string;
current?: number;
total?: number;
increment?: number;
error?: string;
}
export interface DbOperationOptions {
operationId?: string;
onEvent?: (event: DbOperationEvent) => void;
}
export type XtreamImportStatus =
| 'idle'
| 'importing'
| 'completed'
| 'cancelled'
| 'failed';
export function isDbAbortError(error: unknown): boolean {
return error instanceof Error && error.name === 'AbortError';
}
export type GlobalRecentlyAddedKind = 'all' | 'vod' | 'series';
export interface GlobalRecentlyAddedItem extends XtreamContent {
playlist_id: string;
playlist_name: string;
added_at: string;
}
@Injectable({
providedIn: 'root',
})
export class DatabaseService {
private buildXtreamImportStateKey(
playlistId: string,
type: 'live' | 'movie' | 'series'
): string {
return `xtream-import-status:${playlistId}:${type}`;
}
createOperationId(prefix = 'db-op'): string {
return (
globalThis.crypto?.randomUUID?.() ??
`${prefix}-${Date.now()}-${Math.random().toString(16).slice(2)}`
);
}
supportsDbOperationEvents(): boolean {
return typeof window.electron.onDbOperationEvent === 'function';
}
supportsDbOperationCancellation(): boolean {
return typeof window.electron.dbCancelOperation === 'function';
}
async cancelOperation(operationId: string): Promise<boolean> {
if (!operationId || !this.supportsDbOperationCancellation()) {
return false;
}
try {
const result = await window.electron.dbCancelOperation(operationId);
return result.success;
} catch (error) {
console.error('Error cancelling DB operation:', error);
return false;
}
}
private subscribeToOperation(
operationId: string,
operation: string,
onEvent?: (event: DbOperationEvent) => void,
onProgress?: (count: number) => void
): (() => void) | undefined {
if (!onEvent && !onProgress) {
return undefined;
}
if (window.electron.onDbOperationEvent) {
return window.electron.onDbOperationEvent(
(event: DbOperationEvent) => {
if (
event.operationId !== operationId ||
event.operation !== operation
) {
return;
}
onEvent?.(event);
if (event.status === 'progress') {
onProgress?.(event.increment ?? event.current ?? 0);
}
}
);
}
if (onProgress && operation === 'save-content') {
window.electron.onDbSaveContentProgress(onProgress);
return () => window.electron.removeDbSaveContentProgress();
}
return undefined;
}
private async runWithOperationEvents<TResult>(
prefix: string,
operation: string,
execute: (operationId: string) => Promise<TResult>,
options?: DbOperationOptions,
onProgress?: (count: number) => void
): Promise<TResult> {
const operationId =
options?.operationId ?? this.createOperationId(prefix);
const unsubscribe = this.subscribeToOperation(
operationId,
operation,
options?.onEvent,
onProgress
);
try {
return await execute(operationId);
} finally {
unsubscribe?.();
}
}
/**
* Delete a playlist and all its related data
*/
async deletePlaylist(
playlistId: string,
options?: DbOperationOptions
): Promise<boolean> {
try {
await this.runWithOperationEvents(
'db-delete-playlist',
'delete-playlist',
(operationId) =>
window.electron.dbDeletePlaylist(playlistId, operationId),
options
);
return true;
} catch (error) {
if (!isDbAbortError(error)) {
console.error('Error deleting playlist:', error);
}
return false;
}
}
/**
* Delete all content and categories for an Xtream playlist (for refresh)
* Keeps the playlist entry but removes all imported data
* Returns saved favorites, recently viewed xtreamIds, and hidden categories for restoration
*/
async deleteXtreamPlaylistContent(
playlistId: string,
options?: DbOperationOptions
): Promise<{
success: boolean;
favoritedXtreamIds: number[];
recentlyViewedXtreamIds: { xtreamId: number; viewedAt: string }[];
hiddenCategories: { xtreamId: number; type: string }[];
}> {
return this.runWithOperationEvents(
'db-delete-xtream-content',
'delete-xtream-content',
(operationId) =>
window.electron.dbDeleteXtreamContent(playlistId, operationId),
options
);
}
/**
* Restore favorites and recently viewed items after Xtream refresh
*/
async restoreXtreamUserData(
playlistId: string,
favoritedXtreamIds: number[],
recentlyViewedXtreamIds: { xtreamId: number; viewedAt: string }[],
options?: DbOperationOptions
): Promise<void> {
await this.runWithOperationEvents(
'db-restore-xtream-user-data',
'restore-xtream-user-data',
(operationId) =>
window.electron.dbRestoreXtreamUserData(
playlistId,
favoritedXtreamIds,
recentlyViewedXtreamIds,
operationId
),
options
);
}
async deleteAllPlaylists(options?: DbOperationOptions): Promise<boolean> {
try {
await this.runWithOperationEvents(
'db-delete-all-playlists',
'delete-all-playlists',
(operationId) =>
window.electron.dbDeleteAllPlaylists(operationId),
options
);
return true;
} catch (error) {
if (!isDbAbortError(error)) {
console.error('Error deleting all playlists:', error);
}
return false;
}
}
/**
* Update playlist basic info
*/
async updateXtreamPlaylist(
playlist: Pick<XtreamPlaylist, 'id' | 'name'>
): Promise<boolean> {
try {
await window.electron.dbUpdatePlaylist(playlist.id, {
name: playlist.name,
});
return true;
} catch (error) {
console.error('Error updating playlist:', error);
return false;
}
}
/**
* Update playlist details including credentials
*/
async updateXtreamPlaylistDetails(playlist: {
id: string;
title?: string;
username?: string;
password?: string;
serverUrl?: string;
updateDate?: number;
}): Promise<boolean> {
try {
const updates: XtreamDatabasePlaylistUpdate = {};
if (playlist.title) updates.name = playlist.title;
if (playlist.username) updates.username = playlist.username;
if (playlist.password) updates.password = playlist.password;
if (playlist.serverUrl) updates.serverUrl = playlist.serverUrl;
if (playlist.updateDate !== undefined)
updates.lastUpdated = new Date(
playlist.updateDate
).toISOString();
await window.electron.dbUpdatePlaylist(playlist.id, updates);
return true;
} catch (error) {
console.error('Error updating playlist details:', error);
return false;
}
}
/**
* Check if categories exist
*/
async hasXtreamCategories(
playlistId: string,
type: 'live' | 'movies' | 'series'
): Promise<boolean> {
return await window.electron.dbHasCategories(playlistId, type);
}
/**
* Get categories for a playlist
*/
async getXtreamCategories(
playlistId: string,
type: 'live' | 'movies' | 'series'
): Promise<XCategoryFromDb[]> {
return await window.electron.dbGetCategories(playlistId, type);
}
/**
* Save categories in bulk
* Optionally accepts hidden category xtreamIds to restore visibility preferences
*/
async saveXtreamCategories(
playlistId: string,
categories: XtreamCategory[],
type: 'live' | 'movies' | 'series',
hiddenCategoryXtreamIds?: number[]
): Promise<void> {
await window.electron.dbSaveCategories(
playlistId,
categories,
type,
hiddenCategoryXtreamIds
);
}
/**
* Get all categories for a playlist (including hidden, for management dialog)
*/
async getAllXtreamCategories(
playlistId: string,
type: 'live' | 'movies' | 'series'
): Promise<XCategoryFromDb[]> {
return await window.electron.dbGetAllCategories(playlistId, type);
}
/**
* Update category visibility (show/hide categories)
*/
async updateCategoryVisibility(
categoryIds: number[],
hidden: boolean
): Promise<boolean> {
try {
await window.electron.dbUpdateCategoryVisibility(
categoryIds,
hidden
);
return true;
} catch (error) {
console.error('Error updating category visibility:', error);
return false;
}
}
/**
* Check if content exists
*/
async hasXtreamContent(
playlistId: string,
type: 'live' | 'movie' | 'series'
): Promise<boolean> {
return await window.electron.dbHasContent(playlistId, type);
}
/**
* Get content for a playlist
*/
async getXtreamContent(
playlistId: string,
type: 'live' | 'movie' | 'series'
): Promise<XtreamContent[]> {
return await window.electron.dbGetContent(playlistId, type);
}
/**
* Save content in bulk
*/
async saveXtreamContent(
playlistId: string,
streams: XtreamContentStream[],
type: 'live' | 'movie' | 'series',
onProgress?: (count: number) => void,
options?: DbOperationOptions
): Promise<number> {
try {
const result = await this.runWithOperationEvents(
'db-save-content',
'save-content',
(operationId) =>
window.electron.dbSaveContent(
playlistId,
streams,
type,
operationId
),
options,
onProgress
);
return result.count;
} catch (error) {
if (!isDbAbortError(error)) {
console.error('Error saving Xtream content:', error);
}
throw error;
}
}
async clearXtreamImportCache(
playlistId: string,
type: 'live' | 'movie' | 'series'
): Promise<boolean> {
try {
await window.electron.dbClearXtreamImportCache(playlistId, type);
return true;
} catch (error) {
console.error('Error clearing Xtream import cache:', error);
return false;
}
}
async getAppState(key: string): Promise<string | null> {
try {
return await window.electron.dbGetAppState(key);
} catch (error) {
console.error('Error getting app state:', error);
return null;
}
}
async setAppState(key: string, value: string): Promise<boolean> {
try {
await window.electron.dbSetAppState(key, value);
return true;
} catch (error) {
console.error('Error setting app state:', error);
return false;
}
}
async getXtreamImportStatus(
playlistId: string,
type: 'live' | 'movie' | 'series'
): Promise<XtreamImportStatus> {
const value = await this.getAppState(
this.buildXtreamImportStateKey(playlistId, type)
);
switch (value) {
case 'importing':
case 'completed':
case 'cancelled':
case 'failed':
case 'idle':
return value;
default:
return 'idle';
}
}
async setXtreamImportStatus(
playlistId: string,
type: 'live' | 'movie' | 'series',
status: XtreamImportStatus
): Promise<boolean> {
return this.setAppState(
this.buildXtreamImportStateKey(playlistId, type),
status
);
}
/**
* Search content within a playlist
*/
async searchXtreamContent(
playlistId: string,
searchTerm: string,
types: string[],
excludeHidden?: boolean
): Promise<XtreamContent[]> {
return await window.electron.dbSearchContent(
playlistId,
searchTerm,
types,
excludeHidden
);
}
/**
* Global search across all playlists
*/
async globalSearchContent(
searchTerm: string,
types: string[],
excludeHidden?: boolean
): Promise<GlobalSearchResult[]> {
return await window.electron.dbGlobalSearch(searchTerm, types, excludeHidden);
}
/**
* Get recently added VOD and series items across all Xtream playlists.
*/
async getGlobalRecentlyAdded(
kind: GlobalRecentlyAddedKind,
limit = 200
): Promise<GlobalRecentlyAddedItem[]> {
try {
const items = await window.electron.dbGetGlobalRecentlyAdded(
kind,
limit
);
return items || [];
} catch (error) {
console.error('Error getting global recently added items:', error);
return [];
}
}
/**
* Get recently viewed items
*/
async getGlobalRecentlyViewed(): Promise<GlobalRecentItem[]> {
try {
const items = await window.electron.dbGetRecentlyViewed();
return items || [];
} catch (error) {
console.error('Error getting recently viewed:', error);
return [];
}
}
/**
* Get global favorites across all playlists
*/
async getGlobalFavorites(): Promise<GlobalFavoriteItem[]> {
try {
const items = await window.electron.dbGetGlobalFavorites();
return items || [];
} catch (error) {
console.error('Error getting global favorites:', error);
return [];
}
}
/**
* Get global favorites across all playlists (all content types)
*/
async getAllGlobalFavorites(): Promise<GlobalFavoriteItem[]> {
try {
const items = await window.electron.dbGetAllGlobalFavorites();
return items || [];
} catch (error) {
console.error('Error getting all global favorites:', error);
return [];
}
}
/**
* Clear recently viewed items
*/
async clearGlobalRecentlyViewed(): Promise<void> {
try {
await window.electron.dbClearRecentlyViewed();
} catch (error) {
console.error('Error clearing recently viewed:', error);
throw error;
}
}
/**
* Get playlist by ID
*/
async getPlaylistById(playlistId: string): Promise<XtreamPlaylist | null> {
return await window.electron.dbGetPlaylist(playlistId);
}
/**
* Create a new playlist
*/
async createPlaylist(playlist: PlaylistMeta): Promise<void> {
await window.electron.dbCreatePlaylist({
id: playlist._id,
name: playlist.title,
serverUrl: playlist.serverUrl,
username: playlist.username,
password: playlist.password,
type: 'xtream',
});
}
/**
* Add content to favorites
*/
async addToFavorites(
contentId: number,
playlistId: string
): Promise<boolean> {
try {
await window.electron.dbAddFavorite(contentId, playlistId);
return true;
} catch (error) {
console.error('Error adding to favorites:', error);
return false;
}
}
/**
* Remove content from favorites
*/
async removeFromFavorites(
contentId: number,
playlistId: string
): Promise<boolean> {
try {
await window.electron.dbRemoveFavorite(contentId, playlistId);
return true;
} catch (error) {
console.error('Error removing from favorites:', error);
return false;
}
}
/**
* Check if content is favorited
*/
async isFavorite(contentId: number, playlistId: string): Promise<boolean> {
try {
return await window.electron.dbIsFavorite(contentId, playlistId);
} catch (error) {
console.error('Error checking favorite:', error);
return false;
}
}
/**
* Get all favorites for a playlist
*/
async getFavorites(playlistId: string): Promise<XtreamContent[]> {
try {
return await window.electron.dbGetFavorites(playlistId);
} catch (error) {
console.error('Error getting favorites:', error);
return [];
}
}
/**
* Get recently viewed items for a specific playlist
*/
async getRecentItems(playlistId: string): Promise<XtreamContent[]> {
try {
return await window.electron.dbGetRecentItems(playlistId);
} catch (error) {
console.error('Error getting recent items:', error);
return [];
}
}
/**
* Add item to recently viewed
*/
async addRecentItem(
contentId: number,
playlistId: string
): Promise<boolean> {
try {
await window.electron.dbAddRecentItem(contentId, playlistId);
return true;
} catch (error) {
console.error('Error adding recent item:', error);
return false;
}
}
/**
* Clear recently viewed for a specific playlist
*/
async clearPlaylistRecentItems(playlistId: string): Promise<boolean> {
try {
await window.electron.dbClearPlaylistRecentItems(playlistId);
return true;
} catch (error) {
console.error('Error clearing playlist recent items:', error);
return false;
}
}
/**
* Remove specific item from recently viewed
*/
async removeRecentItem(
contentId: number,
playlistId: string
): Promise<boolean> {
try {
await window.electron.dbRemoveRecentItem(contentId, playlistId);
return true;
} catch (error) {
console.error('Error removing recent item:', error);
return false;
}
}
/**
* Get content by xtream ID
*/
async getContentByXtreamId(
xtreamId: number,
playlistId: string
): Promise<XtreamContent | null> {
try {
return await window.electron.dbGetContentByXtreamId(
xtreamId,
playlistId
);
} catch (error) {
console.error('Error getting content by xtream ID:', error);
return null;
}
}
}