refactor: add Xtream data source abstraction

This commit is contained in:
4gray committed 2026-01-11 06:26:38 +01:00
1 parent aa27e6da0d
commit 3a2ee74b96
4 files changed
+1151

No files matched your search

@@ -0,0 +1,281 @@
import { inject, Injectable } from '@angular/core';
import { DatabaseService } from 'services';
import {
XtreamCategory,
XtreamLiveStream,
XtreamSerieItem,
XtreamVodStream,
} from 'shared-interfaces';
import { CategoryType, StreamType, XtreamApiService, XtreamCredentials } from '../services/xtream-api.service';
import {
DbCategoryType,
IXtreamDataSource,
mapCategoryTypeToDbType,
ProgressCallback,
XtreamCategoryFromDb,
XtreamContentItem,
XtreamPlaylistData,
} from './xtream-data-source.interface';
/**
* Electron implementation of the Xtream data source.
* Uses DB-first strategy: check DB, fetch API if needed, cache to DB.
*/
@Injectable({ providedIn: 'root' })
export class ElectronXtreamDataSource implements IXtreamDataSource {
private readonly dbService = inject(DatabaseService);
private readonly apiService = inject(XtreamApiService);
// =========================================================================
// Playlist Operations
// =========================================================================
async getPlaylist(playlistId: string): Promise<XtreamPlaylistData | null> {
const playlist = await this.dbService.getPlaylistById(playlistId);
return playlist as XtreamPlaylistData | null;
}
async createPlaylist(playlist: XtreamPlaylistData): Promise<void> {
await this.dbService.createPlaylist({
_id: playlist.id,
title: playlist.name,
serverUrl: playlist.serverUrl,
username: playlist.username,
password: playlist.password,
} as any);
}
async updatePlaylist(
playlistId: string,
updates: Partial<XtreamPlaylistData>
): Promise<void> {
await this.dbService.updateXtreamPlaylistDetails({
id: playlistId,
title: updates.name,
username: updates.username,
password: updates.password,
serverUrl: updates.serverUrl,
});
}
async deletePlaylist(playlistId: string): Promise<void> {
await this.dbService.deletePlaylist(playlistId);
}
// =========================================================================
// Category Operations
// =========================================================================
async hasCategories(
playlistId: string,
type: DbCategoryType
): Promise<boolean> {
return this.dbService.hasXtreamCategories(playlistId, type);
}
async getCategories(
playlistId: string,
credentials: XtreamCredentials,
type: CategoryType
): Promise<XtreamCategoryFromDb[]> {
const dbType = mapCategoryTypeToDbType(type);
// Check if we have cached data
const exists = await this.dbService.hasXtreamCategories(
playlistId,
dbType
);
if (exists) {
// Return from cache
return this.dbService.getXtreamCategories(playlistId, dbType);
}
// Fetch from API and cache
const remoteData = await this.apiService.getCategories(
credentials,
type
);
if (remoteData && Array.isArray(remoteData) && remoteData.length > 0) {
await this.dbService.saveXtreamCategories(
playlistId,
remoteData,
dbType
);
}
// Return from cache (now populated)
return this.dbService.getXtreamCategories(playlistId, dbType);
}
async getAllCategories(
playlistId: string,
type: DbCategoryType
): Promise<XtreamCategoryFromDb[]> {
return this.dbService.getAllXtreamCategories(playlistId, type);
}
async saveCategories(
playlistId: string,
categories: XtreamCategory[],
type: DbCategoryType
): Promise<void> {
await this.dbService.saveXtreamCategories(playlistId, categories, type);
}
async updateCategoryVisibility(
categoryIds: number[],
hidden: boolean
): Promise<void> {
await this.dbService.updateCategoryVisibility(categoryIds, hidden);
}
// =========================================================================
// Content/Stream Operations
// =========================================================================
async hasContent(
playlistId: string,
type: 'live' | 'movie' | 'series'
): Promise<boolean> {
return this.dbService.hasXtreamContent(playlistId, type);
}
async getContent(
playlistId: string,
credentials: XtreamCredentials,
type: StreamType
): Promise<XtreamContentItem[]> {
// Check if we have cached data
const exists = await this.dbService.hasXtreamContent(playlistId, type);
if (exists) {
// Return from cache
return this.dbService.getXtreamContent(playlistId, type);
}
// Fetch from API
const remoteData = await this.apiService.getStreams(credentials, type);
if (remoteData && Array.isArray(remoteData) && remoteData.length > 0) {
// Save to cache
await this.dbService.saveXtreamContent(
playlistId,
remoteData as any[],
type
);
}
// Return from cache (now populated)
return this.dbService.getXtreamContent(playlistId, type);
}
async saveContent(
playlistId: string,
streams: any[],
type: 'live' | 'movie' | 'series',
onProgress?: ProgressCallback
): Promise<number> {
return this.dbService.saveXtreamContent(
playlistId,
streams,
type,
onProgress
);
}
// =========================================================================
// Search Operations
// =========================================================================
async searchContent(
playlistId: string,
searchTerm: string,
types: string[]
): Promise<XtreamContentItem[]> {
return this.dbService.searchXtreamContent(playlistId, searchTerm, types);
}
// =========================================================================
// Favorites Operations
// =========================================================================
async getFavorites(playlistId: string): Promise<XtreamContentItem[]> {
return this.dbService.getFavorites(playlistId);
}
async addFavorite(contentId: number, playlistId: string): Promise<void> {
await this.dbService.addToFavorites(contentId, playlistId);
}
async removeFavorite(contentId: number, playlistId: string): Promise<void> {
await this.dbService.removeFromFavorites(contentId, playlistId);
}
async isFavorite(contentId: number, playlistId: string): Promise<boolean> {
return this.dbService.isFavorite(contentId, playlistId);
}
// =========================================================================
// Recently Viewed Operations
// =========================================================================
async getRecentItems(playlistId: string): Promise<XtreamContentItem[]> {
return this.dbService.getRecentItems(playlistId);
}
async addRecentItem(contentId: number, playlistId: string): Promise<void> {
await this.dbService.addRecentItem(contentId, playlistId);
}
async removeRecentItem(
contentId: number,
playlistId: string
): Promise<void> {
await this.dbService.removeRecentItem(contentId, playlistId);
}
async clearRecentItems(playlistId: string): Promise<void> {
await this.dbService.clearPlaylistRecentItems(playlistId);
}
// =========================================================================
// Content Lookup
// =========================================================================
async getContentByXtreamId(
xtreamId: number,
playlistId: string
): Promise<XtreamContentItem | null> {
return this.dbService.getContentByXtreamId(xtreamId, playlistId);
}
// =========================================================================
// Cleanup Operations
// =========================================================================
async clearPlaylistContent(playlistId: string): Promise<{
favoritedXtreamIds: number[];
recentlyViewedXtreamIds: { xtreamId: number; viewedAt: string }[];
}> {
const result =
await this.dbService.deleteXtreamPlaylistContent(playlistId);
return {
favoritedXtreamIds: result.favoritedXtreamIds,
recentlyViewedXtreamIds: result.recentlyViewedXtreamIds,
};
}
async restoreUserData(
playlistId: string,
favoritedXtreamIds: number[],
recentlyViewedXtreamIds: { xtreamId: number; viewedAt: string }[]
): Promise<void> {
await this.dbService.restoreXtreamUserData(
playlistId,
favoritedXtreamIds,
recentlyViewedXtreamIds
);
}
}
@@ -0,0 +1,39 @@
import { EnvironmentProviders, inject, Provider } from '@angular/core';
import { ElectronXtreamDataSource } from './electron-xtream-data-source';
import { PwaXtreamDataSource } from './pwa-xtream-data-source';
import { IXtreamDataSource, XTREAM_DATA_SOURCE } from './xtream-data-source.interface';
// Re-export all types and interfaces
export * from './xtream-data-source.interface';
export { ElectronXtreamDataSource } from './electron-xtream-data-source';
export { PwaXtreamDataSource } from './pwa-xtream-data-source';
/**
* Factory function that returns the appropriate data source based on environment.
* - Electron: Uses DatabaseService for DB-first caching
* - PWA: Uses API-only with in-memory caching and localStorage for user data
*/
export function xtreamDataSourceFactory(): IXtreamDataSource {
// Check if we're in Electron environment
if (typeof window !== 'undefined' && (window as any).electron) {
return inject(ElectronXtreamDataSource);
}
// Default to PWA implementation
return inject(PwaXtreamDataSource);
}
/**
* Provider for the Xtream data source.
* Add this to your app providers to enable the data source abstraction.
*/
export function provideXtreamDataSource(): Provider[] {
return [
ElectronXtreamDataSource,
PwaXtreamDataSource,
{
provide: XTREAM_DATA_SOURCE,
useFactory: xtreamDataSourceFactory,
},
];
}
@@ -0,0 +1,511 @@
import { inject, Injectable } from '@angular/core';
import {
XtreamCategory,
XtreamLiveStream,
XtreamSerieItem,
XtreamVodStream,
} from 'shared-interfaces';
import { CategoryType, StreamType, XtreamApiService, XtreamCredentials } from '../services/xtream-api.service';
import {
DbCategoryType,
IXtreamDataSource,
ProgressCallback,
XtreamCategoryFromDb,
XtreamContentItem,
XtreamPlaylistData,
} from './xtream-data-source.interface';
/**
* LocalStorage keys for PWA persistence
*/
const STORAGE_KEYS = {
FAVORITES: 'xtream-favorites',
RECENT_ITEMS: 'xtream-recent-items',
PLAYLISTS: 'xtream-playlists',
};
/**
* PWA implementation of the Xtream data source.
* Uses API-only strategy: always fetch from API, no database caching.
* Favorites and recently viewed are stored in localStorage.
*/
@Injectable({ providedIn: 'root' })
export class PwaXtreamDataSource implements IXtreamDataSource {
private readonly apiService = inject(XtreamApiService);
// In-memory cache for the current session
private categoryCache = new Map<string, XtreamCategory[]>();
private contentCache = new Map<string, any[]>();
// =========================================================================
// Playlist Operations (localStorage)
// =========================================================================
async getPlaylist(playlistId: string): Promise<XtreamPlaylistData | null> {
const playlists = this.getPlaylistsFromStorage();
return playlists.find((p) => p.id === playlistId) || null;
}
async createPlaylist(playlist: XtreamPlaylistData): Promise<void> {
const playlists = this.getPlaylistsFromStorage();
playlists.push(playlist);
this.savePlaylistsToStorage(playlists);
}
async updatePlaylist(
playlistId: string,
updates: Partial<XtreamPlaylistData>
): Promise<void> {
const playlists = this.getPlaylistsFromStorage();
const index = playlists.findIndex((p) => p.id === playlistId);
if (index !== -1) {
playlists[index] = { ...playlists[index], ...updates };
this.savePlaylistsToStorage(playlists);
}
}
async deletePlaylist(playlistId: string): Promise<void> {
const playlists = this.getPlaylistsFromStorage();
const filtered = playlists.filter((p) => p.id !== playlistId);
this.savePlaylistsToStorage(filtered);
// Also clear favorites and recent items for this playlist
this.clearFavoritesForPlaylist(playlistId);
this.clearRecentItemsForPlaylist(playlistId);
// Clear cache
this.clearCacheForPlaylist(playlistId);
}
private getPlaylistsFromStorage(): XtreamPlaylistData[] {
try {
const data = localStorage.getItem(STORAGE_KEYS.PLAYLISTS);
return data ? JSON.parse(data) : [];
} catch {
return [];
}
}
private savePlaylistsToStorage(playlists: XtreamPlaylistData[]): void {
localStorage.setItem(STORAGE_KEYS.PLAYLISTS, JSON.stringify(playlists));
}
// =========================================================================
// Category Operations (API + in-memory cache)
// =========================================================================
async hasCategories(
playlistId: string,
type: DbCategoryType
): Promise<boolean> {
const cacheKey = `${playlistId}-${type}-categories`;
return this.categoryCache.has(cacheKey);
}
async getCategories(
playlistId: string,
credentials: XtreamCredentials,
type: CategoryType
): Promise<XtreamCategory[]> {
const cacheKey = `${playlistId}-${type}-categories`;
// Check in-memory cache first
if (this.categoryCache.has(cacheKey)) {
return this.categoryCache.get(cacheKey)!;
}
// Fetch from API
const categories = await this.apiService.getCategories(
credentials,
type
);
// Cache in memory
this.categoryCache.set(cacheKey, categories);
return categories;
}
async getAllCategories(
playlistId: string,
type: DbCategoryType
): Promise<XtreamCategoryFromDb[]> {
// PWA doesn't track hidden categories - return empty
return [];
}
async saveCategories(
playlistId: string,
categories: XtreamCategory[],
type: DbCategoryType
): Promise<void> {
// In PWA mode, we just cache in memory
const cacheKey = `${playlistId}-${type}-categories`;
this.categoryCache.set(cacheKey, categories);
}
async updateCategoryVisibility(
categoryIds: number[],
hidden: boolean
): Promise<void> {
// Category visibility is not supported in PWA mode
console.warn('Category visibility not supported in PWA mode');
}
// =========================================================================
// Content/Stream Operations (API + in-memory cache)
// =========================================================================
async hasContent(
playlistId: string,
type: 'live' | 'movie' | 'series'
): Promise<boolean> {
const cacheKey = `${playlistId}-${type}-content`;
return this.contentCache.has(cacheKey);
}
async getContent(
playlistId: string,
credentials: XtreamCredentials,
type: StreamType
): Promise<XtreamLiveStream[] | XtreamVodStream[] | XtreamSerieItem[]> {
const cacheKey = `${playlistId}-${type}-content`;
// Check in-memory cache first
if (this.contentCache.has(cacheKey)) {
return this.contentCache.get(cacheKey)!;
}
// Fetch from API
const content = await this.apiService.getStreams(credentials, type);
// Cache in memory
this.contentCache.set(cacheKey, content);
return content;
}
async saveContent(
playlistId: string,
streams: any[],
type: 'live' | 'movie' | 'series',
onProgress?: ProgressCallback
): Promise<number> {
// In PWA mode, we just cache in memory
const cacheKey = `${playlistId}-${type}-content`;
this.contentCache.set(cacheKey, streams);
if (onProgress) {
onProgress(streams.length);
}
return streams.length;
}
// =========================================================================
// Search Operations (in-memory filter)
// =========================================================================
async searchContent(
playlistId: string,
searchTerm: string,
types: string[]
): Promise<XtreamContentItem[]> {
const results: any[] = [];
const searchLower = searchTerm.toLowerCase();
for (const type of types) {
const cacheKey = `${playlistId}-${type}-content`;
const content = this.contentCache.get(cacheKey) || [];
const filtered = content.filter((item: any) => {
const title =
item.name || item.title || item.stream_display_name || '';
return title.toLowerCase().includes(searchLower);
});
results.push(...filtered);
}
return results as XtreamContentItem[];
}
// =========================================================================
// Favorites Operations (localStorage)
// =========================================================================
async getFavorites(playlistId: string): Promise<XtreamContentItem[]> {
const allFavorites = this.getFavoritesFromStorage();
const playlistFavorites = allFavorites[playlistId] || [];
// Match favorites with cached content
const results: any[] = [];
for (const type of ['live', 'movie', 'series']) {
const cacheKey = `${playlistId}-${type}-content`;
const content = this.contentCache.get(cacheKey) || [];
for (const item of content as any[]) {
const itemId = item.stream_id || item.series_id || item.id;
if (playlistFavorites.includes(itemId)) {
results.push(item);
}
}
}
return results as XtreamContentItem[];
}
async addFavorite(contentId: number, playlistId: string): Promise<void> {
const allFavorites = this.getFavoritesFromStorage();
if (!allFavorites[playlistId]) {
allFavorites[playlistId] = [];
}
if (!allFavorites[playlistId].includes(contentId)) {
allFavorites[playlistId].push(contentId);
}
this.saveFavoritesToStorage(allFavorites);
}
async removeFavorite(contentId: number, playlistId: string): Promise<void> {
const allFavorites = this.getFavoritesFromStorage();
if (allFavorites[playlistId]) {
allFavorites[playlistId] = allFavorites[playlistId].filter(
(id: number) => id !== contentId
);
}
this.saveFavoritesToStorage(allFavorites);
}
async isFavorite(contentId: number, playlistId: string): Promise<boolean> {
const allFavorites = this.getFavoritesFromStorage();
return (allFavorites[playlistId] || []).includes(contentId);
}
private getFavoritesFromStorage(): Record<string, number[]> {
try {
const data = localStorage.getItem(STORAGE_KEYS.FAVORITES);
return data ? JSON.parse(data) : {};
} catch {
return {};
}
}
private saveFavoritesToStorage(
favorites: Record<string, number[]>
): void {
localStorage.setItem(STORAGE_KEYS.FAVORITES, JSON.stringify(favorites));
}
private clearFavoritesForPlaylist(playlistId: string): void {
const allFavorites = this.getFavoritesFromStorage();
delete allFavorites[playlistId];
this.saveFavoritesToStorage(allFavorites);
}
// =========================================================================
// Recently Viewed Operations (localStorage)
// =========================================================================
async getRecentItems(playlistId: string): Promise<XtreamContentItem[]> {
const allRecent = this.getRecentItemsFromStorage();
const playlistRecent = allRecent[playlistId] || [];
// Match recent items with cached content
const results: any[] = [];
for (const type of ['live', 'movie', 'series']) {
const cacheKey = `${playlistId}-${type}-content`;
const content = this.contentCache.get(cacheKey) || [];
for (const item of content as any[]) {
const itemId = item.stream_id || item.series_id || item.id;
const recentEntry = playlistRecent.find(
(r: any) => r.id === itemId
);
if (recentEntry) {
results.push({
...item,
viewed_at: recentEntry.viewedAt,
});
}
}
}
// Sort by viewed_at descending
results.sort(
(a, b) =>
new Date(b.viewed_at).getTime() -
new Date(a.viewed_at).getTime()
);
return results as XtreamContentItem[];
}
async addRecentItem(contentId: number, playlistId: string): Promise<void> {
const allRecent = this.getRecentItemsFromStorage();
if (!allRecent[playlistId]) {
allRecent[playlistId] = [];
}
// Remove existing entry if present
allRecent[playlistId] = allRecent[playlistId].filter(
(r: any) => r.id !== contentId
);
// Add new entry at the beginning
allRecent[playlistId].unshift({
id: contentId,
viewedAt: new Date().toISOString(),
});
// Keep only last 50 items
allRecent[playlistId] = allRecent[playlistId].slice(0, 50);
this.saveRecentItemsToStorage(allRecent);
}
async removeRecentItem(
contentId: number,
playlistId: string
): Promise<void> {
const allRecent = this.getRecentItemsFromStorage();
if (allRecent[playlistId]) {
allRecent[playlistId] = allRecent[playlistId].filter(
(r: any) => r.id !== contentId
);
}
this.saveRecentItemsToStorage(allRecent);
}
async clearRecentItems(playlistId: string): Promise<void> {
this.clearRecentItemsForPlaylist(playlistId);
}
private getRecentItemsFromStorage(): Record<
string,
{ id: number; viewedAt: string }[]
> {
try {
const data = localStorage.getItem(STORAGE_KEYS.RECENT_ITEMS);
return data ? JSON.parse(data) : {};
} catch {
return {};
}
}
private saveRecentItemsToStorage(
recentItems: Record<string, { id: number; viewedAt: string }[]>
): void {
localStorage.setItem(
STORAGE_KEYS.RECENT_ITEMS,
JSON.stringify(recentItems)
);
}
private clearRecentItemsForPlaylist(playlistId: string): void {
const allRecent = this.getRecentItemsFromStorage();
delete allRecent[playlistId];
this.saveRecentItemsToStorage(allRecent);
}
// =========================================================================
// Content Lookup
// =========================================================================
async getContentByXtreamId(
xtreamId: number,
playlistId: string
): Promise<XtreamContentItem | null> {
for (const type of ['live', 'movie', 'series']) {
const cacheKey = `${playlistId}-${type}-content`;
const content = this.contentCache.get(cacheKey) || [];
const found = (content as any[]).find((item) => {
const itemXtreamId =
item.stream_id || item.series_id || item.id;
return itemXtreamId === xtreamId;
});
if (found) {
return found as XtreamContentItem;
}
}
return null;
}
// =========================================================================
// Cleanup Operations
// =========================================================================
async clearPlaylistContent(playlistId: string): Promise<{
favoritedXtreamIds: number[];
recentlyViewedXtreamIds: { xtreamId: number; viewedAt: string }[];
}> {
// Get current favorites and recent items
const favorites = this.getFavoritesFromStorage();
const recentItems = this.getRecentItemsFromStorage();
const favoritedXtreamIds = favorites[playlistId] || [];
const recentlyViewedXtreamIds = (recentItems[playlistId] || []).map(
(r: any) => ({
xtreamId: r.id,
viewedAt: r.viewedAt,
})
);
// Clear in-memory cache
this.clearCacheForPlaylist(playlistId);
return { favoritedXtreamIds, recentlyViewedXtreamIds };
}
async restoreUserData(
playlistId: string,
favoritedXtreamIds: number[],
recentlyViewedXtreamIds: { xtreamId: number; viewedAt: string }[]
): Promise<void> {
// Restore favorites
const favorites = this.getFavoritesFromStorage();
favorites[playlistId] = favoritedXtreamIds;
this.saveFavoritesToStorage(favorites);
// Restore recent items
const recentItems = this.getRecentItemsFromStorage();
recentItems[playlistId] = recentlyViewedXtreamIds.map((r) => ({
id: r.xtreamId,
viewedAt: r.viewedAt,
}));
this.saveRecentItemsToStorage(recentItems);
}
// =========================================================================
// Cache Management
// =========================================================================
private clearCacheForPlaylist(playlistId: string): void {
const keysToDelete: string[] = [];
this.categoryCache.forEach((_, key) => {
if (key.startsWith(playlistId)) {
keysToDelete.push(key);
}
});
keysToDelete.forEach((key) => this.categoryCache.delete(key));
const contentKeysToDelete: string[] = [];
this.contentCache.forEach((_, key) => {
if (key.startsWith(playlistId)) {
contentKeysToDelete.push(key);
}
});
contentKeysToDelete.forEach((key) => this.contentCache.delete(key));
}
/**
* Clear all in-memory caches
*/
clearAllCaches(): void {
this.categoryCache.clear();
this.contentCache.clear();
}
}
@@ -0,0 +1,320 @@
import { InjectionToken } from '@angular/core';
import {
XtreamCategory,
XtreamLiveStream,
XtreamSerieItem,
XtreamVodStream,
} from 'shared-interfaces';
import { XtreamCredentials, CategoryType, StreamType } from '../services/xtream-api.service';
/**
* Playlist representation in the data source
*/
export interface XtreamPlaylistData {
id: string;
name: string;
title?: string; // Alias for name, used in some templates
serverUrl: string;
username: string;
password: string;
type: string;
userAgent?: string;
referrer?: string;
origin?: string;
}
/**
* Content item from the data source.
* Includes fields from XtreamItem for template compatibility.
*/
export interface XtreamContentItem {
// Core DB fields
id: number;
category_id: number | string; // Can be number (DB) or string (API)
title: string;
rating: string;
added: string;
poster_url: string;
xtream_id: number;
type: string;
added_at?: string;
viewed_at?: string;
// XtreamItem compatibility fields (optional for search/navigation)
num?: number;
name?: string;
stream_type?: 'live' | 'movie';
stream_id?: number;
stream_icon?: string;
custom_sid?: string;
direct_source?: string;
rating_imdb?: string;
// Global search result fields
description?: string;
playlist_id?: string;
playlist_name?: string;
}
/**
* Category from the database with additional fields
*/
export interface XtreamCategoryFromDb {
id: number;
name: string;
playlist_id: string;
type: 'movies' | 'live' | 'series';
xtream_id: number;
hidden: boolean;
}
/**
* Recently viewed item with playlist info
*/
export interface RecentlyViewedItem extends XtreamContentItem {
viewed_at: string;
}
/**
* Database category type mapping (differs from API type)
*/
export type DbCategoryType = 'live' | 'movies' | 'series';
/**
* Maps CategoryType to DbCategoryType
*/
export function mapCategoryTypeToDbType(type: CategoryType): DbCategoryType {
switch (type) {
case 'live':
return 'live';
case 'vod':
return 'movies';
case 'series':
return 'series';
}
}
/**
* Maps StreamType to DbCategoryType for content storage
*/
export function mapStreamTypeToDbType(type: StreamType): 'live' | 'movie' | 'series' {
return type;
}
/**
* Progress callback for bulk operations
*/
export type ProgressCallback = (count: number) => void;
/**
* Abstract interface for Xtream data source.
* Allows different implementations for Electron (DB-first) and PWA (API-only).
*/
export interface IXtreamDataSource {
// =========================================================================
// Playlist Operations
// =========================================================================
/**
* Get playlist by ID
*/
getPlaylist(playlistId: string): Promise<XtreamPlaylistData | null>;
/**
* Create a new playlist
*/
createPlaylist(playlist: XtreamPlaylistData): Promise<void>;
/**
* Update playlist details
*/
updatePlaylist(
playlistId: string,
updates: Partial<XtreamPlaylistData>
): Promise<void>;
/**
* Delete a playlist and all its data
*/
deletePlaylist(playlistId: string): Promise<void>;
// =========================================================================
// Category Operations
// =========================================================================
/**
* Check if categories exist for a playlist and type
*/
hasCategories(
playlistId: string,
type: DbCategoryType
): Promise<boolean>;
/**
* Get categories for a playlist and type
* Returns only visible categories by default
*/
getCategories(
playlistId: string,
credentials: XtreamCredentials,
type: CategoryType
): Promise<XtreamCategory[] | XtreamCategoryFromDb[]>;
/**
* Get all categories including hidden (for management)
*/
getAllCategories(
playlistId: string,
type: DbCategoryType
): Promise<XtreamCategoryFromDb[]>;
/**
* Save categories in bulk
*/
saveCategories(
playlistId: string,
categories: XtreamCategory[],
type: DbCategoryType
): Promise<void>;
/**
* Update category visibility
*/
updateCategoryVisibility(
categoryIds: number[],
hidden: boolean
): Promise<void>;
// =========================================================================
// Content/Stream Operations
// =========================================================================
/**
* Check if content exists for a playlist and type
*/
hasContent(
playlistId: string,
type: 'live' | 'movie' | 'series'
): Promise<boolean>;
/**
* Get content/streams for a playlist and type
*/
getContent(
playlistId: string,
credentials: XtreamCredentials,
type: StreamType
): Promise<XtreamLiveStream[] | XtreamVodStream[] | XtreamSerieItem[] | XtreamContentItem[]>;
/**
* Save content in bulk
*/
saveContent(
playlistId: string,
streams: any[],
type: 'live' | 'movie' | 'series',
onProgress?: ProgressCallback
): Promise<number>;
// =========================================================================
// Search Operations
// =========================================================================
/**
* Search content within a playlist
*/
searchContent(
playlistId: string,
searchTerm: string,
types: string[]
): Promise<XtreamContentItem[]>;
// =========================================================================
// Favorites Operations
// =========================================================================
/**
* Get all favorites for a playlist
*/
getFavorites(playlistId: string): Promise<XtreamContentItem[]>;
/**
* Add content to favorites
*/
addFavorite(contentId: number, playlistId: string): Promise<void>;
/**
* Remove content from favorites
*/
removeFavorite(contentId: number, playlistId: string): Promise<void>;
/**
* Check if content is favorited
*/
isFavorite(contentId: number, playlistId: string): Promise<boolean>;
// =========================================================================
// Recently Viewed Operations
// =========================================================================
/**
* Get recently viewed items for a playlist
*/
getRecentItems(playlistId: string): Promise<XtreamContentItem[]>;
/**
* Add item to recently viewed
*/
addRecentItem(contentId: number, playlistId: string): Promise<void>;
/**
* Remove item from recently viewed
*/
removeRecentItem(contentId: number, playlistId: string): Promise<void>;
/**
* Clear recently viewed for a playlist
*/
clearRecentItems(playlistId: string): Promise<void>;
// =========================================================================
// Content Lookup
// =========================================================================
/**
* Get content by xtream ID
*/
getContentByXtreamId(
xtreamId: number,
playlistId: string
): Promise<XtreamContentItem | null>;
// =========================================================================
// Cleanup Operations
// =========================================================================
/**
* Clear all content and categories for a playlist (for refresh)
* Returns user data (favorites, recently viewed) for restoration
*/
clearPlaylistContent(playlistId: string): Promise<{
favoritedXtreamIds: number[];
recentlyViewedXtreamIds: { xtreamId: number; viewedAt: string }[];
}>;
/**
* Restore user data after refresh
*/
restoreUserData(
playlistId: string,
favoritedXtreamIds: number[],
recentlyViewedXtreamIds: { xtreamId: number; viewedAt: string }[]
): Promise<void>;
}
/**
* Injection token for the data source
*/
export const XTREAM_DATA_SOURCE = new InjectionToken<IXtreamDataSource>(
'XtreamDataSource'
);