mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-16 14:16:16 -08:00
refactor(electron): migrate from tauri/plugin-sql to libsql
This commit is contained in:
1 parent
6dcbbadda8
commit
0a934d2ac6
31 files changed
+3458
-752
No files matched your search
@@ -1,5 +1,5 @@
|
||||
export * from './lib/data.service';
|
||||
export * from './lib/database.service';
|
||||
export * from './lib/database-electron.service';
|
||||
export * from './lib/epg.service';
|
||||
export * from './lib/playlists.service';
|
||||
export * from './lib/portal-status.service';
|
||||
|
||||
@@ -0,0 +1,392 @@
|
||||
/**
|
||||
* Database service for Electron renderer process
|
||||
* Communicates with the main process database via IPC
|
||||
*/
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { PlaylistMeta } from 'shared-interfaces';
|
||||
|
||||
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;
|
||||
added_at?: string;
|
||||
viewed_at?: string;
|
||||
}
|
||||
|
||||
export interface XtreamPlaylist {
|
||||
id: string;
|
||||
name: string;
|
||||
serverUrl: string;
|
||||
username: string;
|
||||
password: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class DatabaseService {
|
||||
/**
|
||||
* Delete a playlist and all its related data
|
||||
*/
|
||||
async deletePlaylist(playlistId: string): Promise<boolean> {
|
||||
try {
|
||||
await window.electron.dbDeletePlaylist(playlistId);
|
||||
return true;
|
||||
} catch (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
|
||||
*/
|
||||
async deleteXtreamPlaylistContent(playlistId: string): Promise<void> {
|
||||
await window.electron.dbDeleteXtreamContent(playlistId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update playlist basic info
|
||||
*/
|
||||
async updateXtreamPlaylist(playlist: any): 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;
|
||||
}): Promise<boolean> {
|
||||
try {
|
||||
await window.electron.dbUpdatePlaylist(playlist.id, {
|
||||
name: playlist.title,
|
||||
username: playlist.username,
|
||||
password: playlist.password,
|
||||
serverUrl: playlist.serverUrl,
|
||||
});
|
||||
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
|
||||
*/
|
||||
async saveXtreamCategories(
|
||||
playlistId: string,
|
||||
categories: any[],
|
||||
type: 'live' | 'movies' | 'series'
|
||||
): Promise<void> {
|
||||
await window.electron.dbSaveCategories(playlistId, categories, type);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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: any[],
|
||||
type: 'live' | 'movie' | 'series',
|
||||
onProgress?: (count: number) => void
|
||||
): Promise<number> {
|
||||
// Setup progress listener if callback provided
|
||||
if (onProgress) {
|
||||
window.addEventListener('message', (event) => {
|
||||
if (event.data.type === 'DB_SAVE_CONTENT_PROGRESS') {
|
||||
onProgress(event.data.count);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const result = await window.electron.dbSaveContent(
|
||||
playlistId,
|
||||
streams,
|
||||
type
|
||||
);
|
||||
return result.count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search content within a playlist
|
||||
*/
|
||||
async searchXtreamContent(
|
||||
playlistId: string,
|
||||
searchTerm: string,
|
||||
types: string[]
|
||||
): Promise<XtreamContent[]> {
|
||||
return await window.electron.dbSearchContent(
|
||||
playlistId,
|
||||
searchTerm,
|
||||
types
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Global search across all playlists
|
||||
*/
|
||||
async globalSearchContent(
|
||||
searchTerm: string,
|
||||
types: string[]
|
||||
): Promise<GlobalSearchResult[]> {
|
||||
return await window.electron.dbGlobalSearch(searchTerm, types);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,484 +0,0 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import Database from '@tauri-apps/plugin-sql';
|
||||
import { PlaylistMeta } from 'shared-interfaces';
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class DatabaseService {
|
||||
private static db: Database | null = null;
|
||||
|
||||
async getConnection(): Promise<Database> {
|
||||
if (!DatabaseService.db) {
|
||||
DatabaseService.db = await Database.load('sqlite:database.db');
|
||||
}
|
||||
return DatabaseService.db;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a playlist and all its related data
|
||||
* @param playlistId ID of the playlist to delete
|
||||
* @returns True if deletion was successful
|
||||
*/
|
||||
async deletePlaylist(playlistId: string): Promise<boolean> {
|
||||
try {
|
||||
const db = await this.getConnection();
|
||||
|
||||
// Start a transaction to ensure all related data is deleted
|
||||
await db.execute('BEGIN TRANSACTION');
|
||||
|
||||
try {
|
||||
// First, delete favorites related to the playlist to reduce foreign key checks
|
||||
await db.execute(
|
||||
'DELETE FROM favorites WHERE playlist_id = ?',
|
||||
[playlistId]
|
||||
);
|
||||
|
||||
// Delete recently_viewed entries directly using playlist_id
|
||||
await db.execute(
|
||||
'DELETE FROM recently_viewed WHERE playlist_id = ?',
|
||||
[playlistId]
|
||||
);
|
||||
|
||||
// Get category IDs first to speed up content deletion (avoids subquery)
|
||||
const categories = await db.select<{ id: number }[]>(
|
||||
'SELECT id FROM categories WHERE playlist_id = ?',
|
||||
[playlistId]
|
||||
);
|
||||
|
||||
if (categories.length > 0) {
|
||||
// Create a list of category IDs for the IN clause
|
||||
const categoryIds = categories.map((cat) => cat.id);
|
||||
|
||||
// Delete content in batches if there are many categories
|
||||
const BATCH_SIZE = 20;
|
||||
for (let i = 0; i < categoryIds.length; i += BATCH_SIZE) {
|
||||
const batchIds = categoryIds.slice(i, i + BATCH_SIZE);
|
||||
const placeholders = batchIds.map(() => '?').join(',');
|
||||
|
||||
await db.execute(
|
||||
`DELETE FROM content WHERE category_id IN (${placeholders})`,
|
||||
batchIds
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Delete categories related to the playlist
|
||||
await db.execute(
|
||||
'DELETE FROM categories WHERE playlist_id = ?',
|
||||
[playlistId]
|
||||
);
|
||||
|
||||
// Finally, delete the playlist itself
|
||||
await db.execute('DELETE FROM playlists WHERE id = ?', [
|
||||
playlistId,
|
||||
]);
|
||||
|
||||
// Commit the transaction
|
||||
await db.execute('COMMIT');
|
||||
console.log('Playlist deleted successfully');
|
||||
return true;
|
||||
} catch (error) {
|
||||
// If any error occurs, rollback the transaction
|
||||
console.error('Error in transaction, rolling back:', error);
|
||||
await db.execute('ROLLBACK');
|
||||
throw error;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error deleting playlist:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async updateXtreamPlaylist(playlist: any): Promise<boolean> {
|
||||
try {
|
||||
const db = await this.getConnection();
|
||||
await db.execute('UPDATE playlists SET name = ? WHERE id = ?', [
|
||||
playlist.name,
|
||||
playlist.id,
|
||||
]);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error updating playlist:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async updateXtreamPlaylistDetails(playlist: {
|
||||
id: string;
|
||||
title: string;
|
||||
username?: string;
|
||||
password?: string;
|
||||
serverUrl?: string;
|
||||
}): Promise<boolean> {
|
||||
try {
|
||||
const db = await this.getConnection();
|
||||
const updateFields: string[] = ['name = ?'];
|
||||
const params: any[] = [playlist.title];
|
||||
|
||||
if (playlist.username) {
|
||||
updateFields.push('username = ?');
|
||||
params.push(playlist.username);
|
||||
}
|
||||
if (playlist.password) {
|
||||
updateFields.push('password = ?');
|
||||
params.push(playlist.password);
|
||||
}
|
||||
if (playlist.serverUrl) {
|
||||
updateFields.push('serverUrl = ?');
|
||||
params.push(playlist.serverUrl);
|
||||
}
|
||||
|
||||
params.push(playlist.id);
|
||||
|
||||
console.log(params);
|
||||
|
||||
const query = `UPDATE playlists SET ${updateFields.join(', ')} WHERE id = ?`;
|
||||
await db.execute(query, params);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error updating playlist details:', error);
|
||||
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 = ? ORDER BY name COLLATE NOCASE',
|
||||
[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 globalSearchContent(
|
||||
searchTerm: string,
|
||||
types: string[]
|
||||
): Promise<GlobalSearchResult[]> {
|
||||
const db = await this.getConnection();
|
||||
const placeholders = types.map(() => '?').join(',');
|
||||
|
||||
// Use a materialized subquery for better performance
|
||||
return await db.select(
|
||||
`
|
||||
WITH filtered_content AS (
|
||||
SELECT
|
||||
c.id,
|
||||
c.category_id,
|
||||
c.title,
|
||||
c.rating,
|
||||
c.added,
|
||||
c.poster_url,
|
||||
c.xtream_id,
|
||||
c.type,
|
||||
cat.playlist_id,
|
||||
p.name as playlist_name
|
||||
FROM content c
|
||||
INNER JOIN categories cat ON c.category_id = cat.id
|
||||
INNER JOIN playlists p ON cat.playlist_id = p.id
|
||||
WHERE c.type IN (${placeholders})
|
||||
)
|
||||
SELECT * FROM filtered_content
|
||||
WHERE LOWER(title) LIKE LOWER(?)
|
||||
ORDER BY title
|
||||
LIMIT 50
|
||||
`,
|
||||
[...types, `%${searchTerm}%`]
|
||||
);
|
||||
}
|
||||
|
||||
async getGlobalRecentlyViewed(): Promise<GlobalRecentItem[]> {
|
||||
try {
|
||||
console.log('Starting getGlobalRecentlyViewed query...');
|
||||
const db = await this.getConnection();
|
||||
console.log('Got database connection');
|
||||
|
||||
// Check if table exists
|
||||
const tableCheck = await db.select(`
|
||||
SELECT name FROM sqlite_master
|
||||
WHERE type='table' AND name='recently_viewed'
|
||||
`);
|
||||
console.log('Tables check:', tableCheck);
|
||||
|
||||
const items = await db.select<GlobalRecentItem[]>(`
|
||||
SELECT
|
||||
c.id,
|
||||
c.category_id,
|
||||
c.title,
|
||||
c.rating,
|
||||
c.added,
|
||||
c.poster_url,
|
||||
c.xtream_id,
|
||||
c.type,
|
||||
cat.playlist_id,
|
||||
p.name as playlist_name,
|
||||
rv.viewed_at
|
||||
FROM recently_viewed rv
|
||||
INNER JOIN content c ON rv.content_id = c.id
|
||||
INNER JOIN categories cat ON c.category_id = cat.id
|
||||
INNER JOIN playlists p ON cat.playlist_id = p.id
|
||||
ORDER BY rv.viewed_at DESC
|
||||
LIMIT 100
|
||||
`);
|
||||
|
||||
console.log('Query executed, items found:', items?.length);
|
||||
console.log('First few items:', items?.slice(0, 3));
|
||||
|
||||
return items || [];
|
||||
} catch (error) {
|
||||
console.error('Detailed error in getGlobalRecentlyViewed:', error);
|
||||
throw error; // Let's throw the error to see it in the component
|
||||
}
|
||||
}
|
||||
|
||||
async clearGlobalRecentlyViewed(): Promise<void> {
|
||||
try {
|
||||
const db = await this.getConnection();
|
||||
await db.execute('DELETE FROM recently_viewed');
|
||||
} catch (error) {
|
||||
console.error('Error clearing global recently viewed:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
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 as any).flat());
|
||||
totalInserted += chunk.length;
|
||||
onProgress?.(totalInserted);
|
||||
} catch (err) {
|
||||
console.error('Error in bulk insert chunk:', err);
|
||||
}
|
||||
}
|
||||
|
||||
return totalInserted;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user