mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-13 12:36:15 -08:00
feat(portals): carry VOD source pins through playlist backup
The new pins table was invisible to backup: exporting a playlist and re-importing it on a new machine silently dropped every "main source" choice, with nothing in the archive to say the choice had ever been made. Pins now ride along under the playlist they point AT — carrying them anywhere else would restore a preference for a portal the archive never contained. `matchKey` names the film rather than the portal, so it survives untouched and only the playlist id is remapped to the imported copy. `sourcePins` is the one optional collection in the Xtream user state: archives written before multi-source existed simply do not have it, so its absence is age rather than damage. Only a wrong type is rejected, and pins without a usable match key or content id are dropped, since writing one would occupy the unique key of a film it does not describe. Adds `DB_LIST_VOD_SOURCE_PINS` through the usual six seams (operation, worker case, event, preload, bridge contract, service). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
7cce227268
commit
9deb4325e2
18 files changed
+297
-2
No files matched your search
@@ -880,6 +880,8 @@ export interface ElectronBridgeApi {
|
||||
}) => Promise<VodSourceCandidateRow[]>;
|
||||
/** Per-movie pinned source; keys are passed most-trusted first */
|
||||
dbGetVodSourcePin: (matchKeys: string[]) => Promise<VodSourcePin | null>;
|
||||
/** Every pin pointing at this playlist — used by playlist backup. */
|
||||
dbListVodSourcePins: (playlistId: string) => Promise<VodSourcePin[]>;
|
||||
dbSetVodSourcePin: (pin: VodSourcePin) => Promise<ElectronBridgeResult>;
|
||||
dbClearVodSourcePin: (
|
||||
matchKeys: string[]
|
||||
|
||||
@@ -58,11 +58,25 @@ export interface XtreamBackupRecentlyViewedItem {
|
||||
viewedAt: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A per-movie preferred source, carried under the playlist it points AT.
|
||||
*
|
||||
* `matchKey` identifies the film, not the portal, so it survives the restore
|
||||
* untouched — only the playlist id has to be remapped to the imported copy.
|
||||
*/
|
||||
export interface XtreamBackupSourcePin {
|
||||
matchKey: string;
|
||||
contentId: number;
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
export interface XtreamBackupUserState {
|
||||
hiddenCategories: XtreamBackupHiddenCategory[];
|
||||
favorites: XtreamBackupFavoriteItem[];
|
||||
recentlyViewed: XtreamBackupRecentlyViewedItem[];
|
||||
playbackPositions: PlaybackPositionData[];
|
||||
/** Optional: absent in archives written before multi-source existed. */
|
||||
sourcePins?: XtreamBackupSourcePin[];
|
||||
}
|
||||
|
||||
export interface XtreamPlaylistBackupEntry extends PlaylistBackupBaseEntry {
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
XtreamBackupFavoriteItem,
|
||||
XtreamBackupHiddenCategory,
|
||||
XtreamBackupRecentlyViewedItem,
|
||||
XtreamBackupSourcePin,
|
||||
} from './playlist-backup.interface';
|
||||
import { PlaybackPositionData } from './playback-position.interface';
|
||||
|
||||
@@ -11,6 +12,11 @@ export interface XtreamPendingRestoreState {
|
||||
favorites: XtreamBackupFavoriteItem[];
|
||||
recentlyViewed: XtreamBackupRecentlyViewedItem[];
|
||||
playbackPositions: PlaybackPositionData[];
|
||||
/**
|
||||
* Optional: absent from archives and persisted entries written before
|
||||
* multi-source existed. The normalizer always fills it.
|
||||
*/
|
||||
sourcePins?: XtreamBackupSourcePin[];
|
||||
}
|
||||
|
||||
export function getXtreamPendingRestoreStorageKey(playlistId: string): string {
|
||||
@@ -28,6 +34,7 @@ interface RestoreStateCandidate {
|
||||
favorites?: unknown;
|
||||
recentlyViewed?: unknown;
|
||||
playbackPositions?: unknown;
|
||||
sourcePins?: unknown;
|
||||
}
|
||||
|
||||
interface RestoreEntryCandidate {
|
||||
@@ -99,6 +106,7 @@ export function normalizeXtreamPendingRestoreState(
|
||||
favorites: [],
|
||||
recentlyViewed: [],
|
||||
playbackPositions: [],
|
||||
sourcePins: [],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -136,5 +144,38 @@ export function normalizeXtreamPendingRestoreState(
|
||||
playbackPositions: toArray(candidate.playbackPositions).filter(
|
||||
(item): item is PlaybackPositionData => isRecord(item)
|
||||
),
|
||||
sourcePins: normalizeSourcePins(candidate.sourcePins),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* A pin without a usable match key or content id cannot address anything, and
|
||||
* writing it would occupy the unique key of a film it does not describe.
|
||||
*/
|
||||
function normalizeSourcePins(value: unknown): XtreamBackupSourcePin[] {
|
||||
const pins: XtreamBackupSourcePin[] = [];
|
||||
|
||||
for (const item of toArray(value)) {
|
||||
if (!isRecord(item)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const matchKey = (item as { matchKey?: unknown }).matchKey;
|
||||
const contentId = normalizeXtreamBackupId(
|
||||
(item as { contentId?: unknown }).contentId
|
||||
);
|
||||
const updatedAt = (item as { updatedAt?: unknown }).updatedAt;
|
||||
|
||||
if (typeof matchKey !== 'string' || !matchKey || contentId === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
pins.push({
|
||||
matchKey,
|
||||
contentId,
|
||||
...(typeof updatedAt === 'string' ? { updatedAt } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
return pins;
|
||||
}
|
||||
Reference in new issue
Block a user