mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-14 21:26:15 -08:00
fix(xtream): confirm saved identity before restoring metadata
This commit is contained in:
1 parent
e5a5d0581d
commit
2a8009fe6c
5 files changed
+88
-7
No files matched your search
@@ -80,6 +80,15 @@ function favoriteValue(
|
||||
candidate.releaseYear != null &&
|
||||
metadata.releaseYear !== candidate.releaseYear));
|
||||
const content = conflicts ? undefined : candidate;
|
||||
// Keep a compatible provider link, but do not mix a saved identity with
|
||||
// metadata from a returning row that has not confirmed it yet.
|
||||
const snapshotContent =
|
||||
content &&
|
||||
(metadata?.tmdbId == null || metadata.tmdbId === content.tmdbId) &&
|
||||
(metadata?.releaseYear == null ||
|
||||
metadata.releaseYear === content.releaseYear)
|
||||
? content
|
||||
: undefined;
|
||||
return {
|
||||
playlistId,
|
||||
xtreamId: item.xtreamId,
|
||||
@@ -87,13 +96,14 @@ function favoriteValue(
|
||||
contentId: content?.id ?? null,
|
||||
title:
|
||||
item.snapshot?.title?.trim() ||
|
||||
content?.title ||
|
||||
snapshotContent?.title ||
|
||||
`Saved ${item.contentType} #${item.xtreamId}`,
|
||||
posterUrl: item.snapshot?.posterUrl ?? content?.posterUrl,
|
||||
backdropUrl: metadata?.backdropUrl ?? content?.backdropUrl,
|
||||
tmdbId: metadata?.tmdbId ?? content?.tmdbId,
|
||||
releaseYear: metadata?.releaseYear ?? content?.releaseYear,
|
||||
originalTitle: metadata?.originalTitle ?? content?.originalTitle,
|
||||
posterUrl: item.snapshot?.posterUrl ?? snapshotContent?.posterUrl,
|
||||
backdropUrl: metadata?.backdropUrl ?? snapshotContent?.backdropUrl,
|
||||
tmdbId: metadata?.tmdbId ?? snapshotContent?.tmdbId,
|
||||
releaseYear: metadata?.releaseYear ?? snapshotContent?.releaseYear,
|
||||
originalTitle:
|
||||
metadata?.originalTitle ?? snapshotContent?.originalTitle,
|
||||
addedAt: item.addedAt ?? new Date().toISOString(),
|
||||
position: item.position ?? index,
|
||||
};
|
||||
|
||||
@@ -12,6 +12,7 @@ describe('durable Xtream favorites against initialized SQLite', () => {
|
||||
'identity',
|
||||
'collision',
|
||||
'snapshot',
|
||||
'restore-snapshot',
|
||||
'scope',
|
||||
'lock',
|
||||
'receipt',
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { verifyFavoriteSnapshot } from './favorites-snapshot';
|
||||
import { verifyRestoredFavoriteSnapshot } from './favorites-restore-snapshot';
|
||||
import { createFavoriteTestCatalog } from './favorites-test-catalog';
|
||||
import { eq, sql } from 'drizzle-orm';
|
||||
import { closeDatabase, initDatabase } from '@iptvnator/shared/database';
|
||||
@@ -272,6 +273,8 @@ async function main() {
|
||||
assert.equal((await favorites())[0]?.id, 4);
|
||||
} else if (scenario === 'snapshot') {
|
||||
await verifyFavoriteSnapshot(db);
|
||||
} else if (scenario === 'restore-snapshot') {
|
||||
await verifyRestoredFavoriteSnapshot(db);
|
||||
} else if (scenario === 'scope') {
|
||||
await catalog(2, 7, 'Other', 'movie', 'other');
|
||||
await addFavorite(db, 2, 'other');
|
||||
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import * as schema from '@iptvnator/shared/database/schema';
|
||||
import type { AppDatabase } from '../../database.types';
|
||||
import { createFavoriteTestCatalog } from './favorites-test-catalog';
|
||||
import { getFavoritesForBackup } from '../favorites.operations';
|
||||
import {
|
||||
deleteXtreamContent,
|
||||
restoreXtreamFavorites,
|
||||
} from '../xtream.operations';
|
||||
import { setAppState } from '../playlist.operations';
|
||||
|
||||
export async function verifyRestoredFavoriteSnapshot(db: AppDatabase) {
|
||||
const catalog = createFavoriteTestCatalog(db);
|
||||
const cases = [
|
||||
{ saved: { tmdbId: 10 }, candidate: { releaseYear: 2020 } },
|
||||
{ saved: { releaseYear: 2000 }, candidate: { tmdbId: 20 } },
|
||||
];
|
||||
for (const [index, { saved, candidate }] of cases.entries()) {
|
||||
const id = 20 + index * 2;
|
||||
const xtreamId = 100 + index;
|
||||
await catalog(id, xtreamId, 'Replacement', 'movie', 'p', candidate);
|
||||
await restoreXtreamFavorites(db, 'p', [
|
||||
{
|
||||
contentType: 'movie',
|
||||
xtreamId,
|
||||
snapshot: { title: 'Saved original', ...saved },
|
||||
},
|
||||
]);
|
||||
const restored = (await db.select().from(schema.favorites)).find(
|
||||
(row) => row.xtreamId === xtreamId
|
||||
);
|
||||
assert(restored);
|
||||
assert.equal(
|
||||
restored.tmdbId,
|
||||
saved.tmdbId ?? null,
|
||||
'restore must not adopt an unconfirmed TMDB ID'
|
||||
);
|
||||
assert.equal(
|
||||
restored.releaseYear,
|
||||
saved.releaseYear ?? null,
|
||||
'restore must not adopt an unconfirmed year'
|
||||
);
|
||||
assert.equal(
|
||||
restored.posterUrl,
|
||||
null,
|
||||
'restore must not copy replacement artwork'
|
||||
);
|
||||
await deleteXtreamContent(db, 'p');
|
||||
await catalog(id + 1, xtreamId, 'Original returns', 'movie', 'p', {
|
||||
tmdbId: 10,
|
||||
releaseYear: 2000,
|
||||
});
|
||||
await setAppState(db, 'xtream-import-status:p:movie', 'completed');
|
||||
const returned = (await getFavoritesForBackup(db, 'p')).find(
|
||||
(row) => row.xtream_id === xtreamId
|
||||
);
|
||||
assert.equal(
|
||||
returned?.catalog_status,
|
||||
'present',
|
||||
'incomplete restored metadata must not block original content reconnecting'
|
||||
);
|
||||
assert.equal(returned?.favorite_id, restored.id);
|
||||
assert.equal(returned?.title, 'Original returns');
|
||||
}
|
||||
}
|
||||
@@ -52,7 +52,9 @@ catalog ID. Remove/reorder can use the stable favorite ID while detail toggles
|
||||
resolve any category row through the same typed provider identity. Completed
|
||||
imports reconnect valid identities. Display snapshots are replaced only after
|
||||
the returning row confirms every saved TMDB ID/year; incomplete identity keeps
|
||||
the saved title/artwork in collections and backups. Metadata learned in detail
|
||||
the saved title/artwork in collections and backups. Restore applies the same
|
||||
confirmation guard before filling gaps in saved metadata from catalog rows.
|
||||
Metadata learned in detail
|
||||
views is copied into the saved snapshot before catalog loss.
|
||||
Conflicting known TMDB IDs or release years prevent automatic reconnection;
|
||||
changed provider IDs never reconnect by title.
|
||||
|
||||
Reference in new issue
Block a user