mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-10 18:36:15 -08:00
fix(downloads): complete metadata snapshot bridge contract
This commit is contained in:
1 parent
664d333f0f
commit
a3d774f05a
3 files changed
+141
-51
No files matched your search
@@ -13,7 +13,10 @@ import {
|
||||
ANNOUNCE_PLAYLIST_OPEN_LISTENER,
|
||||
OPEN_FILE,
|
||||
} from '@iptvnator/shared/interfaces';
|
||||
import type { ElectronBridgeApi } from '@iptvnator/shared/interfaces';
|
||||
import type {
|
||||
DownloadMetadataSnapshot,
|
||||
ElectronBridgeApi,
|
||||
} from '@iptvnator/shared/interfaces';
|
||||
import {
|
||||
dbPreloadCases,
|
||||
epgPreloadCases,
|
||||
@@ -278,6 +281,24 @@ describe('main preload DB IPC contract', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('forwards download metadata updates with the download id', async () => {
|
||||
const api = getExposedApi();
|
||||
const metadataSnapshot: DownloadMetadataSnapshot = {
|
||||
version: 1,
|
||||
language: 'en',
|
||||
mediaKind: 'movie',
|
||||
title: 'Offline title',
|
||||
};
|
||||
|
||||
await api.downloadsUpdateMetadata(42, metadataSnapshot);
|
||||
|
||||
expect(mockIpcRenderer.invoke).toHaveBeenLastCalledWith(
|
||||
'DOWNLOADS_UPDATE_METADATA',
|
||||
42,
|
||||
metadataSnapshot
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps the legacy save-content progress bridge scoped to progress events', () => {
|
||||
const api = getExposedApi();
|
||||
const callback = jest.fn();
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
toXtreamPreloadPerformanceTargetMethod,
|
||||
} from './xtream-preload-performance-capture';
|
||||
import type {
|
||||
DownloadMetadataSnapshot,
|
||||
EmbeddedMpvBounds,
|
||||
EmbeddedMpvRecordingStartOptions,
|
||||
EmbeddedMpvSession,
|
||||
@@ -986,6 +987,15 @@ const electronApi: ElectronBridgeApi = {
|
||||
ipcRenderer.invoke('DOWNLOADS_GET_LIST', playlistId),
|
||||
downloadsGet: (downloadId: number) =>
|
||||
ipcRenderer.invoke('DOWNLOADS_GET', downloadId),
|
||||
downloadsUpdateMetadata: (
|
||||
downloadId: number,
|
||||
metadataSnapshot: DownloadMetadataSnapshot
|
||||
) =>
|
||||
ipcRenderer.invoke(
|
||||
'DOWNLOADS_UPDATE_METADATA',
|
||||
downloadId,
|
||||
metadataSnapshot
|
||||
),
|
||||
downloadsGetDefaultFolder: () =>
|
||||
ipcRenderer.invoke('DOWNLOADS_GET_DEFAULT_FOLDER'),
|
||||
downloadsSelectFolder: () => ipcRenderer.invoke('DOWNLOADS_SELECT_FOLDER'),
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { createRequire } from 'node:module';
|
||||
import { resolve } from 'node:path';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
import { getTableColumns } from 'drizzle-orm';
|
||||
import { __databaseConnectionTestHooks } from './connection';
|
||||
import { downloads } from './schema';
|
||||
@@ -19,6 +23,105 @@ function createdObjectNames(prefix: string, statements: readonly string[]) {
|
||||
.filter((name): name is string => Boolean(name));
|
||||
}
|
||||
|
||||
function rebuildDownloadsInElectron(metadataSnapshot: string): {
|
||||
metadataSnapshot: string;
|
||||
schemaSql: string;
|
||||
} {
|
||||
const electronPath = createRequire(__filename)('electron') as string;
|
||||
const connectionUrl = pathToFileURL(
|
||||
resolve(__dirname, 'connection.ts')
|
||||
).href;
|
||||
const script = `
|
||||
const { default: Database } = await import('better-sqlite3');
|
||||
const { __databaseConnectionTestHooks } = await import(${JSON.stringify(
|
||||
connectionUrl
|
||||
)});
|
||||
const metadataSnapshot = ${JSON.stringify(metadataSnapshot)};
|
||||
const sqlite = new Database(':memory:');
|
||||
sqlite.exec(\`
|
||||
CREATE TABLE playlists (id TEXT PRIMARY KEY);
|
||||
CREATE TABLE downloads (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
playlist_id TEXT NOT NULL,
|
||||
xtream_id INTEGER NOT NULL,
|
||||
content_type TEXT NOT NULL,
|
||||
series_xtream_id INTEGER,
|
||||
season_number INTEGER,
|
||||
episode_number INTEGER,
|
||||
title TEXT NOT NULL,
|
||||
url TEXT NOT NULL,
|
||||
file_name TEXT,
|
||||
file_path TEXT,
|
||||
poster_url TEXT,
|
||||
request_headers TEXT,
|
||||
metadata_snapshot TEXT,
|
||||
status TEXT NOT NULL DEFAULT 'queued'
|
||||
CHECK (status IN (
|
||||
'queued',
|
||||
'downloading',
|
||||
'completed',
|
||||
'failed',
|
||||
'canceled'
|
||||
)),
|
||||
bytes_downloaded INTEGER,
|
||||
total_bytes INTEGER,
|
||||
error_message TEXT,
|
||||
created_at TEXT,
|
||||
updated_at TEXT
|
||||
);
|
||||
\`);
|
||||
sqlite.prepare('INSERT INTO playlists (id) VALUES (?)')
|
||||
.run('playlist-1');
|
||||
sqlite.prepare(\`
|
||||
INSERT INTO downloads (
|
||||
playlist_id,
|
||||
xtream_id,
|
||||
content_type,
|
||||
title,
|
||||
url,
|
||||
metadata_snapshot
|
||||
) VALUES (?, ?, ?, ?, ?, ?)
|
||||
\`).run(
|
||||
'playlist-1',
|
||||
42,
|
||||
'vod',
|
||||
'Offline title',
|
||||
'https://example.com/movie',
|
||||
metadataSnapshot
|
||||
);
|
||||
console.log = () => undefined;
|
||||
__databaseConnectionTestHooks.ensureDownloadsPauseResumeSchema(sqlite);
|
||||
const row = sqlite.prepare(
|
||||
'SELECT metadata_snapshot FROM downloads WHERE id = 1'
|
||||
).get();
|
||||
const table = sqlite.prepare(
|
||||
"SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'downloads'"
|
||||
).get();
|
||||
sqlite.close();
|
||||
process.stdout.write(JSON.stringify({
|
||||
metadataSnapshot: row.metadata_snapshot,
|
||||
schemaSql: table.sql,
|
||||
}));
|
||||
`;
|
||||
const output = execFileSync(
|
||||
electronPath,
|
||||
['--import', 'tsx', '--eval', script],
|
||||
{
|
||||
cwd: process.cwd(),
|
||||
encoding: 'utf8',
|
||||
env: {
|
||||
...process.env,
|
||||
ELECTRON_RUN_AS_NODE: '1',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return JSON.parse(output) as {
|
||||
metadataSnapshot: string;
|
||||
schemaSql: string;
|
||||
};
|
||||
}
|
||||
|
||||
describe('database schema statements', () => {
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
@@ -63,12 +166,8 @@ describe('database schema statements', () => {
|
||||
}
|
||||
});
|
||||
|
||||
function createRebuildSqlite(
|
||||
legacyTableSql: string | undefined,
|
||||
legacyMetadataSnapshot?: string
|
||||
) {
|
||||
function createRebuildSqlite(legacyTableSql: string | undefined) {
|
||||
const statements: string[] = [];
|
||||
let rebuiltMetadataSnapshot: string | undefined;
|
||||
const transaction = jest.fn((callback: () => void) => callback);
|
||||
const prepare = jest.fn((statement: string) => {
|
||||
if (statement.includes('FROM sqlite_master')) {
|
||||
@@ -81,29 +180,12 @@ describe('database schema statements', () => {
|
||||
}
|
||||
return {
|
||||
run: () => {
|
||||
const compactStatement = compactSql(statement);
|
||||
statements.push(compactStatement);
|
||||
if (
|
||||
compactStatement.startsWith(
|
||||
'INSERT INTO downloads'
|
||||
) &&
|
||||
compactStatement.includes('metadata_snapshot') &&
|
||||
!compactStatement.includes(
|
||||
'NULL AS metadata_snapshot'
|
||||
)
|
||||
) {
|
||||
rebuiltMetadataSnapshot = legacyMetadataSnapshot;
|
||||
}
|
||||
statements.push(compactSql(statement));
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
getRebuiltMetadataSnapshot: () => rebuiltMetadataSnapshot,
|
||||
prepare,
|
||||
statements,
|
||||
transaction,
|
||||
};
|
||||
return { prepare, statements, transaction };
|
||||
}
|
||||
|
||||
it('defines the core fresh-install tables, indexes, and FTS triggers', () => {
|
||||
@@ -547,33 +629,10 @@ describe('database schema statements', () => {
|
||||
mediaKind: 'movie',
|
||||
title: 'Offline title',
|
||||
});
|
||||
const sqlite = createRebuildSqlite(
|
||||
`CREATE TABLE downloads (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
request_headers TEXT,
|
||||
metadata_snapshot TEXT,
|
||||
status TEXT CHECK (
|
||||
status IN (
|
||||
'queued',
|
||||
'downloading',
|
||||
'completed',
|
||||
'failed',
|
||||
'canceled'
|
||||
)
|
||||
)
|
||||
)`,
|
||||
metadataSnapshot
|
||||
);
|
||||
const rebuilt = rebuildDownloadsInElectron(metadataSnapshot);
|
||||
|
||||
ensureDownloadsPauseResumeSchema(sqlite);
|
||||
|
||||
const copy = sqlite.statements.find((statement) =>
|
||||
statement.startsWith('INSERT INTO downloads')
|
||||
);
|
||||
expect(copy).toBeDefined();
|
||||
expect(copy).toContain('metadata_snapshot');
|
||||
expect(copy).not.toContain('NULL AS metadata_snapshot');
|
||||
expect(sqlite.getRebuiltMetadataSnapshot()).toBe(metadataSnapshot);
|
||||
expect(rebuilt.schemaSql).toContain(`'paused'`);
|
||||
expect(rebuilt.metadataSnapshot).toBe(metadataSnapshot);
|
||||
});
|
||||
|
||||
it('skips the downloads rebuild when the table already has the paused contract', () => {
|
||||
|
||||
Reference in new issue
Block a user