diff --git a/apps/electron-backend/src/app/api/main.preload.ts b/apps/electron-backend/src/app/api/main.preload.ts
index d3404676e..de1012256 100644
--- a/apps/electron-backend/src/app/api/main.preload.ts
+++ b/apps/electron-backend/src/app/api/main.preload.ts
@@ -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) =>
diff --git a/apps/electron-backend/src/app/events/database.events.ts b/apps/electron-backend/src/app/events/database.events.ts
index adfe89c01..0e19f1548 100644
--- a/apps/electron-backend/src/app/events/database.events.ts
+++ b/apps/electron-backend/src/app/events/database.events.ts
@@ -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;
}
}
diff --git a/apps/web/src/app/xtream-tauri/xtream.store.ts b/apps/web/src/app/xtream-tauri/xtream.store.ts
index 2415af60c..5d0668a81 100644
--- a/apps/web/src/app/xtream-tauri/xtream.store.ts
+++ b/apps/web/src/app/xtream-tauri/xtream.store.ts
@@ -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:',
diff --git a/apps/web/src/app/xtream/category-view/category-view.component.ts b/apps/web/src/app/xtream/category-view/category-view.component.ts
index 8c308a56e..579e5c993 100644
--- a/apps/web/src/app/xtream/category-view/category-view.component.ts
+++ b/apps/web/src/app/xtream/category-view/category-view.component.ts
@@ -2,7 +2,7 @@ import { Component, inject, input, output } from '@angular/core';
import { MatCardModule } from '@angular/material/card';
import { MatIconModule } from '@angular/material/icon';
import { FilterPipe } from '@iptvnator/pipes';
-import { TranslateModule } from '@ngx-translate/core';
+import { TranslatePipe } from '@ngx-translate/core';
import { XtreamCategory } from 'shared-interfaces';
import { PlaylistErrorViewComponent } from '../playlist-error-view/playlist-error-view.component';
import { PortalStore } from '../portal.store';
@@ -10,36 +10,46 @@ import { PortalStore } from '../portal.store';
@Component({
selector: 'app-category-view',
template: `
- @if (items?.length > 0) { @for ( item of items() | filterBy:
- searchPhrase() : 'category_name'; track $index ) {
-
-
- {{ item.category_name || item.name || 'No category name' }}
-
-
- } @if ( !(items() | filterBy: searchPhrase() : 'category_name')?.length
- ) {
-
- } } @else {
-
+ @if (items().length > 0) {
+ @for (
+ item of items() | filterBy: searchPhrase() : 'category_name';
+ track $index
+ ) {
+
+
+ {{
+ item.category_name ||
+ item.name ||
+ 'No category name'
+ }}
+
+
+ }
+ @if (
+ !(items() | filterBy: searchPhrase() : 'category_name')?.length
+ ) {
+
+ }
+ } @else {
+
}
`,
styleUrl: './category-view.component.scss',
@@ -48,7 +58,7 @@ import { PortalStore } from '../portal.store';
MatCardModule,
MatIconModule,
PlaylistErrorViewComponent,
- TranslateModule,
+ TranslatePipe,
],
})
export class CategoryViewComponent {
diff --git a/global.d.ts b/global.d.ts
index d06fe055a..bacf1bde3 100644
--- a/global.d.ts
+++ b/global.d.ts
@@ -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,
diff --git a/libs/services/src/lib/database-electron.service.ts b/libs/services/src/lib/database-electron.service.ts
index a35591a8d..8e6907bb8 100644
--- a/libs/services/src/lib/database-electron.service.ts
+++ b/libs/services/src/lib/database-electron.service.ts
@@ -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 {
- 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 {
+ await window.electron.dbRestoreXtreamUserData(
+ playlistId,
+ favoritedXtreamIds,
+ recentlyViewedXtreamIds
+ );
}
/**
diff --git a/libs/services/tsconfig.lib.json b/libs/services/tsconfig.lib.json
index b11464cac..61d8f1f15 100644
--- a/libs/services/tsconfig.lib.json
+++ b/libs/services/tsconfig.lib.json
@@ -7,6 +7,7 @@
"inlineSources": true,
"types": []
},
+ "files": ["../../global.d.ts"],
"exclude": [
"src/**/*.spec.ts",
"src/test-setup.ts",
diff --git a/libs/ui/components/src/lib/recent-playlists/recent-playlists.component.ts b/libs/ui/components/src/lib/recent-playlists/recent-playlists.component.ts
index 77449dec7..e7493eb6d 100644
--- a/libs/ui/components/src/lib/recent-playlists/recent-playlists.component.ts
+++ b/libs/ui/components/src/lib/recent-playlists/recent-playlists.component.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'