feat(downloads): redesign download manager (#1313)

* docs(downloads): specify manager MVP redesign

* docs(downloads): plan manager MVP implementation

* docs(downloads): tighten manager validation plan

* fix(downloads): keep renderer download state global

* fix(downloads): make active count accessible

* feat(downloads): derive queue and library view model

* test(downloads): close view model coverage gaps

* fix(downloads): stabilize malformed view model data

* refactor(downloads): isolate library navigation

* fix(downloads): report library navigation failures

* feat(downloads): add ready-to-watch library

* feat(downloads): add active download queue

* feat(downloads): finish manager MVP

* docs(downloads): clarify detail-first offline behavior

* docs(downloads): plan detail navigation follow-up

* fix(downloads): open completed movies in details

* test(downloads): cover pending series navigation

* fix(downloads): honor the global cover size

* fix(downloads): prefer local playback in shared details

* fix(downloads): preserve external launch priority

* fix(downloads): prefer local playback in Xtream details

* test(downloads): cover offline detail journey

* docs(downloads): document offline detail behavior

* docs(downloads): format detail navigation plan

* fix(downloads): open Stalker items in provider details

* docs(downloads): clarify Stalker navigation fallback

* fix(xtream): isolate reused detail identities

* fix(xtream): ignore stale VOD positions

* fix(downloads): keep offline Xtream playback available

* docs(downloads): clarify provider playback availability

* docs(downloads): design missing-file recovery

* docs(downloads): plan missing-file recovery

* feat(downloads): derive completed file availability

* feat(downloads): recover missing completed files

* feat(downloads): refresh missing local files

* feat(downloads): separate missing files from ready media

* feat(downloads): surface missing files for recovery

* refactor(downloads): simplify ready cards

* test(downloads): cover missing-file and series journeys

* feat(downloads): finish missing-file recovery

* docs(downloads): design offline detail views

* docs(downloads): plan offline detail views

* feat(downloads): persist offline metadata snapshots

* fix(downloads): complete metadata snapshot bridge contract

* feat(downloads): manage offline metadata snapshots

* fix(downloads): harden metadata snapshot updates

* fix(downloads): restrict snapshot artwork

* fix(downloads): guard restart artwork URL

* fix(downloads): refine artwork URL checks

* feat(downloads): expose offline metadata updates

* fix(downloads): keep metadata service change focused

* fix(downloads): preserve metadata error conventions

* feat(downloads): derive offline detail content

* fix(downloads): preserve unknown episode coordinates

* feat(downloads): add focused offline detail routes

* fix(downloads): ignore fragments in shell route state

* fix(downloads): normalize fragments before queries

* feat(downloads): open ready cards in offline details

* fix(downloads): use native disabled card styles

* feat(downloads): enrich offline detail metadata

* fix(downloads): harden offline metadata resolution

* fix(downloads): preserve stalker provider titles

* fix(downloads): distinguish stalker metadata seeds

* fix(downloads): stabilize offline metadata refresh

* fix(downloads): throttle sparse metadata refreshes

* fix(downloads): type metadata language settings

* feat(downloads): render offline movie and series details

* fix(downloads): harden offline detail interactions

* fix(downloads): close offline detail edge cases

* feat(downloads): hand off to provider-only details

* fix(downloads): preserve stalker provider handoff

* feat(downloads): capture metadata at download time

* fix(downloads): preserve snapshot source semantics

* fix(downloads): preserve episode snapshot identity

* docs(downloads): document offline details flow

* docs(downloads): clarify stalker provider fallback

* test(downloads): cover offline detail journeys

* test(downloads): stabilize offline detail selectors

* style(downloads): format changed files

* docs(downloads): clean design spec formatting

* fix(downloads): preserve offline library ownership

* test(downloads): fix Windows workspace navigation

* test(database): preserve Electron tsconfig resolution

* perf(downloads): avoid blocking file availability probes
This commit is contained in:
4gray authored and GitHub committed 2026-08-01 18:09:31 +02:00
1 parent 46c58f4f57
commit 760099358b
188 files changed
+31245 -2281

No files matched your search

@@ -22,7 +22,6 @@ function createDeleteHarness() {
['select:favorites', [{ id: 1 }, { id: 2 }]],
['select:recently-viewed', [{ id: 3 }]],
['select:playback-positions', []],
['select:downloads', [{ id: 4 }]],
['select:categories', [{ id: 10 }, { id: 11 }]],
[
'select:content',
@@ -80,15 +79,14 @@ describe('playlist delete performance phases', () => {
)
).resolves.toEqual({ success: true });
expect(harness.transaction).toHaveBeenCalledTimes(7);
// Seven committed chunks plus the final playlist row each keep both
expect(harness.transaction).toHaveBeenCalledTimes(6);
// Six committed chunks plus the final playlist row each keep both
// the pre-write and post-progress cooperative checkpoints.
expect(checkpoint).toHaveBeenCalledTimes(16);
expect(harness.timeline.slice(1, 7)).toEqual([
expect(checkpoint).toHaveBeenCalledTimes(14);
expect(harness.timeline.slice(1, 6)).toEqual([
'select:favorites',
'select:recently-viewed',
'select:playback-positions',
'select:downloads',
'select:categories',
'select:content',
]);
@@ -117,7 +115,7 @@ describe('playlist delete performance phases', () => {
},
{
boundary: 'end',
metadata: { itemCount: 211 },
metadata: { itemCount: 210 },
phase: XTREAM_DATABASE_PERFORMANCE_PHASE.SQLITE_PLAYLIST_DELETE_COLLECT_IDS,
},
{
@@ -126,7 +124,7 @@ describe('playlist delete performance phases', () => {
},
{
boundary: 'end',
metadata: { itemCount: 212 },
metadata: { itemCount: 211 },
phase: XTREAM_DATABASE_PERFORMANCE_PHASE.SQLITE_PLAYLIST_DELETE_WRITE_TRANSACTIONS,
},
]);
@@ -591,7 +591,6 @@ export async function updatePlaylist(
interface PlaylistDeletionCollection {
readonly categoryIds: number[];
readonly contentRows: Array<{ id: number }>;
readonly downloadRows: Array<{ id: number }>;
readonly favoriteRows: Array<{ id: number }>;
readonly playbackPositionRows: Array<{ id: number }>;
readonly recentlyViewedRows: Array<{ id: number }>;
@@ -601,29 +600,21 @@ async function collectPlaylistDeletionRows(
db: AppDatabase,
playlistId: string
): Promise<PlaylistDeletionCollection> {
const [
favoriteRows,
recentlyViewedRows,
playbackPositionRows,
downloadRows,
] = await Promise.all([
db
.select({ id: schema.favorites.id })
.from(schema.favorites)
.where(eq(schema.favorites.playlistId, playlistId)),
db
.select({ id: schema.recentlyViewed.id })
.from(schema.recentlyViewed)
.where(eq(schema.recentlyViewed.playlistId, playlistId)),
db
.select({ id: schema.playbackPositions.id })
.from(schema.playbackPositions)
.where(eq(schema.playbackPositions.playlistId, playlistId)),
db
.select({ id: schema.downloads.id })
.from(schema.downloads)
.where(eq(schema.downloads.playlistId, playlistId)),
]);
const [favoriteRows, recentlyViewedRows, playbackPositionRows] =
await Promise.all([
db
.select({ id: schema.favorites.id })
.from(schema.favorites)
.where(eq(schema.favorites.playlistId, playlistId)),
db
.select({ id: schema.recentlyViewed.id })
.from(schema.recentlyViewed)
.where(eq(schema.recentlyViewed.playlistId, playlistId)),
db
.select({ id: schema.playbackPositions.id })
.from(schema.playbackPositions)
.where(eq(schema.playbackPositions.playlistId, playlistId)),
]);
const categoryRows = await db
.select({ id: schema.categories.id })
@@ -641,7 +632,6 @@ async function collectPlaylistDeletionRows(
return {
categoryIds,
contentRows,
downloadRows,
favoriteRows,
playbackPositionRows,
recentlyViewedRows,
@@ -655,7 +645,6 @@ function countPlaylistDeletionRows(
collection.favoriteRows.length +
collection.recentlyViewedRows.length +
collection.playbackPositionRows.length +
collection.downloadRows.length +
collection.contentRows.length +
collection.categoryIds.length
);
@@ -686,12 +675,6 @@ async function deleteCollectedPlaylistRows(
schema.playbackPositions.id,
schema.playbackPositions,
],
[
'deleting-downloads',
collection.downloadRows.map((row) => row.id),
schema.downloads.id,
schema.downloads,
],
[
'deleting-content',
collection.contentRows.map((row) => row.id),