feat(xtream): Xtream playlist refresh workflow

a dedicated refresh button and workflow for Xtream in the
recent playlists UI. Show a refresh icon only when the playlist has a
serverUrl. For Xtream playlists, prompt the user with a confirmation
dialog, delete locally cached categories and streams, show a toast that
the refresh started, and navigate to the playlist to trigger re-import.
Keep existing refresh IPC logic for non-Xtream (M3U/local) playlists.

Also add English translations for the Xtream refresh tooltip and dialog
messages, and surface errors via snackbar on failure.

This enables safe re-importing of remote Xtream playlists while preserving
favorites and recent items.
This commit is contained in:
4gray committed 2025-10-18 21:55:48 +02:00
1 parent 0a934d2ac6
commit 461e2e2e7e
8 files changed
+160 -60

No files matched your search

@@ -85,6 +85,7 @@ ipcMain.handle(
username?: string;
password?: string;
serverUrl?: string;
lastUpdated?: string;
}
) => {
try {
@@ -3,7 +3,6 @@ import {
AfterViewInit,
Component,
effect,
ElementRef,
inject,
Inject,
Optional,
@@ -55,7 +54,7 @@ interface SearchResultsData {
styleUrls: ['./search-results.component.scss'],
})
export class SearchResultsComponent implements AfterViewInit {
@ViewChild('searchInput') searchInput!: ElementRef;
@ViewChild(SearchFormComponent) searchFormComponent!: SearchFormComponent;
readonly xtreamStore = inject(XtreamStore);
readonly router = inject(Router);
readonly activatedRoute = inject(ActivatedRoute);
@@ -105,7 +104,7 @@ export class SearchResultsComponent implements AfterViewInit {
ngAfterViewInit() {
this.xtreamStore.setSelectedContentType(undefined);
setTimeout(() => {
this.searchInput.nativeElement.focus();
this.searchFormComponent?.focusSearchInput();
});
}
+50 -44
View File
@@ -568,51 +568,57 @@ export const XtreamStore = signalStore(
try {
console.log('Starting content initialization...');
console.log('Fetching live categories...');
await lastValueFrom(
fetchCategories(
XtreamCodeActions.GetLiveCategories,
'liveCategories',
'live'
)
);
await lastValueFrom(
fetchCategories(
XtreamCodeActions.GetVodCategories,
'vodCategories',
'movies'
)
);
await lastValueFrom(
fetchCategories(
XtreamCodeActions.GetSeriesCategories,
'serialCategories',
'series'
)
);
// Fetch all categories in parallel
console.log('Fetching categories...');
await Promise.all([
lastValueFrom(
fetchCategories(
XtreamCodeActions.GetLiveCategories,
'liveCategories',
'live'
)
),
lastValueFrom(
fetchCategories(
XtreamCodeActions.GetVodCategories,
'vodCategories',
'movies'
)
),
lastValueFrom(
fetchCategories(
XtreamCodeActions.GetSeriesCategories,
'serialCategories',
'series'
)
),
]);
// Fetch all content in parallel
console.log('Fetching content...');
await lastValueFrom(
fetchStreams(
XtreamCodeActions.GetLiveStreams,
'liveStreams',
'live'
)
);
await lastValueFrom(
fetchStreams(
XtreamCodeActions.GetVodStreams,
'vodStreams',
'movie'
)
);
await lastValueFrom(
fetchStreams(
XtreamCodeActions.GetSeries,
'serialStreams',
'series'
)
);
await Promise.all([
lastValueFrom(
fetchStreams(
XtreamCodeActions.GetLiveStreams,
'liveStreams',
'live'
)
),
lastValueFrom(
fetchStreams(
XtreamCodeActions.GetVodStreams,
'vodStreams',
'movie'
)
),
lastValueFrom(
fetchStreams(
XtreamCodeActions.GetSeries,
'serialStreams',
'series'
)
),
]);
console.log('Content initialization completed');
} catch (error) {
console.error(
@@ -627,7 +633,7 @@ export const XtreamStore = signalStore(
const checkPortalStatus = rxMethod<void>(
pipe(
combineLatestWith(oldStore.select(selectActivePlaylist)),
switchMap(([_, playlist]) => {
switchMap(([, playlist]) => {
if (!playlist) return EMPTY;
return from(
+7
View File
@@ -54,6 +54,13 @@
},
"UPDATED": "Updated",
"REFRESH": "Refresh playlist",
"REFRESH_XTREAM": "Refresh Xtream playlist from remote",
"REFRESH_XTREAM_DIALOG": {
"TITLE": "Refresh Xtream Playlist",
"MESSAGE": "This will delete locally cached categories and streams and re-import everything from the remote server. Your favorites and recently viewed items will be preserved. Are you sure you want to continue?",
"STARTED": "Refresh started. Navigating to playlist...",
"ERROR": "Failed to refresh playlist. Please try again."
},
"GLOBAL_FAVORITES": "Global favorites",
"GLOBAL_FAVORITES_DESCRIPTION": "Auto-generated playlist with aggregated favorites from all playlists",
"MY_PLAYLISTS": "My playlists",
@@ -92,18 +92,24 @@ export class DatabaseService {
*/
async updateXtreamPlaylistDetails(playlist: {
id: string;
title: string;
title?: string;
username?: string;
password?: string;
serverUrl?: string;
updateDate?: number;
}): Promise<boolean> {
try {
await window.electron.dbUpdatePlaylist(playlist.id, {
name: playlist.title,
username: playlist.username,
password: playlist.password,
serverUrl: playlist.serverUrl,
});
const updates: any = {};
if (playlist.title) updates.name = playlist.title;
if (playlist.username) updates.username = playlist.username;
if (playlist.password) updates.password = playlist.password;
if (playlist.serverUrl) updates.serverUrl = playlist.serverUrl;
if (playlist.updateDate !== undefined)
updates.lastUpdated = new Date(
playlist.updateDate
).toISOString();
await window.electron.dbUpdatePlaylist(playlist.id, updates);
return true;
} catch (error) {
console.error('Error updating playlist details:', error);
@@ -97,6 +97,9 @@ export class PlaylistsService {
...(updatedPlaylist.password !== null
? { password: updatedPlaylist.password }
: {}),
...(updatedPlaylist.updateDate !== undefined
? { updateDate: updatedPlaylist.updateDate }
: {}),
})
)
);
@@ -69,8 +69,12 @@
<ng-container *ngIf="item.importDate?.length > 3">
| {{ 'HOME.PLAYLISTS.ADDED' | translate }}:
{{ item.importDate | date }}
</ng-container></ng-template
>
</ng-container>
<ng-container *ngIf="item.updateDate">
| {{ 'HOME.PLAYLISTS.UPDATED' | translate }}:
{{ item.updateDate | date: 'MMMM d, yyyy, HH:mm' }}
</ng-container>
</ng-template>
</div>
<span matListItemMeta class="action-buttons" *ngIf="showActions">
@@ -84,6 +88,16 @@
>
<mat-icon>sync</mat-icon>
</button>
<button
*ngIf="item.serverUrl"
mat-icon-button
class="refresh-btn"
color="accent"
(click)="$event.stopPropagation(); refreshClicked.emit(item)"
[matTooltip]="'HOME.PLAYLISTS.REFRESH_XTREAM' | translate"
>
<mat-icon>sync</mat-icon>
</button>
<button
mat-icon-button
color="accent"
@@ -249,10 +249,74 @@ export class RecentPlaylistsComponent implements OnInit {
* @param item playlist to update
*/
refreshPlaylist(item: PlaylistMeta) {
this.dataService.sendIpcEvent(PLAYLIST_UPDATE, {
id: item._id,
title: item.title,
...(item.url ? { url: item.url } : { filePath: item.filePath }),
if (item.serverUrl) {
// For Xtream playlists, delete and re-import
this.refreshXtreamPlaylist(item);
} else {
// For M3U playlists, use existing refresh logic
this.dataService.sendIpcEvent(PLAYLIST_UPDATE, {
id: item._id,
title: item.title,
...(item.url ? { url: item.url } : { filePath: item.filePath }),
});
}
}
/**
* Refresh Xtream playlist by deleting all data and re-importing from remote
* @param item Xtream playlist to refresh
*/
async refreshXtreamPlaylist(item: PlaylistMeta) {
this.dialogService.openConfirmDialog({
title: this.translate.instant(
'HOME.PLAYLISTS.REFRESH_XTREAM_DIALOG.TITLE'
),
message: this.translate.instant(
'HOME.PLAYLISTS.REFRESH_XTREAM_DIALOG.MESSAGE'
),
onConfirm: async () => {
try {
// Delete all content and categories for this playlist
await this.databaseService.deleteXtreamPlaylistContent(
item._id
);
const updateDate = Date.now();
// Update the updateDate timestamp in database (Electron)
await this.databaseService.updateXtreamPlaylistDetails({
id: item._id,
updateDate,
});
// Update the updateDate timestamp in IndexedDB
this.store.dispatch(
PlaylistActions.updatePlaylistMeta({
playlist: { ...item, updateDate },
})
);
this.snackBar.open(
this.translate.instant(
'HOME.PLAYLISTS.REFRESH_XTREAM_DIALOG.STARTED'
),
undefined,
{ duration: 2000 }
);
// Navigate to the playlist to trigger re-import
this.router.navigate(['xtreams', item._id]);
} catch (error) {
console.error('Error refreshing Xtream playlist:', error);
this.snackBar.open(
this.translate.instant(
'HOME.PLAYLISTS.REFRESH_XTREAM_DIALOG.ERROR'
),
undefined,
{ duration: 3000 }
);
}
},
});
}