mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-17 06:36:15 -08:00
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:
1 parent
46c58f4f57
commit
760099358b
188 files changed
+31245
-2281
No files matched your search
@@ -105,13 +105,13 @@ const DOWNLOADS_TABLE_SQL = `CREATE TABLE IF NOT EXISTS downloads (
|
||||
poster_url TEXT,
|
||||
request_headers TEXT,
|
||||
resume_validator TEXT,
|
||||
metadata_snapshot TEXT,
|
||||
status TEXT NOT NULL DEFAULT 'queued' CHECK (status IN ('queued', 'downloading', 'paused', 'completed', 'failed', 'canceled')),
|
||||
bytes_downloaded INTEGER DEFAULT 0,
|
||||
total_bytes INTEGER,
|
||||
error_message TEXT,
|
||||
created_at TEXT DEFAULT (datetime('now')),
|
||||
updated_at TEXT DEFAULT (datetime('now')),
|
||||
FOREIGN KEY (playlist_id) REFERENCES playlists (id) ON DELETE CASCADE
|
||||
updated_at TEXT DEFAULT (datetime('now'))
|
||||
)`;
|
||||
const DOWNLOADS_INDEX_STATEMENTS = [
|
||||
`CREATE UNIQUE INDEX IF NOT EXISTS downloads_xtream_playlist_unique ON downloads(xtream_id, playlist_id, content_type)`,
|
||||
@@ -379,6 +379,8 @@ const COLUMN_MIGRATION_STATEMENTS = [
|
||||
`ALTER TABLE epg_programs ADD COLUMN source_url TEXT`,
|
||||
// Pause/resume: entity validator (ETag/Last-Modified) sent as If-Range on resume
|
||||
`ALTER TABLE downloads ADD COLUMN resume_validator TEXT`,
|
||||
// Offline details: provider-neutral display metadata captured at download time
|
||||
`ALTER TABLE downloads ADD COLUMN metadata_snapshot TEXT`,
|
||||
];
|
||||
|
||||
const INDEX_MIGRATION_STATEMENTS = [
|
||||
@@ -579,8 +581,7 @@ function normalizeXtreamContentAddedEpochs(sqliteDb: Database.Database): void {
|
||||
const migrationState = sqliteDb
|
||||
.prepare(`SELECT value FROM app_state WHERE key = ?`)
|
||||
.get(XTREAM_ADDED_EPOCH_SECONDS_MIGRATION_KEY) as
|
||||
| { value?: unknown }
|
||||
| undefined;
|
||||
{ value?: unknown } | undefined;
|
||||
|
||||
if (migrationState?.value === 'done') {
|
||||
return;
|
||||
@@ -697,8 +698,7 @@ function upgradeContentTitleFtsTokenizer(sqliteDb: Database.Database): boolean {
|
||||
const migrationState = sqliteDb
|
||||
.prepare(`SELECT value FROM app_state WHERE key = ?`)
|
||||
.get(CONTENT_TITLE_FTS_DIACRITICS_MIGRATION_KEY) as
|
||||
| { value?: unknown }
|
||||
| undefined;
|
||||
{ value?: unknown } | undefined;
|
||||
|
||||
// The marker alone is not evidence. `createTables` declares this table
|
||||
// too, with the plain tokenizer, so a table recreated by that path
|
||||
@@ -759,8 +759,7 @@ function ensureContentTitleFts(sqliteDb: Database.Database): void {
|
||||
const migrationState = sqliteDb
|
||||
.prepare(`SELECT value FROM app_state WHERE key = ?`)
|
||||
.get(CONTENT_TITLE_FTS_MIGRATION_KEY) as
|
||||
| { value?: unknown }
|
||||
| undefined;
|
||||
{ value?: unknown } | undefined;
|
||||
|
||||
if (migrationState?.value === 'done') {
|
||||
return;
|
||||
@@ -803,8 +802,7 @@ function backfillEpgProgramSourceUrls(sqliteDb: Database.Database): void {
|
||||
const migrationState = sqliteDb
|
||||
.prepare(`SELECT value FROM app_state WHERE key = ?`)
|
||||
.get(EPG_PROGRAM_SOURCE_URL_BACKFILL_MIGRATION_KEY) as
|
||||
| { value?: unknown }
|
||||
| undefined;
|
||||
{ value?: unknown } | undefined;
|
||||
|
||||
if (migrationState?.value === 'done') {
|
||||
return;
|
||||
@@ -938,8 +936,7 @@ function cleanupLegacyTmdbSearchCache(sqliteDb: Database.Database): void {
|
||||
const migrationState = sqliteDb
|
||||
.prepare(`SELECT value FROM app_state WHERE key = ?`)
|
||||
.get(TMDB_SEARCH_LOOKUP_V2_CACHE_CLEANUP_MIGRATION_KEY) as
|
||||
| { value?: unknown }
|
||||
| undefined;
|
||||
{ value?: unknown } | undefined;
|
||||
|
||||
if (migrationState?.value === 'done') {
|
||||
return;
|
||||
@@ -978,9 +975,9 @@ function cleanupLegacyTmdbSearchCache(sqliteDb: Database.Database): void {
|
||||
}
|
||||
|
||||
/**
|
||||
* Existing downloads tables had a status CHECK without 'paused'. SQLite cannot
|
||||
* alter CHECK constraints in place, so rebuild only when the table SQL still
|
||||
* reflects the old contract or lacks request_headers.
|
||||
* Downloads remain locally owned after their source playlist is removed.
|
||||
* SQLite cannot alter CHECK or foreign-key constraints in place, so rebuild
|
||||
* when the table still has the old pause/header contract or source cascade.
|
||||
*/
|
||||
function ensureDownloadsPauseResumeSchema(sqliteDb: Database.Database): void {
|
||||
try {
|
||||
@@ -995,13 +992,24 @@ function ensureDownloadsPauseResumeSchema(sqliteDb: Database.Database): void {
|
||||
|
||||
const hasPausedStatus = row.sql.includes(`'paused'`);
|
||||
const hasRequestHeaders = row.sql.includes('request_headers');
|
||||
if (hasPausedStatus && hasRequestHeaders) {
|
||||
const hasPlaylistForeignKey = /\bREFERENCES\s+["`[]?playlists\b/i.test(
|
||||
row.sql
|
||||
);
|
||||
if (hasPausedStatus && hasRequestHeaders && !hasPlaylistForeignKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
const legacyHeadersSelect = hasRequestHeaders
|
||||
? 'request_headers'
|
||||
: 'NULL AS request_headers';
|
||||
const hasMetadataSnapshot = row.sql.includes('metadata_snapshot');
|
||||
const legacyMetadataSnapshotSelect = hasMetadataSnapshot
|
||||
? 'metadata_snapshot'
|
||||
: 'NULL AS metadata_snapshot';
|
||||
const hasResumeValidator = row.sql.includes('resume_validator');
|
||||
const legacyResumeValidatorSelect = hasResumeValidator
|
||||
? 'resume_validator'
|
||||
: 'NULL AS resume_validator';
|
||||
const rebuild = sqliteDb.transaction(() => {
|
||||
for (const statement of DOWNLOADS_INDEX_STATEMENTS) {
|
||||
const match = statement.match(
|
||||
@@ -1034,6 +1042,8 @@ function ensureDownloadsPauseResumeSchema(sqliteDb: Database.Database): void {
|
||||
file_path,
|
||||
poster_url,
|
||||
request_headers,
|
||||
resume_validator,
|
||||
metadata_snapshot,
|
||||
status,
|
||||
bytes_downloaded,
|
||||
total_bytes,
|
||||
@@ -1055,6 +1065,8 @@ function ensureDownloadsPauseResumeSchema(sqliteDb: Database.Database): void {
|
||||
file_path,
|
||||
poster_url,
|
||||
${legacyHeadersSelect},
|
||||
${legacyResumeValidatorSelect},
|
||||
${legacyMetadataSnapshotSelect},
|
||||
status,
|
||||
bytes_downloaded,
|
||||
total_bytes,
|
||||
@@ -1064,18 +1076,16 @@ function ensureDownloadsPauseResumeSchema(sqliteDb: Database.Database): void {
|
||||
FROM downloads_pause_resume_legacy`
|
||||
)
|
||||
.run();
|
||||
sqliteDb
|
||||
.prepare(`DROP TABLE downloads_pause_resume_legacy`)
|
||||
.run();
|
||||
sqliteDb.prepare(`DROP TABLE downloads_pause_resume_legacy`).run();
|
||||
for (const statement of DOWNLOADS_INDEX_STATEMENTS) {
|
||||
sqliteDb.prepare(statement).run();
|
||||
}
|
||||
});
|
||||
|
||||
rebuild();
|
||||
console.log('[DB] Rebuilt downloads table with pause/resume schema');
|
||||
console.log('[DB] Rebuilt downloads table with local ownership schema');
|
||||
} catch (error) {
|
||||
console.warn('[DB] downloads pause/resume migration failed:', error);
|
||||
console.warn('[DB] downloads ownership migration failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user