mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-10 18:36:15 -08:00
refactor(xtream): refactor xtream store
This commit is contained in:
1 parent
aca262185e
commit
37783b3848
4 files changed
+500
-398
No files matched your search
@@ -1,5 +1,34 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import Database from '@tauri-apps/plugin-sql';
|
||||
import { PlaylistMeta } from '../shared/playlist-meta.type';
|
||||
|
||||
export interface XCategoryFromDb {
|
||||
id: number;
|
||||
name: string;
|
||||
playlist_id: string;
|
||||
type: 'movies' | 'live' | 'series';
|
||||
xtream_id: number;
|
||||
}
|
||||
|
||||
export interface XtreamContent {
|
||||
id: number;
|
||||
category_id: number;
|
||||
title: string;
|
||||
rating: string;
|
||||
added: string;
|
||||
poster_url: string;
|
||||
xtream_id: number;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface XtreamPlaylist {
|
||||
id: string;
|
||||
name: string;
|
||||
serverUrl: string;
|
||||
username: string;
|
||||
password: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
@@ -128,4 +157,215 @@ export class DatabaseService {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async hasXtreamCategories(
|
||||
playlistId: string,
|
||||
type: 'live' | 'movies' | 'series'
|
||||
): Promise<boolean> {
|
||||
const db = await this.getConnection();
|
||||
const result = await db.select<XCategoryFromDb[]>(
|
||||
'SELECT * FROM categories WHERE playlist_id = ? AND type = ?',
|
||||
[playlistId, type]
|
||||
);
|
||||
return result.length > 0;
|
||||
}
|
||||
|
||||
async getXtreamCategories(
|
||||
playlistId: string,
|
||||
type: 'live' | 'movies' | 'series'
|
||||
): Promise<XCategoryFromDb[]> {
|
||||
const db = await this.getConnection();
|
||||
return await db.select<XCategoryFromDb[]>(
|
||||
'SELECT * FROM categories WHERE playlist_id = ? AND type = ?',
|
||||
[playlistId, type]
|
||||
);
|
||||
}
|
||||
|
||||
async saveXtreamCategories(
|
||||
playlistId: string,
|
||||
categories: any[],
|
||||
type: 'live' | 'movies' | 'series'
|
||||
): Promise<void> {
|
||||
const db = await this.getConnection();
|
||||
for (const category of categories) {
|
||||
await db.execute(
|
||||
'INSERT INTO categories (playlist_id, name, type, xtream_id) VALUES (?, ?, ?, ?)',
|
||||
[playlistId, category.category_name, type, category.category_id]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async hasXtreamContent(
|
||||
playlistId: string,
|
||||
type: 'live' | 'movie' | 'series'
|
||||
): Promise<boolean> {
|
||||
const db = await this.getConnection();
|
||||
const result = await db.select(
|
||||
`SELECT c.* FROM content c
|
||||
JOIN categories cat ON c.category_id = cat.id
|
||||
WHERE cat.playlist_id = ? AND c.type = ?
|
||||
ORDER BY c.added`,
|
||||
[playlistId, type]
|
||||
);
|
||||
return (result as any[]).length > 0;
|
||||
}
|
||||
|
||||
async getXtreamContent(
|
||||
playlistId: string,
|
||||
type: 'live' | 'movie' | 'series'
|
||||
): Promise<XtreamContent[]> {
|
||||
const db = await this.getConnection();
|
||||
return await db.select(
|
||||
`SELECT
|
||||
c.id, c.category_id, c.title, c.rating,
|
||||
c.added, c.poster_url, c.xtream_id, c.type
|
||||
FROM content c
|
||||
INNER JOIN categories cat ON c.category_id = cat.id
|
||||
WHERE cat.playlist_id = ? AND c.type = ?
|
||||
ORDER BY c.added DESC`,
|
||||
[playlistId, type]
|
||||
);
|
||||
}
|
||||
|
||||
async saveXtreamContent(
|
||||
playlistId: string,
|
||||
streams: any[],
|
||||
type: 'live' | 'movie' | 'series',
|
||||
onProgress?: (count: number) => void
|
||||
): Promise<number> {
|
||||
const db = await this.getConnection();
|
||||
const dbType =
|
||||
type === 'series' ? 'series' : type === 'movie' ? 'movies' : 'live';
|
||||
|
||||
const categories = await db.select<{ id: number; xtream_id: number }[]>(
|
||||
'SELECT id, xtream_id FROM categories WHERE playlist_id = ? AND type = ?',
|
||||
[playlistId, dbType]
|
||||
);
|
||||
|
||||
const categoryMap = new Map(
|
||||
categories.map((c) => [parseInt(c.xtream_id.toString()), c.id])
|
||||
);
|
||||
|
||||
const bulkInsertData = this.prepareBulkInsertData(
|
||||
streams,
|
||||
type,
|
||||
categoryMap
|
||||
);
|
||||
return await this.executeBulkInsert(db, bulkInsertData, onProgress);
|
||||
}
|
||||
|
||||
async searchXtreamContent(
|
||||
playlistId: string,
|
||||
searchTerm: string,
|
||||
types: string[]
|
||||
): Promise<XtreamContent[]> {
|
||||
const db = await this.getConnection();
|
||||
const placeholders = types.map(() => '?').join(',');
|
||||
return await db.select(
|
||||
`SELECT c.* FROM content c
|
||||
JOIN categories cat ON c.category_id = cat.id
|
||||
WHERE (c.title LIKE ?)
|
||||
AND cat.playlist_id = ?
|
||||
AND c.type IN (${placeholders})
|
||||
LIMIT 50`,
|
||||
[`%${searchTerm}%`, playlistId, ...types]
|
||||
);
|
||||
}
|
||||
|
||||
async getPlaylistById(playlistId: string): Promise<XtreamPlaylist | null> {
|
||||
const db = await this.getConnection();
|
||||
const results = await db.select<XtreamPlaylist[]>(
|
||||
'SELECT * FROM playlists WHERE id = ?',
|
||||
[playlistId]
|
||||
);
|
||||
return results[0] || null;
|
||||
}
|
||||
|
||||
async createPlaylist(playlist: PlaylistMeta): Promise<void> {
|
||||
const db = await this.getConnection();
|
||||
await db.execute(
|
||||
'INSERT INTO playlists (id, name, serverUrl, username, password, type) VALUES (?, ?, ?, ?, ?, ?)',
|
||||
[
|
||||
playlist._id,
|
||||
playlist.title,
|
||||
playlist.serverUrl,
|
||||
playlist.username,
|
||||
playlist.password,
|
||||
'xtream',
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
private prepareBulkInsertData(
|
||||
streams: any[],
|
||||
type: string,
|
||||
categoryMap: Map<number, number>
|
||||
): any[] {
|
||||
return streams
|
||||
.map((stream) => {
|
||||
const streamCategoryId =
|
||||
type === 'series'
|
||||
? parseInt(stream.category_id || '0')
|
||||
: parseInt(stream.category_id);
|
||||
|
||||
const categoryId = categoryMap.get(streamCategoryId);
|
||||
if (!categoryId) return null;
|
||||
|
||||
const title =
|
||||
type === 'series'
|
||||
? stream.title ||
|
||||
stream.name ||
|
||||
`Unknown Series ${stream.series_id}`
|
||||
: stream.name ||
|
||||
stream.title ||
|
||||
`Unknown Stream ${stream.stream_id}`;
|
||||
|
||||
return [
|
||||
categoryId,
|
||||
title,
|
||||
stream.rating || stream.rating_imdb || '',
|
||||
type === 'series'
|
||||
? stream.last_modified || ''
|
||||
: stream.added || '',
|
||||
stream.stream_icon || stream.poster || stream.cover || '',
|
||||
type === 'series'
|
||||
? parseInt(stream.series_id || '0')
|
||||
: parseInt(stream.stream_id || '0'),
|
||||
type,
|
||||
];
|
||||
})
|
||||
.filter((data) => data !== null);
|
||||
}
|
||||
|
||||
private async executeBulkInsert(
|
||||
db: Database,
|
||||
data: any[],
|
||||
onProgress?: (count: number) => void
|
||||
): Promise<number> {
|
||||
const CHUNK_SIZE = 100;
|
||||
let totalInserted = 0;
|
||||
|
||||
for (let i = 0; i < data.length; i += CHUNK_SIZE) {
|
||||
const chunk = data.slice(i, i + CHUNK_SIZE);
|
||||
const placeholders = chunk
|
||||
.map(() => '(?, ?, ?, ?, ?, ?, ?)')
|
||||
.join(', ');
|
||||
const query = `
|
||||
INSERT INTO content (
|
||||
category_id, title, rating, added,
|
||||
poster_url, xtream_id, type
|
||||
) VALUES ${placeholders}
|
||||
`;
|
||||
|
||||
try {
|
||||
await db.execute(query, chunk.flat());
|
||||
totalInserted += chunk.length;
|
||||
onProgress?.(totalInserted);
|
||||
} catch (err) {
|
||||
console.error('Error in bulk insert chunk:', err);
|
||||
}
|
||||
}
|
||||
|
||||
return totalInserted;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import { XtreamCodeActions } from '../../../../shared/xtream-code-actions';
|
||||
import { DataService } from '../../services/data.service';
|
||||
|
||||
export interface XtreamCredentials {
|
||||
url: string;
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class XtreamApiService {
|
||||
private dataService = inject(DataService);
|
||||
|
||||
private buildUrl(baseUrl: string): string {
|
||||
return `${baseUrl}/player_api.php`;
|
||||
}
|
||||
|
||||
async getCategories(
|
||||
credentials: XtreamCredentials,
|
||||
type: 'live' | 'vod' | 'series'
|
||||
) {
|
||||
const action = this.getCategoryAction(type);
|
||||
return await this.dataService.fetchData(
|
||||
this.buildUrl(credentials.url),
|
||||
{
|
||||
action,
|
||||
username: credentials.username,
|
||||
password: credentials.password,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async getStreams(
|
||||
credentials: XtreamCredentials,
|
||||
type: 'live' | 'vod' | 'series'
|
||||
) {
|
||||
const action = this.getStreamAction(type);
|
||||
return await this.dataService.fetchData(
|
||||
this.buildUrl(credentials.url),
|
||||
{
|
||||
action,
|
||||
username: credentials.username,
|
||||
password: credentials.password,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async getVodDetails(credentials: XtreamCredentials, vodId: string) {
|
||||
return await this.dataService.fetchData(
|
||||
this.buildUrl(credentials.url),
|
||||
{
|
||||
action: XtreamCodeActions.GetVodInfo,
|
||||
username: credentials.username,
|
||||
password: credentials.password,
|
||||
vod_id: vodId,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async getSerialDetails(credentials: XtreamCredentials, serialId: string) {
|
||||
return await this.dataService.fetchData(
|
||||
this.buildUrl(credentials.url),
|
||||
{
|
||||
action: XtreamCodeActions.GetSeriesInfo,
|
||||
username: credentials.username,
|
||||
password: credentials.password,
|
||||
series_id: serialId,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async getEpgData(credentials: XtreamCredentials, streamId: number) {
|
||||
return await this.dataService.fetchData(
|
||||
this.buildUrl(credentials.url),
|
||||
{
|
||||
action: 'get_short_epg',
|
||||
username: credentials.username,
|
||||
password: credentials.password,
|
||||
stream_id: streamId,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private getCategoryAction(
|
||||
type: 'live' | 'vod' | 'series'
|
||||
): XtreamCodeActions {
|
||||
switch (type) {
|
||||
case 'live':
|
||||
return XtreamCodeActions.GetLiveCategories;
|
||||
case 'vod':
|
||||
return XtreamCodeActions.GetVodCategories;
|
||||
case 'series':
|
||||
return XtreamCodeActions.GetSeriesCategories;
|
||||
}
|
||||
}
|
||||
|
||||
private getStreamAction(
|
||||
type: 'live' | 'vod' | 'series'
|
||||
): XtreamCodeActions {
|
||||
switch (type) {
|
||||
case 'live':
|
||||
return XtreamCodeActions.GetLiveStreams;
|
||||
case 'vod':
|
||||
return XtreamCodeActions.GetVodStreams;
|
||||
case 'series':
|
||||
return XtreamCodeActions.GetSeries;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import { inject } from '@angular/core';
|
||||
import {
|
||||
patchState,
|
||||
signalStoreFeature,
|
||||
withMethods,
|
||||
withState,
|
||||
} from '@ngrx/signals';
|
||||
import { DatabaseService } from '../services/database.service';
|
||||
import { FavoritesService } from './services/favorites.service';
|
||||
|
||||
export const withFavorites = function () {
|
||||
return signalStoreFeature(
|
||||
withState({
|
||||
isFavorite: false,
|
||||
}),
|
||||
withMethods(
|
||||
(
|
||||
store,
|
||||
dbService = inject(DatabaseService),
|
||||
favoritesService = inject(FavoritesService)
|
||||
) => ({
|
||||
async toggleFavorite(xtreamId: number, playlistId: string) {
|
||||
if (!xtreamId || !playlistId) return;
|
||||
|
||||
const db = await dbService.getConnection();
|
||||
|
||||
const content: any = await db.select(
|
||||
'SELECT content.id FROM content ' +
|
||||
'INNER JOIN categories ON content.category_id = categories.id ' +
|
||||
'WHERE content.xtream_id = ? AND categories.playlist_id = ?',
|
||||
[xtreamId, playlistId]
|
||||
);
|
||||
|
||||
if (!content || content.length === 0) {
|
||||
console.error('Content not found in database');
|
||||
return;
|
||||
}
|
||||
|
||||
const contentId = content[0].id;
|
||||
const isFavorite = await favoritesService.isFavorite(
|
||||
xtreamId,
|
||||
playlistId
|
||||
);
|
||||
|
||||
if (isFavorite) {
|
||||
await favoritesService.removeFromFavorites(
|
||||
contentId,
|
||||
playlistId
|
||||
);
|
||||
} else {
|
||||
await favoritesService.addToFavorites({
|
||||
content_id: contentId,
|
||||
playlist_id: playlistId,
|
||||
});
|
||||
}
|
||||
|
||||
patchState(store, { isFavorite: !isFavorite });
|
||||
},
|
||||
|
||||
async checkFavoriteStatus(
|
||||
xtreamId: number,
|
||||
playlistId: string
|
||||
) {
|
||||
if (!xtreamId || !playlistId) {
|
||||
patchState(store, { isFavorite: false });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!xtreamId) {
|
||||
patchState(store, { isFavorite: false });
|
||||
return;
|
||||
}
|
||||
|
||||
const isFavorite = await favoritesService.isFavorite(
|
||||
xtreamId,
|
||||
playlistId
|
||||
);
|
||||
|
||||
patchState(store, { isFavorite });
|
||||
},
|
||||
})
|
||||
)
|
||||
);
|
||||
};
|
||||
@@ -10,41 +10,13 @@ import {
|
||||
import { rxMethod } from '@ngrx/signals/rxjs-interop';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { EMPTY, from, lastValueFrom, pipe, switchMap, tap } from 'rxjs';
|
||||
import { XtreamCategory } from '../../../shared/xtream-category.interface';
|
||||
import { XtreamCodeActions } from '../../../shared/xtream-code-actions';
|
||||
import { XtreamLiveStream } from '../../../shared/xtream-live-stream.interface';
|
||||
import { XtreamSerieItem } from '../../../shared/xtream-serie-item.interface';
|
||||
import { XtreamVodStream } from '../../../shared/xtream-vod-stream.interface';
|
||||
import { DataService } from '../services/data.service';
|
||||
import { DatabaseService } from '../services/database.service';
|
||||
import { selectActivePlaylist } from '../state/selectors';
|
||||
import { EpgItem } from '../xtream/epg-item.interface';
|
||||
import { FavoritesService } from './services/favorites.service';
|
||||
import { withFavorites } from './with-favorites.feature';
|
||||
import { withRecentItems } from './with-recent-items';
|
||||
|
||||
type XtreamState = {
|
||||
isLoadingCategories: boolean;
|
||||
isLoadingContent: boolean;
|
||||
isImporting: boolean;
|
||||
liveCategories: XtreamCategory[];
|
||||
vodCategories: XtreamCategory[];
|
||||
serialCategories: XtreamCategory[];
|
||||
liveStreams: XtreamLiveStream[];
|
||||
vodStreams: XtreamVodStream[];
|
||||
serialStreams: XtreamSerieItem[];
|
||||
page: number;
|
||||
limit: number;
|
||||
selectedCategoryId: number | null;
|
||||
searchResults: any[];
|
||||
selectedContentType: 'live' | 'vod' | 'series';
|
||||
selectedItem: any | null;
|
||||
importCount: number;
|
||||
itemsToImport: number;
|
||||
currentPlaylist: any | null;
|
||||
isFavorite: boolean;
|
||||
epgItems: EpgItem[];
|
||||
hideExternalInfoDialog: boolean;
|
||||
};
|
||||
import { XtreamState } from './xtream-state';
|
||||
|
||||
const initialState: XtreamState = {
|
||||
isLoadingCategories: false,
|
||||
@@ -65,20 +37,11 @@ const initialState: XtreamState = {
|
||||
selectedItem: null,
|
||||
importCount: 0,
|
||||
currentPlaylist: null,
|
||||
isFavorite: false,
|
||||
epgItems: [],
|
||||
hideExternalInfoDialog:
|
||||
localStorage.getItem('hideExternalInfoDialog') === 'true',
|
||||
};
|
||||
|
||||
interface XCategoryFromDb {
|
||||
id: number;
|
||||
name: string;
|
||||
playlist_id: string; // uuid
|
||||
type: 'movies' | 'live' | 'series';
|
||||
xtream_id: number;
|
||||
}
|
||||
|
||||
/** to decode epg */
|
||||
function b64DecodeUnicode(str: string) {
|
||||
return decodeURIComponent(
|
||||
@@ -93,6 +56,7 @@ function b64DecodeUnicode(str: string) {
|
||||
export const XtreamStore = signalStore(
|
||||
withState(initialState),
|
||||
withRecentItems(),
|
||||
withFavorites(),
|
||||
withComputed((store) => {
|
||||
return {
|
||||
getCategoriesBySelectedType: computed(() => {
|
||||
@@ -198,231 +162,8 @@ export const XtreamStore = signalStore(
|
||||
route = inject(ActivatedRoute),
|
||||
dataService = inject(DataService),
|
||||
dbService = inject(DatabaseService),
|
||||
oldStore = inject(Store),
|
||||
favoritesService = inject(FavoritesService)
|
||||
oldStore = inject(Store)
|
||||
) => {
|
||||
const checkLocalData = async (
|
||||
type: 'live' | 'movies' | 'series'
|
||||
) => {
|
||||
const playlistId = route.snapshot.params.id;
|
||||
console.log('Checking local data...', playlistId, type);
|
||||
const db = await dbService.getConnection();
|
||||
const result = await db.select<XCategoryFromDb[]>(
|
||||
'SELECT * FROM categories WHERE playlist_id = ? AND type = ?',
|
||||
[playlistId, type]
|
||||
);
|
||||
console.log(`Local data for ${type}:`, result);
|
||||
return result.length > 0;
|
||||
};
|
||||
|
||||
const saveCategoriesToDb = async (
|
||||
categories: XtreamCategory[],
|
||||
type: 'live' | 'movies' | 'series'
|
||||
) => {
|
||||
const playlistId = route.snapshot.params.id;
|
||||
const db = await dbService.getConnection();
|
||||
for (const category of categories) {
|
||||
await db.execute(
|
||||
'INSERT INTO categories (playlist_id, name, type, xtream_id) VALUES (?, ?, ?, ?)',
|
||||
[
|
||||
playlistId,
|
||||
category.category_name,
|
||||
type,
|
||||
category.category_id,
|
||||
]
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const getLocalCategories = async (
|
||||
type: 'live' | 'movies' | 'series'
|
||||
) => {
|
||||
const playlistId = route.snapshot.params.id;
|
||||
|
||||
const db = await dbService.getConnection();
|
||||
return await db.select(
|
||||
'SELECT * FROM categories WHERE playlist_id = ? AND type = ?',
|
||||
[playlistId, type]
|
||||
);
|
||||
};
|
||||
|
||||
const checkLocalContent = async (
|
||||
type: 'live' | 'movie' | 'series'
|
||||
) => {
|
||||
const playlistId = route.snapshot.params.id;
|
||||
console.log('Checking local content...');
|
||||
const db = await dbService.getConnection();
|
||||
const result = await db.select(
|
||||
`SELECT c.* FROM content c
|
||||
JOIN categories cat ON c.category_id = cat.id
|
||||
WHERE cat.playlist_id = ? AND c.type = ?
|
||||
ORDER BY c.added`,
|
||||
[playlistId, type]
|
||||
);
|
||||
return (result as any[]).length > 0;
|
||||
};
|
||||
|
||||
const saveContentToDb = async (
|
||||
streams: any[],
|
||||
type: 'live' | 'movie' | 'series'
|
||||
) => {
|
||||
const playlistId = route.snapshot.params.id;
|
||||
|
||||
console.log('Saving content to db...', {
|
||||
playlistId,
|
||||
streamCount: streams.length,
|
||||
type,
|
||||
sampleStream: streams[0],
|
||||
});
|
||||
|
||||
patchState(store, {
|
||||
itemsToImport: store.itemsToImport() + streams.length,
|
||||
});
|
||||
|
||||
try {
|
||||
const db = await dbService.getConnection();
|
||||
const dbType =
|
||||
type === 'series'
|
||||
? 'series'
|
||||
: type === 'movie'
|
||||
? 'movies'
|
||||
: 'live';
|
||||
|
||||
const categories: any[] = await db.select(
|
||||
'SELECT id, xtream_id FROM categories WHERE playlist_id = ? AND type = ?',
|
||||
[playlistId, dbType]
|
||||
);
|
||||
|
||||
console.log('Found categories in DB:', {
|
||||
type: dbType,
|
||||
count: categories.length,
|
||||
categories: categories,
|
||||
});
|
||||
|
||||
const categoryMap = new Map(
|
||||
categories.map((c) => [parseInt(c.xtream_id), c.id])
|
||||
);
|
||||
|
||||
// Prepare bulk insert data
|
||||
const bulkInsertData = streams
|
||||
.map((stream) => {
|
||||
const streamCategoryId =
|
||||
type === 'series'
|
||||
? parseInt(stream.category_id || '0')
|
||||
: parseInt(stream.category_id);
|
||||
|
||||
const categoryId =
|
||||
categoryMap.get(streamCategoryId);
|
||||
if (!categoryId) return null;
|
||||
|
||||
const title =
|
||||
type === 'series'
|
||||
? stream.title ||
|
||||
stream.name ||
|
||||
`Unknown Series ${stream.series_id}`
|
||||
: stream.name ||
|
||||
stream.title ||
|
||||
`Unknown Stream ${stream.stream_id}`;
|
||||
|
||||
return [
|
||||
categoryId,
|
||||
title,
|
||||
stream.rating || stream.rating_imdb || '',
|
||||
type === 'series'
|
||||
? stream.last_modified || ''
|
||||
: stream.added || '',
|
||||
stream.stream_icon ||
|
||||
stream.poster ||
|
||||
stream.cover ||
|
||||
'',
|
||||
type === 'series'
|
||||
? parseInt(stream.series_id || '0')
|
||||
: parseInt(stream.stream_id || '0'),
|
||||
type,
|
||||
];
|
||||
})
|
||||
.filter((data) => data !== null);
|
||||
|
||||
if (bulkInsertData.length > 0) {
|
||||
// Process in chunks to avoid SQLite variable limit
|
||||
// Each row has 7 variables, so we'll insert 100 rows at a time (700 variables)
|
||||
const CHUNK_SIZE = 100;
|
||||
const chunks = [];
|
||||
|
||||
for (
|
||||
let i = 0;
|
||||
i < bulkInsertData.length;
|
||||
i += CHUNK_SIZE
|
||||
) {
|
||||
chunks.push(
|
||||
bulkInsertData.slice(i, i + CHUNK_SIZE)
|
||||
);
|
||||
}
|
||||
|
||||
let totalInserted = 0;
|
||||
|
||||
for (const chunk of chunks) {
|
||||
try {
|
||||
// Construct the bulk insert query for this chunk
|
||||
const placeholders = chunk
|
||||
.map(() => '(?, ?, ?, ?, ?, ?, ?)')
|
||||
.join(', ');
|
||||
|
||||
const query = `
|
||||
INSERT INTO content (
|
||||
category_id, title, rating, added,
|
||||
poster_url, xtream_id, type
|
||||
) VALUES ${placeholders}
|
||||
`;
|
||||
|
||||
// Flatten the chunk array for the bulk insert
|
||||
const flattenedValues = chunk.flat();
|
||||
|
||||
await db.execute(query, flattenedValues);
|
||||
totalInserted += chunk.length;
|
||||
|
||||
console.log(
|
||||
`Successfully inserted chunk of ${chunk.length} streams. Total: ${totalInserted}/${bulkInsertData.length}`
|
||||
);
|
||||
} catch (err) {
|
||||
console.error(
|
||||
'Error in bulk insert chunk:',
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
patchState(store, {
|
||||
importCount: store.importCount() + totalInserted,
|
||||
});
|
||||
|
||||
console.log(
|
||||
`Completed bulk insert: ${totalInserted} of ${bulkInsertData.length} streams inserted`
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error in saveContentToDb:', err);
|
||||
}
|
||||
};
|
||||
|
||||
const getLocalContent = async (
|
||||
type: 'live' | 'movie' | 'series'
|
||||
) => {
|
||||
const playlistId = route.snapshot.params.id;
|
||||
console.log('Getting local content...');
|
||||
const db = await dbService.getConnection();
|
||||
return await db.select(
|
||||
`SELECT
|
||||
c.id, c.category_id, c.title, c.rating,
|
||||
c.added, c.poster_url, c.xtream_id, c.type
|
||||
FROM content c
|
||||
INNER JOIN categories cat ON c.category_id = cat.id
|
||||
WHERE cat.playlist_id = ? AND c.type = ?
|
||||
ORDER BY c.added DESC`,
|
||||
[playlistId, type]
|
||||
);
|
||||
};
|
||||
|
||||
const fetchCategories = (
|
||||
action: XtreamCodeActions,
|
||||
stateKey: keyof Pick<
|
||||
@@ -439,51 +180,45 @@ export const XtreamStore = signalStore(
|
||||
password: store.currentPlaylist().password,
|
||||
};
|
||||
|
||||
return from(checkLocalData(type)).pipe(
|
||||
return from(
|
||||
dbService.hasXtreamCategories(
|
||||
route.snapshot.params.id,
|
||||
type
|
||||
)
|
||||
).pipe(
|
||||
switchMap(async (exists) => {
|
||||
try {
|
||||
if (exists) {
|
||||
const localData =
|
||||
await getLocalCategories(type);
|
||||
await dbService.getXtreamCategories(
|
||||
route.snapshot.params.id,
|
||||
type
|
||||
);
|
||||
patchState(store, {
|
||||
[stateKey]: localData,
|
||||
isLoadingCategories: false,
|
||||
});
|
||||
return localData;
|
||||
}
|
||||
console.log(
|
||||
'Fetching remote categories:',
|
||||
store.currentPlaylist()
|
||||
);
|
||||
|
||||
const remoteData = await dataService.fetchData(
|
||||
`${store.currentPlaylist().serverUrl}/player_api.php`,
|
||||
queryParams
|
||||
);
|
||||
|
||||
if (remoteData && Array.isArray(remoteData)) {
|
||||
console.log(
|
||||
`Got remote ${type} categories:`,
|
||||
remoteData.length
|
||||
await dbService.saveXtreamCategories(
|
||||
route.snapshot.params.id,
|
||||
remoteData,
|
||||
type
|
||||
);
|
||||
await saveCategoriesToDb(remoteData, type);
|
||||
} else {
|
||||
console.error(
|
||||
'Invalid remote data received:',
|
||||
remoteData
|
||||
);
|
||||
patchState(store, {
|
||||
isLoadingCategories: false,
|
||||
});
|
||||
return [];
|
||||
}
|
||||
|
||||
const localData: any =
|
||||
await getLocalCategories(type);
|
||||
console.log(
|
||||
`Loaded ${type} categories from DB:`,
|
||||
localData.length
|
||||
);
|
||||
|
||||
const localData =
|
||||
await dbService.getXtreamCategories(
|
||||
route.snapshot.params.id,
|
||||
type
|
||||
);
|
||||
patchState(store, {
|
||||
[stateKey]: localData,
|
||||
isLoadingCategories: false,
|
||||
@@ -518,11 +253,17 @@ export const XtreamStore = signalStore(
|
||||
password: store.currentPlaylist().password,
|
||||
};
|
||||
|
||||
return from(checkLocalContent(type)).pipe(
|
||||
return from(
|
||||
dbService.hasXtreamContent(route.snapshot.params.id, type)
|
||||
).pipe(
|
||||
switchMap(async (exists) => {
|
||||
try {
|
||||
if (exists) {
|
||||
const localData = await getLocalContent(type);
|
||||
const localData =
|
||||
await dbService.getXtreamContent(
|
||||
route.snapshot.params.id,
|
||||
type
|
||||
);
|
||||
patchState(store, {
|
||||
[stateKey]: localData,
|
||||
isLoadingContent: false,
|
||||
@@ -536,11 +277,28 @@ export const XtreamStore = signalStore(
|
||||
);
|
||||
|
||||
if (remoteData && Array.isArray(remoteData)) {
|
||||
await saveContentToDb(remoteData, type);
|
||||
patchState(store, {
|
||||
itemsToImport:
|
||||
store.itemsToImport() +
|
||||
remoteData.length,
|
||||
});
|
||||
const insertedCount =
|
||||
await dbService.saveXtreamContent(
|
||||
route.snapshot.params.id,
|
||||
remoteData,
|
||||
type,
|
||||
(count) =>
|
||||
patchState(store, {
|
||||
importCount: count,
|
||||
})
|
||||
);
|
||||
console.log(`Inserted ${insertedCount} items`);
|
||||
}
|
||||
|
||||
const localData = await getLocalContent(type);
|
||||
|
||||
const localData = await dbService.getXtreamContent(
|
||||
route.snapshot.params.id,
|
||||
type
|
||||
);
|
||||
patchState(store, {
|
||||
[stateKey]: localData,
|
||||
isLoadingContent: false,
|
||||
@@ -560,60 +318,31 @@ export const XtreamStore = signalStore(
|
||||
searchTerm: string,
|
||||
types: string[]
|
||||
) => {
|
||||
const db = await dbService.getConnection();
|
||||
const placeholders = types.map(() => '?').join(',');
|
||||
const playlistId = route.snapshot.params.id;
|
||||
const results: any[] = await db.select(
|
||||
`SELECT c.* FROM content c
|
||||
JOIN categories cat ON c.category_id = cat.id
|
||||
WHERE (c.title LIKE ?)
|
||||
AND cat.playlist_id = ?
|
||||
AND c.type IN (${placeholders})
|
||||
LIMIT 50`,
|
||||
[`%${searchTerm}%`, playlistId, ...types]
|
||||
const results = await dbService.searchXtreamContent(
|
||||
route.snapshot.params.id,
|
||||
searchTerm,
|
||||
types
|
||||
);
|
||||
|
||||
patchState(store, { searchResults: results });
|
||||
};
|
||||
|
||||
const fetchXtreamPlaylist = rxMethod<void>(
|
||||
pipe(() => {
|
||||
const playlistId = route.snapshot.params.id;
|
||||
if (!playlistId) return EMPTY;
|
||||
return from(dbService.getConnection()).pipe(
|
||||
switchMap((db) =>
|
||||
from(
|
||||
db.select(
|
||||
'SELECT * FROM playlists WHERE id = ?',
|
||||
[playlistId]
|
||||
)
|
||||
)
|
||||
),
|
||||
switchMap(async (res) => {
|
||||
if (res[0]) {
|
||||
patchState(store, { currentPlaylist: res[0] });
|
||||
return from(dbService.getPlaylistById(playlistId)).pipe(
|
||||
switchMap(async (playlist) => {
|
||||
if (playlist) {
|
||||
patchState(store, {
|
||||
currentPlaylist: playlist,
|
||||
});
|
||||
} else {
|
||||
const playlist =
|
||||
oldStore.selectSignal(
|
||||
selectActivePlaylist
|
||||
)();
|
||||
console.log(
|
||||
'Fetched playlist from default store:',
|
||||
playlist
|
||||
);
|
||||
if (playlist) {
|
||||
const db = await dbService.getConnection();
|
||||
// Insert into DB
|
||||
await db.execute(
|
||||
'INSERT INTO playlists (id, name, serverUrl, username, password, type) VALUES (?, ?, ?, ?, ?, ?)',
|
||||
[
|
||||
playlist._id,
|
||||
playlist.title,
|
||||
playlist.serverUrl,
|
||||
playlist.username,
|
||||
playlist.password,
|
||||
'xtream',
|
||||
]
|
||||
);
|
||||
await dbService.createPlaylist(playlist);
|
||||
patchState(store, {
|
||||
currentPlaylist: {
|
||||
...playlist,
|
||||
@@ -907,69 +636,6 @@ export const XtreamStore = signalStore(
|
||||
return [];
|
||||
}
|
||||
},
|
||||
async toggleFavorite() {
|
||||
const item = store.selectedItem();
|
||||
const playlist = store.currentPlaylist();
|
||||
|
||||
if (!item || !playlist) return;
|
||||
|
||||
const db = await dbService.getConnection();
|
||||
const xtreamId = item.stream_id || item.series_id;
|
||||
|
||||
const content: any = await db.select(
|
||||
'SELECT id FROM content WHERE xtream_id = ?',
|
||||
[xtreamId]
|
||||
);
|
||||
|
||||
if (!content || content.length === 0) {
|
||||
console.error('Content not found in database');
|
||||
return;
|
||||
}
|
||||
|
||||
const contentId = content[0].id;
|
||||
const isFavorite = await favoritesService.isFavorite(
|
||||
xtreamId,
|
||||
playlist.id
|
||||
);
|
||||
|
||||
if (isFavorite) {
|
||||
await favoritesService.removeFromFavorites(
|
||||
contentId,
|
||||
playlist.id
|
||||
);
|
||||
} else {
|
||||
await favoritesService.addToFavorites({
|
||||
content_id: contentId,
|
||||
playlist_id: playlist.id,
|
||||
});
|
||||
}
|
||||
|
||||
patchState(store, { isFavorite: !isFavorite });
|
||||
},
|
||||
|
||||
async checkFavoriteStatus() {
|
||||
const item = store.selectedItem();
|
||||
const playlist = store.currentPlaylist();
|
||||
|
||||
if (!item || !playlist) {
|
||||
patchState(store, { isFavorite: false });
|
||||
return;
|
||||
}
|
||||
|
||||
const xtreamId = item.stream_id || item.series_id;
|
||||
|
||||
if (!xtreamId) {
|
||||
patchState(store, { isFavorite: false });
|
||||
return;
|
||||
}
|
||||
|
||||
const isFavorite = await favoritesService.isFavorite(
|
||||
xtreamId,
|
||||
playlist.id
|
||||
);
|
||||
|
||||
patchState(store, { isFavorite });
|
||||
},
|
||||
setHideExternalInfoDialog(hideExternalInfoDialog: boolean) {
|
||||
localStorage.setItem(
|
||||
'hideExternalInfoDialog',
|
||||
|
||||
Reference in new issue
Block a user