mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
- 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.
789 lines
22 KiB
TypeScript
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;
|
|
}
|
|
}
|
|
}
|