fix(xtream): resolve unlocked favorite category copies

This commit is contained in:
4gray committed 2026-10-14 00:40:27 +02:00
1 parent c0edfef616
commit 844aaa5329
6 files changed
+79 -4

No files matched your search

@@ -1,14 +1,16 @@
import { and, eq, sql } from 'drizzle-orm';
import * as schema from '@iptvnator/shared/database/schema';
import type { AppDatabase } from '../database.types';
import { isParentalLockActive } from '../parental-lock-state';
/** A covering provider-id lookup per favorite, never a scan of the catalog. */
export function resolvedFavoriteContentId() {
export function resolvedFavoriteContentId(includeLocked: boolean) {
return sql<number | null>`(SELECT candidate.id FROM content candidate
INNER JOIN categories owner ON owner.id = candidate.category_id
WHERE candidate.xtream_id = ${schema.favorites.xtreamId}
AND candidate.type = ${schema.favorites.type}
AND owner.playlist_id = ${schema.favorites.playlistId}
${!includeLocked && isParentalLockActive() ? sql`AND owner.locked = 0` : sql``}
AND (
(${schema.favorites.tmdbId} IS NULL OR candidate.tmdb_id IS NULL
OR ${schema.favorites.tmdbId} = candidate.tmdb_id)
@@ -76,7 +78,7 @@ export async function reconcileCompletedFavoriteCatalog(
if (!match) return;
db.transaction((tx) => {
tx.update(schema.favorites)
.set({ contentId: resolvedFavoriteContentId() })
.set({ contentId: resolvedFavoriteContentId(true) })
.where(
and(
eq(schema.favorites.playlistId, match[1]),
@@ -52,7 +52,7 @@ export function selectFavoriteRows(
.from(schema.favorites)
.leftJoin(
schema.content,
eq(schema.content.id, resolvedFavoriteContentId())
eq(schema.content.id, resolvedFavoriteContentId(includeLocked))
)
.leftJoin(
schema.categories,
@@ -15,6 +15,7 @@ describe('durable Xtream favorites against initialized SQLite', () => {
'restore-snapshot',
'scope',
'lock',
'lock-duplicate',
'receipt',
])(
'%s preserves the saved favorite independently of catalog rows',
@@ -1,5 +1,6 @@
import assert from 'node:assert/strict';
import { verifyFavoriteSnapshot } from './favorites-snapshot';
import { verifyUnlockedFavoriteDuplicate } from './favorites-locked-duplicate';
import { verifyRestoredFavoriteSnapshot } from './favorites-restore-snapshot';
import { createFavoriteTestCatalog } from './favorites-test-catalog';
import { eq, sql } from 'drizzle-orm';
@@ -275,6 +276,8 @@ async function main() {
await verifyFavoriteSnapshot(db);
} else if (scenario === 'restore-snapshot') {
await verifyRestoredFavoriteSnapshot(db);
} else if (scenario === 'lock-duplicate') {
await verifyUnlockedFavoriteDuplicate(db);
} else if (scenario === 'scope') {
await catalog(2, 7, 'Other', 'movie', 'other');
await addFavorite(db, 2, 'other');
@@ -0,0 +1,66 @@
import assert from 'node:assert/strict';
import { eq } from 'drizzle-orm';
import * as schema from '@iptvnator/shared/database/schema';
import type { AppDatabase } from '../../database.types';
import { setParentalLockActive } from '../../parental-lock-state';
import { createFavoriteTestCatalog } from './favorites-test-catalog';
import {
addFavorite,
getFavorites,
getAllGlobalFavorites,
getGlobalFavorites,
getFavoritesForBackup,
} from '../favorites.operations';
export async function verifyUnlockedFavoriteDuplicate(db: AppDatabase) {
const catalog = createFavoriteTestCatalog(db);
for (const [index, type] of (
['live', 'movie', 'series'] as const
).entries()) {
const first = 10 + index * 10;
const second = first + 1;
const xtreamId = 100 + index;
await catalog(first, xtreamId, 'Locked copy', type);
await catalog(second, xtreamId, 'Unlocked copy', type);
await db
.update(schema.categories)
.set({ locked: true })
.where(eq(schema.categories.id, first));
await addFavorite(db, second, 'p');
setParentalLockActive(true);
const reads = [getFavorites(db, 'p'), getAllGlobalFavorites(db)];
if (type === 'live') reads.push(getGlobalFavorites(db));
for (const rows of await Promise.all(reads)) {
assert.equal(
rows.find((row) => row.xtream_id === xtreamId)?.id,
second,
'an unlocked duplicate must remain visible'
);
}
await db
.update(schema.categories)
.set({ locked: true })
.where(eq(schema.categories.id, second));
assert.equal(
(await getFavorites(db, 'p')).some(
(row) => row.xtream_id === xtreamId
),
false,
'all locked copies must still be withheld'
);
assert.equal(
(await getFavoritesForBackup(db, 'p')).find(
(row) => row.xtream_id === xtreamId
)?.id,
first,
'backup remains unfiltered'
);
setParentalLockActive(false);
assert.equal(
(await getFavorites(db, 'p')).find(
(row) => row.xtream_id === xtreamId
)?.id,
first
);
}
}