mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-15 05:36:17 -08:00
* 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
95 lines
3.5 KiB
TypeScript
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);
|
|
})();
|
|
}
|