mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-12 20:16:15 -08:00
512 lines
17 KiB
TypeScript
512 lines
17 KiB
TypeScript
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();
|
|
}
|
|
}
|