mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-11 19:46:16 -08:00
Merge pull request #702 from 4gray/refactor/restructure-database-events
refactor(electron-backend): split DB event handlers into modules
This commit is contained in:
7 files changed
+1089
-1003
No files matched your search
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* Category IPC event handlers
|
||||
* Operations for managing categories within playlists
|
||||
*/
|
||||
|
||||
import { and, eq, sql } from 'drizzle-orm';
|
||||
import { ipcMain } from 'electron';
|
||||
import { getDatabase } from '../../database/connection';
|
||||
import * as schema from '../../database/schema';
|
||||
|
||||
/**
|
||||
* Check if categories exist for a playlist
|
||||
*/
|
||||
ipcMain.handle(
|
||||
'DB_HAS_CATEGORIES',
|
||||
async (event, playlistId: string, type: 'live' | 'movies' | 'series') => {
|
||||
try {
|
||||
const db = await getDatabase();
|
||||
const result = await db
|
||||
.select({ count: sql<number>`count(*)` })
|
||||
.from(schema.categories)
|
||||
.where(
|
||||
and(
|
||||
eq(schema.categories.playlistId, playlistId),
|
||||
eq(schema.categories.type, type)
|
||||
)
|
||||
);
|
||||
return result[0].count > 0;
|
||||
} catch (error) {
|
||||
console.error('Error checking categories:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Get categories for a playlist
|
||||
*/
|
||||
ipcMain.handle(
|
||||
'DB_GET_CATEGORIES',
|
||||
async (event, playlistId: string, type: 'live' | 'movies' | 'series') => {
|
||||
try {
|
||||
const db = await getDatabase();
|
||||
const result = await db
|
||||
.select()
|
||||
.from(schema.categories)
|
||||
.where(
|
||||
and(
|
||||
eq(schema.categories.playlistId, playlistId),
|
||||
eq(schema.categories.type, type)
|
||||
)
|
||||
)
|
||||
.orderBy(sql`name COLLATE NOCASE`);
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('Error getting categories:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Save categories in bulk
|
||||
*/
|
||||
ipcMain.handle(
|
||||
'DB_SAVE_CATEGORIES',
|
||||
async (
|
||||
event,
|
||||
playlistId: string,
|
||||
categories: Array<{
|
||||
category_name: string;
|
||||
category_id: number;
|
||||
}>,
|
||||
type: 'live' | 'movies' | 'series'
|
||||
) => {
|
||||
try {
|
||||
const db = await getDatabase();
|
||||
const values = categories.map((cat) => ({
|
||||
playlistId,
|
||||
name: cat.category_name,
|
||||
type,
|
||||
xtreamId: cat.category_id,
|
||||
}));
|
||||
|
||||
await db.insert(schema.categories).values(values);
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('Error saving categories:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -0,0 +1,328 @@
|
||||
/**
|
||||
* Content IPC event handlers
|
||||
* Operations for managing and searching content (streams, movies, series)
|
||||
*/
|
||||
|
||||
import { and, desc, eq, inArray, sql } from 'drizzle-orm';
|
||||
import { ipcMain } from 'electron';
|
||||
import { getDatabase } from '../../database/connection';
|
||||
import * as schema from '../../database/schema';
|
||||
|
||||
/**
|
||||
* Escape special LIKE pattern characters to prevent unexpected matching behavior.
|
||||
* This escapes %, _, and \ which have special meaning in SQL LIKE patterns.
|
||||
*/
|
||||
function escapeLikePattern(term: string): string {
|
||||
return term.replace(/[%_\\]/g, '\\$&');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if content exists
|
||||
*/
|
||||
ipcMain.handle(
|
||||
'DB_HAS_CONTENT',
|
||||
async (event, playlistId: string, type: 'live' | 'movie' | 'series') => {
|
||||
try {
|
||||
const db = await getDatabase();
|
||||
const result = await db
|
||||
.select({ count: sql<number>`count(*)` })
|
||||
.from(schema.content)
|
||||
.innerJoin(
|
||||
schema.categories,
|
||||
eq(schema.content.categoryId, schema.categories.id)
|
||||
)
|
||||
.where(
|
||||
and(
|
||||
eq(schema.categories.playlistId, playlistId),
|
||||
eq(schema.content.type, type)
|
||||
)
|
||||
);
|
||||
return result[0].count > 0;
|
||||
} catch (error) {
|
||||
console.error('Error checking content:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Get content for a playlist
|
||||
*/
|
||||
ipcMain.handle(
|
||||
'DB_GET_CONTENT',
|
||||
async (event, playlistId: string, type: 'live' | 'movie' | 'series') => {
|
||||
try {
|
||||
const db = await getDatabase();
|
||||
const result = await db
|
||||
.select({
|
||||
id: schema.content.id,
|
||||
category_id: schema.content.categoryId,
|
||||
title: schema.content.title,
|
||||
rating: schema.content.rating,
|
||||
added: schema.content.added,
|
||||
poster_url: schema.content.posterUrl,
|
||||
xtream_id: schema.content.xtreamId,
|
||||
type: schema.content.type,
|
||||
})
|
||||
.from(schema.content)
|
||||
.innerJoin(
|
||||
schema.categories,
|
||||
eq(schema.content.categoryId, schema.categories.id)
|
||||
)
|
||||
.where(
|
||||
and(
|
||||
eq(schema.categories.playlistId, playlistId),
|
||||
eq(schema.content.type, type)
|
||||
)
|
||||
)
|
||||
.orderBy(desc(schema.content.added));
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('Error getting content:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Save content in bulk with progress reporting
|
||||
*/
|
||||
ipcMain.handle(
|
||||
'DB_SAVE_CONTENT',
|
||||
async (
|
||||
event,
|
||||
playlistId: string,
|
||||
streams: Array<Record<string, unknown>>,
|
||||
type: 'live' | 'movie' | 'series'
|
||||
) => {
|
||||
try {
|
||||
console.log(
|
||||
`>>> DB_SAVE_CONTENT called: ${streams.length} items, type: ${type}, playlist: ${playlistId}`
|
||||
);
|
||||
const db = await getDatabase();
|
||||
const dbType =
|
||||
type === 'series'
|
||||
? 'series'
|
||||
: type === 'movie'
|
||||
? 'movies'
|
||||
: 'live';
|
||||
|
||||
// Get categories with their IDs
|
||||
const categories = await db
|
||||
.select({
|
||||
id: schema.categories.id,
|
||||
xtreamId: schema.categories.xtreamId,
|
||||
})
|
||||
.from(schema.categories)
|
||||
.where(
|
||||
and(
|
||||
eq(schema.categories.playlistId, playlistId),
|
||||
eq(schema.categories.type, dbType)
|
||||
)
|
||||
);
|
||||
|
||||
const categoryMap = new Map(
|
||||
categories.map((c) => [c.xtreamId, c.id])
|
||||
);
|
||||
|
||||
// Prepare bulk insert data
|
||||
const values = streams
|
||||
.map((stream) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const s = stream as any;
|
||||
const streamCategoryId =
|
||||
type === 'series'
|
||||
? parseInt(s.category_id || '0')
|
||||
: parseInt(s.category_id);
|
||||
|
||||
const categoryId = categoryMap.get(streamCategoryId);
|
||||
if (!categoryId) return null;
|
||||
|
||||
const title =
|
||||
type === 'series'
|
||||
? s.title ||
|
||||
s.name ||
|
||||
`Unknown Series ${s.series_id}`
|
||||
: s.name ||
|
||||
s.title ||
|
||||
`Unknown Stream ${s.stream_id}`;
|
||||
|
||||
return {
|
||||
categoryId,
|
||||
title,
|
||||
rating: s.rating || s.rating_imdb || '',
|
||||
added:
|
||||
type === 'series'
|
||||
? s.last_modified || ''
|
||||
: s.added || '',
|
||||
posterUrl: s.stream_icon || s.poster || s.cover || '',
|
||||
xtreamId:
|
||||
type === 'series'
|
||||
? parseInt(s.series_id || '0')
|
||||
: parseInt(s.stream_id || '0'),
|
||||
type,
|
||||
};
|
||||
})
|
||||
.filter((data) => data !== null);
|
||||
|
||||
// Insert in chunks for better performance
|
||||
const CHUNK_SIZE = 100;
|
||||
let totalInserted = 0;
|
||||
|
||||
for (let i = 0; i < values.length; i += CHUNK_SIZE) {
|
||||
const chunk = values.slice(i, i + CHUNK_SIZE);
|
||||
await db.insert(schema.content).values(
|
||||
chunk as Array<{
|
||||
categoryId: number;
|
||||
title: string;
|
||||
rating: string;
|
||||
added: string;
|
||||
posterUrl: string;
|
||||
xtreamId: number;
|
||||
type: 'live' | 'movie' | 'series';
|
||||
}>
|
||||
);
|
||||
totalInserted += chunk.length;
|
||||
|
||||
// Send progress update
|
||||
event.sender.send('DB_SAVE_CONTENT_PROGRESS', totalInserted);
|
||||
}
|
||||
|
||||
return { success: true, count: totalInserted };
|
||||
} catch (error) {
|
||||
console.error('Error saving content:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Get content by xtream ID
|
||||
*/
|
||||
ipcMain.handle(
|
||||
'DB_GET_CONTENT_BY_XTREAM_ID',
|
||||
async (event, xtreamId: number, playlistId: string) => {
|
||||
try {
|
||||
const db = await getDatabase();
|
||||
const result = await db
|
||||
.select({
|
||||
id: schema.content.id,
|
||||
category_id: schema.content.categoryId,
|
||||
title: schema.content.title,
|
||||
rating: schema.content.rating,
|
||||
added: schema.content.added,
|
||||
poster_url: schema.content.posterUrl,
|
||||
xtream_id: schema.content.xtreamId,
|
||||
type: schema.content.type,
|
||||
})
|
||||
.from(schema.content)
|
||||
.innerJoin(
|
||||
schema.categories,
|
||||
eq(schema.content.categoryId, schema.categories.id)
|
||||
)
|
||||
.where(
|
||||
and(
|
||||
eq(schema.content.xtreamId, xtreamId),
|
||||
eq(schema.categories.playlistId, playlistId)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
return result[0] || null;
|
||||
} catch (error) {
|
||||
console.error('Error getting content by xtream ID:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Search content within a specific playlist
|
||||
*/
|
||||
ipcMain.handle(
|
||||
'DB_SEARCH_CONTENT',
|
||||
async (event, playlistId: string, searchTerm: string, types: string[]) => {
|
||||
try {
|
||||
const db = await getDatabase();
|
||||
const result = await db
|
||||
.select({
|
||||
id: schema.content.id,
|
||||
category_id: schema.content.categoryId,
|
||||
title: schema.content.title,
|
||||
rating: schema.content.rating,
|
||||
added: schema.content.added,
|
||||
poster_url: schema.content.posterUrl,
|
||||
xtream_id: schema.content.xtreamId,
|
||||
type: schema.content.type,
|
||||
})
|
||||
.from(schema.content)
|
||||
.innerJoin(
|
||||
schema.categories,
|
||||
eq(schema.content.categoryId, schema.categories.id)
|
||||
)
|
||||
.where(
|
||||
and(
|
||||
eq(schema.categories.playlistId, playlistId),
|
||||
sql`LOWER(${schema.content.title}) LIKE ${'%' + escapeLikePattern(searchTerm.toLowerCase()) + '%'} ESCAPE '\\'`,
|
||||
inArray(
|
||||
schema.content.type,
|
||||
types as Array<'live' | 'movie' | 'series'>
|
||||
)
|
||||
)
|
||||
)
|
||||
.limit(50);
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('Error searching content:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Global search across all playlists
|
||||
*/
|
||||
ipcMain.handle(
|
||||
'DB_GLOBAL_SEARCH',
|
||||
async (event, searchTerm: string, types: string[]) => {
|
||||
try {
|
||||
const db = await getDatabase();
|
||||
const result = await db
|
||||
.select({
|
||||
id: schema.content.id,
|
||||
category_id: schema.content.categoryId,
|
||||
title: schema.content.title,
|
||||
rating: schema.content.rating,
|
||||
added: schema.content.added,
|
||||
poster_url: schema.content.posterUrl,
|
||||
xtream_id: schema.content.xtreamId,
|
||||
type: schema.content.type,
|
||||
playlist_id: schema.categories.playlistId,
|
||||
playlist_name: schema.playlists.name,
|
||||
})
|
||||
.from(schema.content)
|
||||
.innerJoin(
|
||||
schema.categories,
|
||||
eq(schema.content.categoryId, schema.categories.id)
|
||||
)
|
||||
.innerJoin(
|
||||
schema.playlists,
|
||||
eq(schema.categories.playlistId, schema.playlists.id)
|
||||
)
|
||||
.where(
|
||||
and(
|
||||
sql`LOWER(${schema.content.title}) LIKE ${'%' + escapeLikePattern(searchTerm.toLowerCase()) + '%'} ESCAPE '\\'`,
|
||||
inArray(
|
||||
schema.content.type,
|
||||
types as Array<'live' | 'movie' | 'series'>
|
||||
)
|
||||
)
|
||||
)
|
||||
.orderBy(schema.content.title)
|
||||
.limit(50);
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('Error in global search:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -0,0 +1,110 @@
|
||||
/**
|
||||
* Favorites IPC event handlers
|
||||
* Operations for managing user's favorite content
|
||||
*/
|
||||
|
||||
import { and, desc, eq, sql } from 'drizzle-orm';
|
||||
import { ipcMain } from 'electron';
|
||||
import { getDatabase } from '../../database/connection';
|
||||
import * as schema from '../../database/schema';
|
||||
|
||||
/**
|
||||
* Add content to favorites
|
||||
*/
|
||||
ipcMain.handle(
|
||||
'DB_ADD_FAVORITE',
|
||||
async (event, contentId: number, playlistId: string) => {
|
||||
try {
|
||||
const db = await getDatabase();
|
||||
await db.insert(schema.favorites).values({
|
||||
contentId,
|
||||
playlistId,
|
||||
});
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('Error adding favorite:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Remove content from favorites
|
||||
*/
|
||||
ipcMain.handle(
|
||||
'DB_REMOVE_FAVORITE',
|
||||
async (event, contentId: number, playlistId: string) => {
|
||||
try {
|
||||
const db = await getDatabase();
|
||||
await db
|
||||
.delete(schema.favorites)
|
||||
.where(
|
||||
and(
|
||||
eq(schema.favorites.contentId, contentId),
|
||||
eq(schema.favorites.playlistId, playlistId)
|
||||
)
|
||||
);
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('Error removing favorite:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Check if content is favorited
|
||||
*/
|
||||
ipcMain.handle(
|
||||
'DB_IS_FAVORITE',
|
||||
async (event, contentId: number, playlistId: string) => {
|
||||
try {
|
||||
const db = await getDatabase();
|
||||
const result = await db
|
||||
.select({ count: sql<number>`count(*)` })
|
||||
.from(schema.favorites)
|
||||
.where(
|
||||
and(
|
||||
eq(schema.favorites.contentId, contentId),
|
||||
eq(schema.favorites.playlistId, playlistId)
|
||||
)
|
||||
);
|
||||
return result[0].count > 0;
|
||||
} catch (error) {
|
||||
console.error('Error checking favorite:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Get all favorites for a playlist
|
||||
*/
|
||||
ipcMain.handle('DB_GET_FAVORITES', async (event, playlistId: string) => {
|
||||
try {
|
||||
const db = await getDatabase();
|
||||
const result = await db
|
||||
.select({
|
||||
id: schema.content.id,
|
||||
category_id: schema.content.categoryId,
|
||||
title: schema.content.title,
|
||||
rating: schema.content.rating,
|
||||
added: schema.content.added,
|
||||
poster_url: schema.content.posterUrl,
|
||||
xtream_id: schema.content.xtreamId,
|
||||
type: schema.content.type,
|
||||
added_at: schema.favorites.addedAt,
|
||||
})
|
||||
.from(schema.favorites)
|
||||
.innerJoin(
|
||||
schema.content,
|
||||
eq(schema.favorites.contentId, schema.content.id)
|
||||
)
|
||||
.where(eq(schema.favorites.playlistId, playlistId))
|
||||
.orderBy(desc(schema.favorites.addedAt));
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('Error getting favorites:', error);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,144 @@
|
||||
/**
|
||||
* Playlist IPC event handlers
|
||||
* CRUD operations for playlists
|
||||
*/
|
||||
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { ipcMain } from 'electron';
|
||||
import { getDatabase } from '../../database/connection';
|
||||
import * as schema from '../../database/schema';
|
||||
|
||||
/**
|
||||
* Create a new playlist
|
||||
*/
|
||||
ipcMain.handle(
|
||||
'DB_CREATE_PLAYLIST',
|
||||
async (
|
||||
event,
|
||||
playlist: {
|
||||
id: string;
|
||||
name: string;
|
||||
serverUrl?: string;
|
||||
username?: string;
|
||||
password?: string;
|
||||
type: string;
|
||||
}
|
||||
) => {
|
||||
try {
|
||||
const db = await getDatabase();
|
||||
await db.insert(schema.playlists).values({
|
||||
id: playlist.id,
|
||||
name: playlist.name,
|
||||
serverUrl: playlist.serverUrl,
|
||||
username: playlist.username,
|
||||
password: playlist.password,
|
||||
// enforce supported types
|
||||
type: playlist.type as
|
||||
| 'xtream'
|
||||
| 'stalker'
|
||||
| 'm3u-file'
|
||||
| 'm3u-text'
|
||||
| 'm3u-url',
|
||||
});
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('Error creating playlist:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Get playlist by ID
|
||||
*/
|
||||
ipcMain.handle('DB_GET_PLAYLIST', async (event, playlistId: string) => {
|
||||
try {
|
||||
const db = await getDatabase();
|
||||
const result = await db
|
||||
.select()
|
||||
.from(schema.playlists)
|
||||
.where(eq(schema.playlists.id, playlistId))
|
||||
.limit(1);
|
||||
return result[0] || null;
|
||||
} catch (error) {
|
||||
console.error('Error getting playlist:', error);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Update playlist
|
||||
*/
|
||||
ipcMain.handle(
|
||||
'DB_UPDATE_PLAYLIST',
|
||||
async (
|
||||
event,
|
||||
playlistId: string,
|
||||
updates: {
|
||||
name?: string;
|
||||
username?: string;
|
||||
password?: string;
|
||||
serverUrl?: string;
|
||||
lastUpdated?: string;
|
||||
}
|
||||
) => {
|
||||
try {
|
||||
const db = await getDatabase();
|
||||
await db
|
||||
.update(schema.playlists)
|
||||
.set(updates)
|
||||
.where(eq(schema.playlists.id, playlistId));
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('Error updating playlist:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Delete playlist and all related data
|
||||
*/
|
||||
ipcMain.handle('DB_DELETE_PLAYLIST', async (event, playlistId: string) => {
|
||||
try {
|
||||
const db = await getDatabase();
|
||||
|
||||
// Delete playlist (cascade will handle related data)
|
||||
await db
|
||||
.delete(schema.playlists)
|
||||
.where(eq(schema.playlists.id, playlistId));
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('Error deleting playlist:', error);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Delete all playlists and related data from SQLite
|
||||
*/
|
||||
ipcMain.handle('DB_DELETE_ALL_PLAYLISTS', async () => {
|
||||
try {
|
||||
const db = await getDatabase();
|
||||
|
||||
// Delete in order respecting foreign key constraints
|
||||
// First delete favorites and recently_viewed (they reference content)
|
||||
await db.delete(schema.favorites);
|
||||
await db.delete(schema.recentlyViewed);
|
||||
|
||||
// Then delete content (references categories)
|
||||
await db.delete(schema.content);
|
||||
|
||||
// Then delete categories (references playlists)
|
||||
await db.delete(schema.categories);
|
||||
|
||||
// Finally delete playlists
|
||||
await db.delete(schema.playlists);
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('Error deleting all playlists:', error);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,210 @@
|
||||
/**
|
||||
* Recently Viewed IPC event handlers
|
||||
* Operations for managing user's recently viewed content
|
||||
*/
|
||||
|
||||
import { and, desc, eq, inArray, sql } from 'drizzle-orm';
|
||||
import { ipcMain } from 'electron';
|
||||
import { getDatabase } from '../../database/connection';
|
||||
import * as schema from '../../database/schema';
|
||||
|
||||
/**
|
||||
* Get recently viewed items across all playlists
|
||||
*/
|
||||
ipcMain.handle('DB_GET_RECENTLY_VIEWED', async () => {
|
||||
try {
|
||||
const db = await getDatabase();
|
||||
const result = await db
|
||||
.select({
|
||||
id: schema.content.id,
|
||||
category_id: schema.content.categoryId,
|
||||
title: schema.content.title,
|
||||
rating: schema.content.rating,
|
||||
added: schema.content.added,
|
||||
poster_url: schema.content.posterUrl,
|
||||
xtream_id: schema.content.xtreamId,
|
||||
type: schema.content.type,
|
||||
playlist_id: schema.categories.playlistId,
|
||||
playlist_name: schema.playlists.name,
|
||||
viewed_at: schema.recentlyViewed.viewedAt,
|
||||
})
|
||||
.from(schema.recentlyViewed)
|
||||
.innerJoin(
|
||||
schema.content,
|
||||
eq(schema.recentlyViewed.contentId, schema.content.id)
|
||||
)
|
||||
.innerJoin(
|
||||
schema.categories,
|
||||
eq(schema.content.categoryId, schema.categories.id)
|
||||
)
|
||||
.innerJoin(
|
||||
schema.playlists,
|
||||
eq(schema.categories.playlistId, schema.playlists.id)
|
||||
)
|
||||
.orderBy(desc(schema.recentlyViewed.viewedAt))
|
||||
.limit(100);
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('Error getting recently viewed:', error);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Clear all recently viewed items
|
||||
*/
|
||||
ipcMain.handle('DB_CLEAR_RECENTLY_VIEWED', async () => {
|
||||
try {
|
||||
const db = await getDatabase();
|
||||
await db.delete(schema.recentlyViewed);
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('Error clearing recently viewed:', error);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Get recently viewed items for a specific playlist
|
||||
*/
|
||||
ipcMain.handle('DB_GET_RECENT_ITEMS', async (event, playlistId: string) => {
|
||||
try {
|
||||
const db = await getDatabase();
|
||||
const result = await db
|
||||
.select({
|
||||
id: schema.content.id,
|
||||
category_id: schema.content.categoryId,
|
||||
title: schema.content.title,
|
||||
rating: schema.content.rating,
|
||||
added: schema.content.added,
|
||||
poster_url: schema.content.posterUrl,
|
||||
xtream_id: schema.content.xtreamId,
|
||||
type: schema.content.type,
|
||||
viewed_at: schema.recentlyViewed.viewedAt,
|
||||
})
|
||||
.from(schema.recentlyViewed)
|
||||
.innerJoin(
|
||||
schema.content,
|
||||
eq(schema.recentlyViewed.contentId, schema.content.id)
|
||||
)
|
||||
.innerJoin(
|
||||
schema.categories,
|
||||
eq(schema.content.categoryId, schema.categories.id)
|
||||
)
|
||||
.where(eq(schema.categories.playlistId, playlistId))
|
||||
.orderBy(desc(schema.recentlyViewed.viewedAt))
|
||||
.limit(100);
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('Error getting recent items:', error);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Add item to recently viewed
|
||||
*/
|
||||
ipcMain.handle(
|
||||
'DB_ADD_RECENT_ITEM',
|
||||
async (event, contentId: number, playlistId: string) => {
|
||||
try {
|
||||
const db = await getDatabase();
|
||||
|
||||
// Check if already exists
|
||||
const existing = await db
|
||||
.select()
|
||||
.from(schema.recentlyViewed)
|
||||
.where(
|
||||
and(
|
||||
eq(schema.recentlyViewed.contentId, contentId),
|
||||
eq(schema.recentlyViewed.playlistId, playlistId)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
if (existing.length > 0) {
|
||||
// Update viewed_at timestamp
|
||||
await db
|
||||
.update(schema.recentlyViewed)
|
||||
.set({ viewedAt: sql`CURRENT_TIMESTAMP` })
|
||||
.where(
|
||||
and(
|
||||
eq(schema.recentlyViewed.contentId, contentId),
|
||||
eq(schema.recentlyViewed.playlistId, playlistId)
|
||||
)
|
||||
);
|
||||
} else {
|
||||
// Insert new entry
|
||||
await db.insert(schema.recentlyViewed).values({
|
||||
contentId,
|
||||
playlistId,
|
||||
});
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('Error adding recent item:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Clear recently viewed for a specific playlist
|
||||
*/
|
||||
ipcMain.handle(
|
||||
'DB_CLEAR_PLAYLIST_RECENT_ITEMS',
|
||||
async (event, playlistId: string) => {
|
||||
try {
|
||||
const db = await getDatabase();
|
||||
|
||||
// Get content IDs that belong to this playlist
|
||||
const contentIds = await db
|
||||
.select({ id: schema.content.id })
|
||||
.from(schema.content)
|
||||
.innerJoin(
|
||||
schema.categories,
|
||||
eq(schema.content.categoryId, schema.categories.id)
|
||||
)
|
||||
.where(eq(schema.categories.playlistId, playlistId));
|
||||
|
||||
if (contentIds.length > 0) {
|
||||
await db.delete(schema.recentlyViewed).where(
|
||||
inArray(
|
||||
schema.recentlyViewed.contentId,
|
||||
contentIds.map((c) => c.id)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('Error clearing playlist recent items:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Remove specific item from recently viewed
|
||||
*/
|
||||
ipcMain.handle(
|
||||
'DB_REMOVE_RECENT_ITEM',
|
||||
async (event, contentId: number, playlistId: string) => {
|
||||
try {
|
||||
const db = await getDatabase();
|
||||
await db
|
||||
.delete(schema.recentlyViewed)
|
||||
.where(
|
||||
and(
|
||||
eq(schema.recentlyViewed.contentId, contentId),
|
||||
eq(schema.recentlyViewed.playlistId, playlistId)
|
||||
)
|
||||
);
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('Error removing recent item:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -0,0 +1,198 @@
|
||||
/**
|
||||
* Xtream-specific IPC event handlers
|
||||
* Operations for refreshing and managing Xtream playlist data
|
||||
*/
|
||||
|
||||
import { and, eq, inArray } from 'drizzle-orm';
|
||||
import { ipcMain } from 'electron';
|
||||
import { getDatabase } from '../../database/connection';
|
||||
import * as schema from '../../database/schema';
|
||||
|
||||
/**
|
||||
* Delete all content and categories for an Xtream playlist
|
||||
* Keeps the playlist entry but removes all imported data
|
||||
*/
|
||||
ipcMain.handle(
|
||||
'DB_DELETE_XTREAM_CONTENT',
|
||||
async (event, playlistId: string) => {
|
||||
try {
|
||||
const db = await getDatabase();
|
||||
|
||||
// First, get all category IDs for this playlist
|
||||
const categories = await db
|
||||
.select({ id: schema.categories.id })
|
||||
.from(schema.categories)
|
||||
.where(eq(schema.categories.playlistId, playlistId));
|
||||
|
||||
const categoryIds = categories.map((c) => c.id);
|
||||
|
||||
// Before deleting content, save xtreamIds of favorited and recently viewed items
|
||||
// so they can be restored after refresh
|
||||
let favoritedXtreamIds: number[] = [];
|
||||
let recentlyViewedXtreamIds: {
|
||||
xtreamId: number;
|
||||
viewedAt: string;
|
||||
}[] = [];
|
||||
|
||||
if (categoryIds.length > 0) {
|
||||
// Get favorites with their xtreamIds
|
||||
const favorites = await db
|
||||
.select({
|
||||
xtreamId: schema.content.xtreamId,
|
||||
})
|
||||
.from(schema.favorites)
|
||||
.innerJoin(
|
||||
schema.content,
|
||||
eq(schema.favorites.contentId, schema.content.id)
|
||||
)
|
||||
.where(
|
||||
and(
|
||||
eq(schema.favorites.playlistId, playlistId),
|
||||
inArray(schema.content.categoryId, categoryIds)
|
||||
)
|
||||
);
|
||||
|
||||
favoritedXtreamIds = favorites.map((f) => f.xtreamId);
|
||||
|
||||
// Get recently viewed with their xtreamIds and timestamps
|
||||
const recentlyViewed = await db
|
||||
.select({
|
||||
xtreamId: schema.content.xtreamId,
|
||||
viewedAt: schema.recentlyViewed.viewedAt,
|
||||
})
|
||||
.from(schema.recentlyViewed)
|
||||
.innerJoin(
|
||||
schema.content,
|
||||
eq(schema.recentlyViewed.contentId, schema.content.id)
|
||||
)
|
||||
.where(
|
||||
and(
|
||||
eq(schema.recentlyViewed.playlistId, playlistId),
|
||||
inArray(schema.content.categoryId, categoryIds)
|
||||
)
|
||||
);
|
||||
|
||||
recentlyViewedXtreamIds = recentlyViewed.map((rv) => ({
|
||||
xtreamId: rv.xtreamId,
|
||||
viewedAt: rv.viewedAt || new Date().toISOString(),
|
||||
}));
|
||||
|
||||
// Delete all content for these categories
|
||||
// (This will cascade delete favorites and recently viewed)
|
||||
await db
|
||||
.delete(schema.content)
|
||||
.where(inArray(schema.content.categoryId, categoryIds));
|
||||
}
|
||||
|
||||
// Delete all categories for this playlist
|
||||
await db
|
||||
.delete(schema.categories)
|
||||
.where(eq(schema.categories.playlistId, playlistId));
|
||||
|
||||
// Return the saved xtreamIds so favorites and recently viewed can be restored
|
||||
return {
|
||||
success: true,
|
||||
favoritedXtreamIds,
|
||||
recentlyViewedXtreamIds,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error deleting Xtream content:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Restore favorites and recently viewed items after Xtream refresh
|
||||
* Matches content by xtreamId and re-creates favorites/recently viewed entries
|
||||
*/
|
||||
ipcMain.handle(
|
||||
'DB_RESTORE_XTREAM_USER_DATA',
|
||||
async (
|
||||
event,
|
||||
playlistId: string,
|
||||
favoritedXtreamIds: number[],
|
||||
recentlyViewedXtreamIds: { xtreamId: number; viewedAt: string }[]
|
||||
) => {
|
||||
try {
|
||||
const db = await getDatabase();
|
||||
|
||||
// Restore favorites
|
||||
if (favoritedXtreamIds.length > 0) {
|
||||
// Find content IDs for the xtreamIds
|
||||
const content = await db
|
||||
.select({
|
||||
id: schema.content.id,
|
||||
xtreamId: schema.content.xtreamId,
|
||||
})
|
||||
.from(schema.content)
|
||||
.innerJoin(
|
||||
schema.categories,
|
||||
eq(schema.content.categoryId, schema.categories.id)
|
||||
)
|
||||
.where(
|
||||
and(
|
||||
eq(schema.categories.playlistId, playlistId),
|
||||
inArray(schema.content.xtreamId, favoritedXtreamIds)
|
||||
)
|
||||
);
|
||||
|
||||
// Re-create favorite entries
|
||||
for (const item of content) {
|
||||
await db.insert(schema.favorites).values({
|
||||
contentId: item.id,
|
||||
playlistId: playlistId,
|
||||
addedAt: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Restore recently viewed
|
||||
if (recentlyViewedXtreamIds.length > 0) {
|
||||
const xtreamIds = recentlyViewedXtreamIds.map(
|
||||
(rv) => rv.xtreamId
|
||||
);
|
||||
|
||||
// Find content IDs for the xtreamIds
|
||||
const content = await db
|
||||
.select({
|
||||
id: schema.content.id,
|
||||
xtreamId: schema.content.xtreamId,
|
||||
})
|
||||
.from(schema.content)
|
||||
.innerJoin(
|
||||
schema.categories,
|
||||
eq(schema.content.categoryId, schema.categories.id)
|
||||
)
|
||||
.where(
|
||||
and(
|
||||
eq(schema.categories.playlistId, playlistId),
|
||||
inArray(schema.content.xtreamId, xtreamIds)
|
||||
)
|
||||
);
|
||||
|
||||
// Create a map of xtreamId -> contentId
|
||||
const xtreamIdToContentId = new Map(
|
||||
content.map((item) => [item.xtreamId, item.id])
|
||||
);
|
||||
|
||||
// Re-create recently viewed entries with original timestamps
|
||||
for (const item of recentlyViewedXtreamIds) {
|
||||
const contentId = xtreamIdToContentId.get(item.xtreamId);
|
||||
if (contentId) {
|
||||
await db.insert(schema.recentlyViewed).values({
|
||||
contentId: contentId,
|
||||
playlistId: playlistId,
|
||||
viewedAt: item.viewedAt,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('Error restoring Xtream user data:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
);
|
||||
Reference in new issue
Block a user