Files
iptvnator/libs/shared/database/src/lib/download-tables.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

85 lines
3.1 KiB
TypeScript

import type { CatchupDownloadMetadata } from '@iptvnator/shared/interfaces';
import { sql } from 'drizzle-orm';
import {
index,
integer,
sqliteTable,
text,
uniqueIndex,
} from 'drizzle-orm/sqlite-core';
// Downloads table
export const downloads = sqliteTable(
'downloads',
{
id: integer('id').primaryKey({ autoIncrement: true }),
playlistId: text('playlist_id').notNull(),
// Content identifiers
xtreamId: integer('xtream_id').notNull(),
contentType: text('content_type', {
enum: ['vod', 'episode', 'catchup'],
}).notNull(),
programmeStart: integer('programme_start').notNull().default(0),
catchup: text('catchup', {
mode: 'json',
}).$type<CatchupDownloadMetadata>(),
// For episodes: store series info
seriesXtreamId: integer('series_xtream_id'),
seasonNumber: integer('season_number'),
episodeNumber: integer('episode_number'),
episodeIdentityScope: text('episode_identity_scope'),
// Download metadata
title: text('title').notNull(),
url: text('url').notNull(),
fileName: text('file_name'),
filePath: text('file_path'),
posterUrl: text('poster_url'),
requestHeaders: text('request_headers'),
resumeValidator: text('resume_validator'),
metadataSnapshot: text('metadata_snapshot'),
// Download progress
status: text('status', {
enum: [
'queued',
'downloading',
'paused',
'completed',
'failed',
'canceled',
],
})
.notNull()
.default('queued'),
bytesDownloaded: integer('bytes_downloaded').default(0),
totalBytes: integer('total_bytes'),
errorMessage: text('error_message'),
// Timestamps
createdAt: text('created_at').default(sql`CURRENT_TIMESTAMP`),
updatedAt: text('updated_at').default(sql`CURRENT_TIMESTAMP`),
},
(table) => ({
playlistIdx: index('downloads_playlist_idx').on(table.playlistId),
statusIdx: index('downloads_status_idx').on(table.status),
xtreamPlaylistUnique: uniqueIndex('downloads_xtream_playlist_unique')
.on(table.xtreamId, table.playlistId, table.contentType)
.where(sql`${table.contentType} != 'catchup'`),
catchupUnique: uniqueIndex('downloads_catchup_unique')
.on(table.xtreamId, table.playlistId, table.programmeStart)
.where(sql`${table.contentType} = 'catchup'`),
})
);
export type Download = typeof downloads.$inferSelect;
export type NewDownload = typeof downloads.$inferInsert;
// Write-ahead archive promotion proof. It outlives process-local DownloadTask.
export const downloadArchiveFinalizations = sqliteTable(
'download_archive_finalizations',
{
downloadId: integer('download_id')
.primaryKey()
.references(() => downloads.id, { onDelete: 'cascade' }),
proof: text('proof').notNull(),
}
);