feat(xtream): preserve user Xtream data during refresh

Save and restore favorited and recently viewed Xtream items when
deleting and re-importing playlist content.

- Return favoritedXtreamIds and recentlyViewedXtreamIds from
  deleteXtreamPlaylistContent so callers can capture user data before
 .
- Add restoreXtreamUserData to the database service to allow restoring
  favorites and recently viewed entries after a refresh.
- Query and collect xtreamIds and timestamps for favorites and recently
  viewed items in the backend before deleting content.
- Persist the captured user data to localStorage in the recent playlists
  component so it can be used to restore state after re-importing the
  playlist.
- Keep existing playlist metadata update flow intact.

This prevents loss of user-specific favorites and recent history when
refreshing Xtream playlists.
This commit is contained in:
4gray committed 2025-11-13 19:26:37 +01:00
1 parent 67f54a94c7
commit 730200f704
7 files changed
+207 -21

No files matched your search

@@ -80,6 +80,17 @@ contextBridge.exposeInMainWorld('electron', {
ipcRenderer.invoke('DB_DELETE_PLAYLIST', playlistId),
dbDeleteXtreamContent: (playlistId: string) =>
ipcRenderer.invoke('DB_DELETE_XTREAM_CONTENT', playlistId),
dbRestoreXtreamUserData: (
playlistId: string,
favoritedXtreamIds: number[],
recentlyViewedXtreamIds: { xtreamId: number; viewedAt: string }[]
) =>
ipcRenderer.invoke(
'DB_RESTORE_XTREAM_USER_DATA',
playlistId,
favoritedXtreamIds,
recentlyViewedXtreamIds
),
dbHasCategories: (playlistId: string, type: string) =>
ipcRenderer.invoke('DB_HAS_CATEGORIES', playlistId, type),
dbGetCategories: (playlistId: string, type: string) =>
@@ -139,39 +139,149 @@ ipcMain.handle(
const categoryIds = categories.map((c) => c.id);
console.log(
`Deleting playlist content for ${playlistId}: ${categoryIds.length} categories found`
);
// 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));
console.log(
`Deleted content for ${categoryIds.length} categories`
);
}
// Delete all categories for this playlist
// (Could rely on cascade delete, but being explicit is clearer)
await db
.delete(schema.categories)
.where(eq(schema.categories.playlistId, playlistId));
console.log(
`Deleted ${categoryIds.length} categories for playlist ${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;
}
}
);
// NOTE: Do NOT delete user favorites or recently viewed items here.
// When refreshing an Xtream playlist we want to remove only the
// imported categories and content so user-specific data (favorites,
// recently viewed) is preserved.
/**
* 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 deleting Xtream content:', error);
console.error('Error restoring Xtream user data:', error);
throw error;
}
}
@@ -620,6 +620,29 @@ export const XtreamStore = signalStore(
),
]);
console.log('Content initialization completed');
// Check if we need to restore favorites and recently viewed after refresh
const playlistId = route.snapshot.params.id;
if (playlistId) {
const restoreKey = `xtream-restore-${playlistId}`;
const restoreData = localStorage.getItem(restoreKey);
if (restoreData) {
try {
const {
favoritedXtreamIds,
recentlyViewedXtreamIds,
} = JSON.parse(restoreData);
await dbService.restoreXtreamUserData(
playlistId,
favoritedXtreamIds,
recentlyViewedXtreamIds
);
localStorage.removeItem(restoreKey);
} catch (err) {
console.error('Error restoring user data:', err);
}
}
}
} catch (error) {
console.error(
'Error during content initialization:',
Vendored
+9
View File
@@ -81,6 +81,15 @@ declare global {
) => Promise<{ success: boolean }>;
dbDeleteXtreamContent: (
playlistId: string
) => Promise<{
success: boolean;
favoritedXtreamIds: number[];
recentlyViewedXtreamIds: { xtreamId: number; viewedAt: string }[];
}>;
dbRestoreXtreamUserData: (
playlistId: string,
favoritedXtreamIds: number[],
recentlyViewedXtreamIds: { xtreamId: number; viewedAt: string }[]
) => Promise<{ success: boolean }>;
dbHasCategories: (
playlistId: string,
@@ -67,9 +67,29 @@ export class DatabaseService {
/**
* Delete all content and categories for an Xtream playlist (for refresh)
* Keeps the playlist entry but removes all imported data
* Returns saved favorites and recently viewed xtreamIds for restoration
*/
async deleteXtreamPlaylistContent(playlistId: string): Promise<void> {
await window.electron.dbDeleteXtreamContent(playlistId);
async deleteXtreamPlaylistContent(playlistId: string): Promise<{
success: boolean;
favoritedXtreamIds: number[];
recentlyViewedXtreamIds: { xtreamId: number; viewedAt: string }[];
}> {
return await window.electron.dbDeleteXtreamContent(playlistId);
}
/**
* Restore favorites and recently viewed items after Xtream refresh
*/
async restoreXtreamUserData(
playlistId: string,
favoritedXtreamIds: number[],
recentlyViewedXtreamIds: { xtreamId: number; viewedAt: string }[]
): Promise<void> {
await window.electron.dbRestoreXtreamUserData(
playlistId,
favoritedXtreamIds,
recentlyViewedXtreamIds
);
}
/**
+1
View File
@@ -7,6 +7,7 @@
"inlineSources": true,
"types": []
},
"files": ["../../global.d.ts"],
"exclude": [
"src/**/*.spec.ts",
"src/test-setup.ts",
@@ -223,9 +223,11 @@ export class RecentPlaylistsComponent {
onConfirm: async () => {
try {
// Delete all content and categories for this playlist
await this.databaseService.deleteXtreamPlaylistContent(
item._id
);
// This returns the xtreamIds of favorites and recently viewed items
const { favoritedXtreamIds, recentlyViewedXtreamIds } =
await this.databaseService.deleteXtreamPlaylistContent(
item._id
);
const updateDate = Date.now();
@@ -242,6 +244,16 @@ export class RecentPlaylistsComponent {
})
);
// Store the user data in localStorage to restore after re-import
const restoreKey = `xtream-restore-${item._id}`;
localStorage.setItem(
restoreKey,
JSON.stringify({
favoritedXtreamIds,
recentlyViewedXtreamIds,
})
);
this.snackBar.open(
this.translate.instant(
'HOME.PLAYLISTS.REFRESH_XTREAM_DIALOG.STARTED'