Merge pull request #702 from 4gray/refactor/restructure-database-events

refactor(electron-backend): split DB event handlers into modules
This commit is contained in:
4gray authored and GitHub committed 2025-12-26 01:02:24 +01:00
commit fce4d8798e
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;
}
}
);