mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-14 13:16:16 -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
85 lines
3.1 KiB
TypeScript
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(),
|
|
}
|
|
);
|