feat(playback): recommend recovery actions (#1374)

* docs(playback): design recovery recommendations

* docs(playback): plan recovery recommendations

* refactor(playback): extract diagnostic utilities

* feat(playback): define recovery recommendation contracts

* feat(playback): rank recovery recommendations

* feat(playback): track session recovery attempts

* feat(playback): identify content recovery sessions

* feat(ui): add ranked playback diagnostic panel

* feat(playback): switch temporarily to recommended players

* test(playback): cover temporary player recommendation

* test(playback): verify recommendation capability guards

* docs(playback): document recovery recommendations

* fix(playback): keep recovery keys credential-free

* fix(playback): remove derived tracking ownership

* fix(playback): preserve distinct recovery fallbacks

* fix(playback): reset resume for new sources

* fix(playback): preserve desktop recovery guidance

* docs(playback): clarify recovery policy exceptions

* fix(playback): reject stale progress updates

* fix(playback): keep protected recovery guidance neutral

* test(playback): cover stale progress output

* fix(playback): neutralize protected diagnostic copy

* fix(playback): harden runtime guidance ownership

* fix(playback): stabilize recovery application ownership

* fix(ci): classify playback util coverage

* fix(e2e): preserve playback fixture bytes
This commit is contained in:
4gray authored and GitHub committed 2026-08-08 01:04:39 +02:00
1 parent 5e4f2ca3dd
commit fd96b85c19
176 files changed
+15228 -1908

No files matched your search

@@ -4,13 +4,17 @@ import { TranslateService } from '@ngx-translate/core';
import { DatabaseService, PlaylistsService } from '@iptvnator/services';
import { UnifiedFavoriteChannel } from '@iptvnator/portal/shared/util';
import { GlobalFavoritesService } from './global-favorites.service';
import { of } from 'rxjs';
describe('GlobalFavoritesService', () => {
let service: GlobalFavoritesService;
let electronApi: {
dbGetAppState: jest.Mock;
dbReorderGlobalFavorites: jest.Mock;
dbSetAppState: jest.Mock;
};
const storeSelect = jest.fn();
const getPlaylistById = jest.fn();
const makeChannel = (
overrides: Partial<UnifiedFavoriteChannel> &
@@ -25,7 +29,10 @@ describe('GlobalFavoritesService', () => {
});
beforeEach(() => {
storeSelect.mockReset().mockReturnValue(of([]));
getPlaylistById.mockReset();
electronApi = {
dbGetAppState: jest.fn().mockResolvedValue(null),
dbReorderGlobalFavorites: jest
.fn()
.mockResolvedValue({ success: true }),
@@ -39,9 +46,12 @@ describe('GlobalFavoritesService', () => {
TestBed.configureTestingModule({
providers: [
GlobalFavoritesService,
{ provide: Store, useValue: { select: jest.fn() } },
{ provide: Store, useValue: { select: storeSelect } },
{ provide: DatabaseService, useValue: {} },
{ provide: PlaylistsService, useValue: {} },
{
provide: PlaylistsService,
useValue: { getPlaylistById },
},
{
provide: TranslateService,
useValue: { instant: (key: string) => key },
@@ -58,6 +68,52 @@ describe('GlobalFavoritesService', () => {
});
});
it('keeps legacy URL UIDs aligned with the persisted M3U order', async () => {
const firstUrl = 'https://streams.example/one.ts';
const secondUrl = 'https://streams.example/two.ts';
storeSelect.mockReturnValue(
of([
{
_id: 'playlist-one',
title: 'Playlist',
favorites: [firstUrl, secondUrl],
},
])
);
getPlaylistById.mockReturnValue(
of({
playlist: {
items: [
{
id: 'channel-one',
name: 'Channel One',
url: firstUrl,
},
{
id: 'channel-two',
name: 'Channel Two',
url: secondUrl,
},
],
},
})
);
electronApi.dbGetAppState.mockResolvedValue(
JSON.stringify([
`m3u::playlist-one::${secondUrl}`,
`m3u::playlist-one::${firstUrl}`,
])
);
const channels = await service.getUnifiedLiveFavorites();
expect(channels.map((channel) => channel.uid)).toEqual([
`m3u::playlist-one::${secondUrl}`,
`m3u::playlist-one::${firstUrl}`,
]);
expect(channels.map((channel) => channel.position)).toEqual([0, 1]);
});
describe('reorder', () => {
it('sends playlist-scoped position updates for Xtream favorites', async () => {
// The backend UPDATE filters by (contentId, playlistId); a payload
@@ -109,7 +165,10 @@ describe('GlobalFavoritesService', () => {
expect(electronApi.dbReorderGlobalFavorites).not.toHaveBeenCalled();
expect(electronApi.dbSetAppState).toHaveBeenCalledWith(
'global-favorites-channel-order-v1',
JSON.stringify(['m3u::playlist-m::url', 'stalker::playlist-s::5'])
JSON.stringify([
'm3u::playlist-m::url',
'stalker::playlist-s::5',
])
);
});
});