feat: enhance playlist backup export functionality and UI feedback

Entire-Checkpoint: c6e522b4276c
This commit is contained in:
4gray committed 2026-04-21 08:54:03 +02:00
1 parent 1c1a53743a
commit c8c93798f9
26 files changed
+2254 -375

No files matched your search

@@ -5,7 +5,11 @@ import { Router } from '@angular/router';
import { Store } from '@ngrx/store';
import { TranslateService } from '@ngx-translate/core';
import { DialogService } from 'components';
import { DatabaseService, PlaylistRefreshService } from 'services';
import {
DatabaseService,
PlaybackPositionService,
PlaylistRefreshService,
} from 'services';
import { ChannelActions, PlaylistActions } from 'm3u-state';
import { Playlist, PlaylistMeta } from 'shared-interfaces';
import { PlaylistContextFacade } from './playlist-context.facade';
@@ -46,7 +50,12 @@ describe('PlaylistRefreshActionService', () => {
let playlistRefreshService: {
refreshPlaylist: jest.Mock;
};
let routeProvider: ReturnType<typeof signal<'playlists' | 'xtreams' | null>>;
let playbackPositionService: {
getAllPlaybackPositions: jest.Mock;
};
let routeProvider: ReturnType<
typeof signal<'playlists' | 'xtreams' | null>
>;
let resolvedPlaylistId: ReturnType<typeof signal<string | null>>;
beforeEach(() => {
@@ -56,14 +65,24 @@ describe('PlaylistRefreshActionService', () => {
createOperationId: jest.fn((prefix: string) => `${prefix}-op`),
deleteXtreamPlaylistContent: jest.fn().mockResolvedValue({
success: true,
favoritedXtreamIds: [101, 202],
recentlyViewedXtreamIds: [
favorites: [
{
xtreamId: 101,
contentType: 'live',
},
{
xtreamId: 202,
contentType: 'movie',
},
],
recentlyViewed: [
{
xtreamId: 303,
contentType: 'series',
viewedAt: '2026-04-04T08:00:00.000Z',
},
],
hiddenCategories: [{ xtreamId: 404, type: 'live' }],
hiddenCategories: [{ xtreamId: 404, categoryType: 'live' }],
}),
updateXtreamPlaylistDetails: jest.fn().mockResolvedValue(true),
};
@@ -82,6 +101,9 @@ describe('PlaylistRefreshActionService', () => {
playlistRefreshService = {
refreshPlaylist: jest.fn(),
};
playbackPositionService = {
getAllPlaybackPositions: jest.fn().mockResolvedValue([]),
};
routeProvider = signal<'playlists' | 'xtreams' | null>('xtreams');
resolvedPlaylistId = signal<string | null>(null);
@@ -118,6 +140,10 @@ describe('PlaylistRefreshActionService', () => {
provide: PlaylistRefreshService,
useValue: playlistRefreshService,
},
{
provide: PlaybackPositionService,
useValue: playbackPositionService,
},
{
provide: PlaylistContextFacade,
useValue: {
@@ -171,14 +197,25 @@ describe('PlaylistRefreshActionService', () => {
expect(setItemSpy).toHaveBeenCalledWith(
`xtream-restore-${item._id}`,
JSON.stringify({
favoritedXtreamIds: [101, 202],
recentlyViewedXtreamIds: [
hiddenCategories: [{ xtreamId: 404, categoryType: 'live' }],
favorites: [
{
xtreamId: 101,
contentType: 'live',
},
{
xtreamId: 202,
contentType: 'movie',
},
],
recentlyViewed: [
{
xtreamId: 303,
contentType: 'series',
viewedAt: '2026-04-04T08:00:00.000Z',
},
],
hiddenCategories: [{ xtreamId: 404, type: 'live' }],
playbackPositions: [],
})
);
expect(store.dispatch).toHaveBeenCalledWith(
@@ -212,7 +249,9 @@ describe('PlaylistRefreshActionService', () => {
} as Playlist;
routeProvider.set('playlists');
resolvedPlaylistId.set(item._id);
playlistRefreshService.refreshPlaylist.mockResolvedValue(refreshedPlaylist);
playlistRefreshService.refreshPlaylist.mockResolvedValue(
refreshedPlaylist
);
service.refresh(item);
await Promise.resolve();
@@ -7,7 +7,9 @@ import { DialogService } from 'components';
import {
DatabaseService,
isDbAbortError,
PlaybackPositionService,
PlaylistRefreshService,
XtreamPendingRestoreService,
} from 'services';
import { ChannelActions, PlaylistActions } from 'm3u-state';
import { PlaylistMeta } from 'shared-interfaces';
@@ -21,8 +23,12 @@ export class PlaylistRefreshActionService {
private readonly snackBar = inject(MatSnackBar);
private readonly dialogService = inject(DialogService);
private readonly databaseService = inject(DatabaseService);
private readonly playbackPositionService = inject(PlaybackPositionService);
private readonly playlistRefreshService = inject(PlaylistRefreshService);
private readonly playlistContext = inject(PlaylistContextFacade);
private readonly pendingRestoreService = inject(
XtreamPendingRestoreService
);
readonly isRefreshing = signal(false);
@@ -31,9 +37,7 @@ export class PlaylistRefreshActionService {
return false;
}
return Boolean(
playlist.serverUrl || playlist.url || playlist.filePath
);
return Boolean(playlist.serverUrl || playlist.url || playlist.filePath);
}
refresh(playlist: PlaylistMeta): void {
@@ -75,34 +79,27 @@ export class PlaylistRefreshActionService {
);
const updateDate = Date.now();
const [
{
favoritedXtreamIds,
recentlyViewedXtreamIds,
hiddenCategories,
},
] = await Promise.all([
this.databaseService.deleteXtreamPlaylistContent(
item._id,
{ operationId }
),
this.databaseService.updateXtreamPlaylistDetails({
id: item._id,
updateDate,
}),
]);
const restoreKey = `xtream-restore-${item._id}`;
const restorePayload = {
favoritedXtreamIds,
recentlyViewedXtreamIds,
hiddenCategories,
};
localStorage.setItem(
restoreKey,
JSON.stringify(restorePayload)
const [restoreState, playbackPositions] = await Promise.all(
[
this.databaseService.deleteXtreamPlaylistContent(
item._id,
{ operationId }
),
this.playbackPositionService.getAllPlaybackPositions(
item._id
),
this.databaseService.updateXtreamPlaylistDetails({
id: item._id,
updateDate,
}),
]
);
this.pendingRestoreService.set(item._id, {
...restoreState,
playbackPositions,
});
this.store.dispatch(
PlaylistActions.updatePlaylistMeta({
playlist: { ...item, updateDate },
@@ -197,10 +194,7 @@ export class PlaylistRefreshActionService {
}
}
private getRefreshErrorMessage(
error: unknown,
item: PlaylistMeta
): string {
private getRefreshErrorMessage(error: unknown, item: PlaylistMeta): string {
if (
error instanceof Error &&
error.message?.includes('ENOENT') &&
@@ -211,8 +205,6 @@ export class PlaylistRefreshActionService {
);
}
return this.translate.instant(
'HOME.PLAYLISTS.PLAYLIST_UPDATE_ERROR'
);
return this.translate.instant('HOME.PLAYLISTS.PLAYLIST_UPDATE_ERROR');
}
}