Files
iptvnator/libs/shared/database/src/lib/download-schema.ts
T
4gray bad8a0991e feat(downloads): download completed Xtream catch-up programmes as TS (#1572)
* feat(epg): copy catch-up programme URLs without changing playback

* feat(downloads): save completed Xtream archive programmes as TS

* fix(epg): let newer archive copy requests supersede pending work

* fix(downloads): protect archive partials and independent submissions

* fix(downloads): verify archive identity through finalization

* fix(downloads): bound archive storage and capture cleanup entries

* fix(downloads): preserve archive ownership across failure paths

* fix(downloads): recover explicitly verified archive completions

* fix(downloads): journal archive promotion before publishing files

* fix(downloads): reset archive proof before an explicit restart

* fix(downloads): preserve archive recovery ownership and interruption

* fix(downloads): verify durable archive identity at resume open

* fix(downloads): fence archive commands during completion commit

* fix(downloads): persist archive ownership throughout its lifecycle

* fix(downloads): protect archive removal and missing-file recovery

* fix(downloads): journal private cleanup captures for recovery

* fix(downloads): journal active archive cleanup before removal

* fix(downloads): clean settled archives before deleting stale rows

* fix(downloads): preserve archive ownership on removal and resubmission

* fix(downloads): recover proven archive completions before retry

* fix(downloads): recover local archives before remote transfer checks

* test(downloads): resolve archive fixture from workspace root

* fix(downloads): distinguish reused archive inodes by creation time

* fix(downloads): bind fresh archive reservations to owned files

* fix(downloads): clean reservations when ownership writes fail

* fix(downloads): commit archive reservation and ownership atomically

* fix(downloads): retain captures until replacement restoration succeeds

* fix(downloads): require durable ownership before cleanup relocation

* fix(downloads): preserve 64-bit archive file identities on Windows

* refactor(release): keep capture fixture constants in their shared module

* fix(downloads): preserve the last link of captured foreign files

* fix(downloads): expose retained archive recovery files

* fix(downloads): keep recovery instructions open while copying
2026-09-08 20:33:05 +02:00

95 lines
3.5 KiB
TypeScript

import type Database from 'better-sqlite3';
export const DOWNLOADS_TABLE_SQL = `CREATE TABLE IF NOT EXISTS downloads (
id INTEGER PRIMARY KEY AUTOINCREMENT,
playlist_id TEXT NOT NULL,
xtream_id INTEGER NOT NULL,
content_type TEXT NOT NULL CHECK (content_type IN ('vod', 'episode', 'catchup')),
programme_start INTEGER NOT NULL DEFAULT 0,
catchup TEXT,
series_xtream_id INTEGER,
season_number INTEGER,
episode_number INTEGER,
episode_identity_scope TEXT,
title TEXT NOT NULL,
url TEXT NOT NULL,
file_name TEXT,
file_path TEXT,
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'))
)`;
export const DOWNLOADS_INDEX_STATEMENTS = [
`CREATE UNIQUE INDEX IF NOT EXISTS downloads_xtream_playlist_unique ON downloads(xtream_id, playlist_id, content_type) WHERE content_type != 'catchup'`,
`CREATE INDEX IF NOT EXISTS downloads_playlist_idx ON downloads(playlist_id)`,
`CREATE INDEX IF NOT EXISTS downloads_status_idx ON downloads(status)`,
];
const ARCHIVE_FINALIZATIONS_SQL = `CREATE TABLE IF NOT EXISTS download_archive_finalizations (
download_id INTEGER PRIMARY KEY REFERENCES downloads(id) ON DELETE CASCADE,
proof TEXT NOT NULL
)`;
const CATCHUP_INDEX = `CREATE UNIQUE INDEX IF NOT EXISTS downloads_catchup_unique
ON downloads(xtream_id, playlist_id, programme_start) WHERE content_type = 'catchup'`;
/** Widen the CHECK transactionally; existing ids, files and resume state survive. */
export function ensureDownloadsCatchupSchema(db: Database.Database): void {
const row = db
.prepare(
"SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'downloads'"
)
.get() as { sql: string } | undefined;
if (!row) return;
if (row.sql.includes("'catchup'")) {
db.exec(CATCHUP_INDEX);
db.exec(ARCHIVE_FINALIZATIONS_SQL);
return;
}
const columns = [
'id',
'playlist_id',
'xtream_id',
'content_type',
'series_xtream_id',
'season_number',
'episode_number',
'episode_identity_scope',
'title',
'url',
'file_name',
'file_path',
'poster_url',
'request_headers',
'resume_validator',
'metadata_snapshot',
'status',
'bytes_downloaded',
'total_bytes',
'error_message',
'created_at',
'updated_at',
];
db.transaction(() => {
db.exec('DROP INDEX IF EXISTS downloads_xtream_playlist_unique');
db.exec('DROP INDEX IF EXISTS downloads_playlist_idx');
db.exec('DROP INDEX IF EXISTS downloads_status_idx');
db.exec('ALTER TABLE downloads RENAME TO downloads_catchup_legacy');
db.exec(DOWNLOADS_TABLE_SQL);
db.exec(
`INSERT INTO downloads (${columns.join(',')}) SELECT ${columns.join(',')} FROM downloads_catchup_legacy`
);
db.exec('DROP TABLE downloads_catchup_legacy');
for (const statement of DOWNLOADS_INDEX_STATEMENTS) db.exec(statement);
db.exec(CATCHUP_INDEX);
db.exec(ARCHIVE_FINALIZATIONS_SQL);
})();
}