mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
Merge pull request #649 from 4gray/feat/xtream-preserve-user-data
feat/xtream-preserve-user-data
This commit is contained in:
8 files changed
+249
-53
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:',
|
||||
|
||||
@@ -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 ) {
|
||||
<mat-card
|
||||
appearance="outlined"
|
||||
class="category-item"
|
||||
(click)="categoryClicked.emit(item)"
|
||||
>
|
||||
<mat-card-content>
|
||||
{{ item.category_name || item.name || 'No category name' }}
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
} @if ( !(items() | filterBy: searchPhrase() : 'category_name')?.length
|
||||
) {
|
||||
<app-playlist-error-view
|
||||
title="No results"
|
||||
[description]="
|
||||
'PORTALS.EMPTY_LIST_VIEW.NO_SEARCH_RESULTS' | translate
|
||||
"
|
||||
[showActionButtons]="false"
|
||||
[viewType]="'NO_SEARCH_RESULTS'"
|
||||
/>
|
||||
} } @else {
|
||||
<app-playlist-error-view
|
||||
[title]="'PORTALS.ERROR_VIEW.EMPTY_CATEGORY.TITLE' | translate"
|
||||
[description]="
|
||||
'PORTALS.ERROR_VIEW.EMPTY_CATEGORY.DESCRIPTION' | translate
|
||||
"
|
||||
[showActionButtons]="false"
|
||||
[viewType]="'EMPTY_CATEGORY'"
|
||||
/>
|
||||
@if (items().length > 0) {
|
||||
@for (
|
||||
item of items() | filterBy: searchPhrase() : 'category_name';
|
||||
track $index
|
||||
) {
|
||||
<mat-card
|
||||
appearance="outlined"
|
||||
class="category-item"
|
||||
(click)="categoryClicked.emit(item)"
|
||||
>
|
||||
<mat-card-content>
|
||||
{{
|
||||
item.category_name ||
|
||||
item.name ||
|
||||
'No category name'
|
||||
}}
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
}
|
||||
@if (
|
||||
!(items() | filterBy: searchPhrase() : 'category_name')?.length
|
||||
) {
|
||||
<app-playlist-error-view
|
||||
title="No results"
|
||||
[description]="
|
||||
'PORTALS.EMPTY_LIST_VIEW.NO_SEARCH_RESULTS' | translate
|
||||
"
|
||||
[showActionButtons]="false"
|
||||
[viewType]="'NO_SEARCH_RESULTS'"
|
||||
/>
|
||||
}
|
||||
} @else {
|
||||
<app-playlist-error-view
|
||||
[title]="'PORTALS.ERROR_VIEW.EMPTY_CATEGORY.TITLE' | translate"
|
||||
[description]="
|
||||
'PORTALS.ERROR_VIEW.EMPTY_CATEGORY.DESCRIPTION' | translate
|
||||
"
|
||||
[showActionButtons]="false"
|
||||
[viewType]="'EMPTY_CATEGORY'"
|
||||
/>
|
||||
}
|
||||
`,
|
||||
styleUrl: './category-view.component.scss',
|
||||
@@ -48,7 +58,7 @@ import { PortalStore } from '../portal.store';
|
||||
MatCardModule,
|
||||
MatIconModule,
|
||||
PlaylistErrorViewComponent,
|
||||
TranslateModule,
|
||||
TranslatePipe,
|
||||
],
|
||||
})
|
||||
export class CategoryViewComponent {
|
||||
|
||||
Vendored
+9
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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'
|
||||
|
||||
Reference in new issue
Block a user