mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-15 05:36:17 -08:00
fix(xtream): resolve unlocked favorite category copies
This commit is contained in:
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');
|
||||
|
||||
+66
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -72,7 +72,10 @@ unavailable closes its active detail and invalidates pending selection work,
|
||||
including a retained player during a channel switch. Unresolved records are not
|
||||
promoted to dashboard hero slides. Parental locks still gate display: an
|
||||
unresolved category cannot prove that it is unlocked, so those records remain
|
||||
stored but are withheld while locks apply.
|
||||
stored but are withheld while locks apply. For duplicate category copies, ordinary
|
||||
reads choose an unlocked compatible row before resolving the catalog link; a
|
||||
locked earlier copy cannot hide a later unlocked copy. Backup reads remain
|
||||
unfiltered.
|
||||
|
||||
Legacy pending favorites are converted before destructive refresh, import
|
||||
requests and ordinary collection reads, even if portal-status preflight is
|
||||
|
||||
Reference in new issue
Block a user