fix(downloads): accept nullable legacy coordinates

This commit is contained in:
4gray committed 2026-08-02 19:40:33 +02:00
1 parent 339dbde3c4
commit a667bfbe05
5 files changed
+61 -12

No files matched your search

+5 -3
View File
@@ -955,9 +955,11 @@ engine` (restart required) or
fallback that migrates reusable rows to the canonical id. Renderer lookup
preserves ambiguity or conflicting ownership as a distinct ineligible state,
so neither the episode action nor the season count treats it as a row-less
download. Pending and active rows plus completed available/unknown rows are
skipped; failed, canceled, completed-missing, and unambiguous row-less
episodes remain eligible.
download. SQLite `null` and optional `undefined` coordinates both mean an
incomplete canonical legacy row, matching the backend resolver. Pending and
active rows plus completed available/unknown rows are skipped; failed,
canceled, completed-missing, and unambiguous row-less episodes remain
eligible.
- Ready movie and grouped-series cards open a focused local detail. Movies play
the finalized local file; series list only locally available episode rows and
every episode action targets its own downloaded file. Focused routes disable
+8 -6
View File
@@ -82,12 +82,14 @@ variants, contextual buttons, and theme-aware styling.
coordinates are a legacy episode-compatibility fallback; ambiguous or
conflicting matches resolve to an explicit renderer conflict state rather
than masquerading as a missing row. Both the episode action and season count
fail closed for that state, while restarting an eligible legacy row migrates
it to the canonical id. Renderer-pending, queued, downloading, and paused
episodes are skipped, as are completed rows whose file is available or whose
availability is still unknown. Failed, canceled, completed-missing, and
unambiguous row-less episodes are eligible; a completed-missing row is
restarted as a fresh download. `DOWNLOADS_START` remains the only start IPC
fail closed for that state. SQLite `null` and optional `undefined` coordinates
both mean that a canonical legacy row is incomplete, matching the backend
resolver, while restarting an eligible legacy row migrates it to the
canonical id. Renderer-pending, queued, downloading, and paused episodes are
skipped, as are completed rows whose file is available or whose availability
is still unknown. Failed, canceled, completed-missing, and unambiguous
row-less episodes are eligible; a completed-missing row is restarted as a
fresh download. `DOWNLOADS_START` remains the only start IPC
and may return the stable `reason: 'already-in-progress'` for an active match,
which the coordinator counts as skipped. There is no batch IPC, schema
migration, parallel transfer, or queue reordering: destination authorization,
@@ -42,6 +42,22 @@ describe('episode download identity', () => {
});
});
it.each([
['series id', { seriesXtreamId: null }],
['season number', { seasonNumber: null }],
['episode number', { episodeNumber: null }],
])(
'treats a null legacy %s as an incomplete canonical row',
(_label, override) => {
const canonical = row(override);
expect(resolveEpisodeDownload(identity, [canonical])).toEqual({
kind: 'match',
download: canonical,
});
}
);
it('fails closed when canonical and coordinate lookups find different rows', () => {
const coordinateFallback = row({ xtreamId: 999 });
const canonical = row();
@@ -18,9 +18,9 @@ export interface EpisodeDownloadRecord {
readonly playlistId: string;
readonly contentType: ElectronBridgePlaybackContentType;
readonly xtreamId: number;
readonly seriesXtreamId?: number;
readonly seasonNumber?: number;
readonly episodeNumber?: number;
readonly seriesXtreamId?: number | null;
readonly seasonNumber?: number | null;
readonly episodeNumber?: number | null;
readonly status: ElectronBridgeDownloadStatus;
readonly fileAvailability?: ElectronDownloadFileAvailability;
readonly filePath?: string;
@@ -37,8 +37,11 @@ function hasConflictingCompleteCoordinates(
): boolean {
const { seriesXtreamId, seasonNumber, episodeNumber } = download;
if (
seriesXtreamId === null ||
seriesXtreamId === undefined ||
seasonNumber === null ||
seasonNumber === undefined ||
episodeNumber === null ||
episodeNumber === undefined
) {
return false;
@@ -638,6 +638,32 @@ describe('SeasonContainerComponent', () => {
expect(downloadsServiceStub.startDownload).not.toHaveBeenCalled();
});
it('keeps a failed canonical legacy row with null coordinates downloadable', () => {
const episode = createEpisode();
const legacy = {
...createDownload(episode, { status: 'failed' }),
seriesXtreamId: null,
seasonNumber: null,
episodeNumber: null,
} as unknown as DownloadItem;
downloadsServiceStub.downloads.set([legacy]);
enableDownloads();
setRequiredInputs({ '1': [episode] });
fixture.detectChanges();
const action = episodeAction(episode.id);
const seasonButton = fixture.nativeElement.querySelector(
'[data-test-id="download-season"]'
) as HTMLButtonElement;
expect(action.disabled).toBe(false);
expect(action.querySelector('mat-icon')?.textContent).toContain(
'download'
);
expect(seasonButton.disabled).toBe(false);
expect(seasonButton.textContent).toContain('Download season (1)');
});
it('gives the grid and list view radios localized accessible names', () => {
TestBed.inject(TranslateService).setTranslation(
'en',