mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
feat(playback): theater stage and opt-in ambient fill for the inline portal player (#1223)
* fix(tmdb): purge obsolete search cache rows * feat(playback): theater stage and opt-in ambient fill for the inline portal player On wide-short windows the VOD/series inline player left a strip of app surface next to the video: with `width: auto`, the viewport's `max-height` transferred through `aspect-ratio` into a max-width (CSS transferred size constraints), re-clamping the stage to 16:9 and leaving the leftover outside it. - Theater stage: give `.player-shell__viewport` a definite `width: 100%` so it always fills the content row; the player renders as the largest 16:9 box that fits the stage height, centered — the leftover is always the stage's black background, never app surface (YouTube-style letterbox). Applies to every inline engine. - Ambient fill: new `playerAmbientMode` setting (default off, Settings > Playback, web players only) renders a blurred, dimmed copy of the poster behind the player, filling the letterbox margins. Enforced at runtime too: Embedded MPV never gets the extra DOM layer. Live channels and non-http(s) poster URLs are excluded. Verified live via CDP at 1720x760 (stage 1362x532, player 946x532 with symmetric 208px margins) and 1280x950 (stage exactly 16:9, no bars). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(i18n): add ambient-mode setting keys to all remaining locales The i18n drift gate requires SETTINGS.PLAYER_AMBIENT_MODE and its description in every locale; the feature commit only covered en and ru. Translated via the i18n-fill workflow (per-locale patch + mechanical merge, glossary-matched against each existing file). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(settings): include playerAmbientMode in expected default settings settings.component.spec asserts the persisted settings object with toEqual; the new default-off field has to be part of the fixture. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
b3e130aa65
commit
db70b07093
34 files changed
+426
-23
No files matched your search
@@ -6,6 +6,7 @@ const {
|
||||
createTableStatements,
|
||||
indexMigrationStatements,
|
||||
runMigrations,
|
||||
cleanupLegacyTmdbSearchCache,
|
||||
} = __databaseConnectionTestHooks;
|
||||
|
||||
type SqliteHandle = Parameters<typeof runMigrations>[0];
|
||||
@@ -22,10 +23,7 @@ type StatementHandler = {
|
||||
|
||||
type HandlerRule = [pattern: string, handler: StatementHandler];
|
||||
|
||||
function createSqliteMock(
|
||||
rules: HandlerRule[],
|
||||
exec: jest.Mock = jest.fn()
|
||||
) {
|
||||
function createSqliteMock(rules: HandlerRule[], exec: jest.Mock = jest.fn()) {
|
||||
const prepare = jest.fn((statement: string) => {
|
||||
const compact = compactSql(statement);
|
||||
const rule = rules.find(([pattern]) => compact.includes(pattern));
|
||||
@@ -129,6 +127,45 @@ describe('runMigrations error tolerance', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('TMDB search lookup v2 cache cleanup', () => {
|
||||
it('deletes legacy search rows and records the migration atomically', () => {
|
||||
const deleteRun = jest.fn();
|
||||
const markerRun = jest.fn();
|
||||
const { sqlite, transaction } = createSqliteMock([
|
||||
['SELECT value FROM app_state', { get: () => undefined }],
|
||||
['DELETE FROM tmdb_metadata', { run: deleteRun }],
|
||||
['INSERT INTO app_state', { run: markerRun }],
|
||||
]);
|
||||
|
||||
cleanupLegacyTmdbSearchCache(sqlite);
|
||||
|
||||
expect(transaction).toHaveBeenCalledTimes(1);
|
||||
expect(deleteRun).toHaveBeenCalledTimes(1);
|
||||
expect(markerRun).toHaveBeenCalledWith(
|
||||
'migration:tmdb-search-lookup-v2-cache-cleanup:v1'
|
||||
);
|
||||
const deleteSql = compactSql(
|
||||
(sqlite.prepare as jest.Mock).mock.calls.find(([statement]) =>
|
||||
statement.includes('DELETE FROM tmdb_metadata')
|
||||
)?.[0]
|
||||
);
|
||||
expect(deleteSql).toContain(
|
||||
"lookup_key LIKE 'title:%|year:%' AND lookup_key NOT LIKE 'title:%|year:%|v%'"
|
||||
);
|
||||
});
|
||||
|
||||
it('does nothing after the migration has completed', () => {
|
||||
const { sqlite, prepare, transaction } = createSqliteMock([
|
||||
completedMigrationStateRule,
|
||||
]);
|
||||
|
||||
cleanupLegacyTmdbSearchCache(sqlite);
|
||||
|
||||
expect(transaction).not.toHaveBeenCalled();
|
||||
expect(prepare).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('runMigrations Xtream cache deduplication', () => {
|
||||
it('re-points content to the canonical duplicate category before deleting the rest', () => {
|
||||
const candidatesAll = jest.fn(() => [
|
||||
@@ -141,7 +178,11 @@ describe('runMigrations Xtream cache deduplication', () => {
|
||||
completedMigrationStateRule,
|
||||
[
|
||||
'FROM categories GROUP BY playlist_id, type, xtream_id',
|
||||
{ all: () => [{ playlistId: 'p1', type: 'live', xtreamId: 5 }] },
|
||||
{
|
||||
all: () => [
|
||||
{ playlistId: 'p1', type: 'live', xtreamId: 5 },
|
||||
],
|
||||
},
|
||||
],
|
||||
['LEFT JOIN content', { all: candidatesAll }],
|
||||
[
|
||||
@@ -149,7 +190,10 @@ describe('runMigrations Xtream cache deduplication', () => {
|
||||
{ run: updateContentRun },
|
||||
],
|
||||
['DELETE FROM categories WHERE id = ?', { run: deleteCategoryRun }],
|
||||
['FROM content GROUP BY category_id, type, xtream_id', { all: () => [] }],
|
||||
[
|
||||
'FROM content GROUP BY category_id, type, xtream_id',
|
||||
{ all: () => [] },
|
||||
],
|
||||
]);
|
||||
|
||||
runMigrations(sqlite);
|
||||
|
||||
@@ -30,6 +30,8 @@ const CONTENT_TITLE_FTS_MIGRATION_KEY =
|
||||
'migration:content-title-fts-trigram:v1';
|
||||
const EPG_PROGRAM_SOURCE_URL_BACKFILL_MIGRATION_KEY =
|
||||
'migration:epg-program-source-url-backfill:v1';
|
||||
const TMDB_SEARCH_LOOKUP_V2_CACHE_CLEANUP_MIGRATION_KEY =
|
||||
'migration:tmdb-search-lookup-v2-cache-cleanup:v1';
|
||||
const EPG_PROGRAM_SOURCE_URL_BACKFILL_BATCH_SIZE = 50_000;
|
||||
|
||||
function readTraceFlag(name: string): boolean {
|
||||
@@ -373,6 +375,7 @@ export const __databaseConnectionTestHooks = {
|
||||
normalizeXtreamContentAddedEpochs,
|
||||
ensureContentTitleFts,
|
||||
backfillEpgProgramSourceUrls,
|
||||
cleanupLegacyTmdbSearchCache,
|
||||
runMigrations,
|
||||
} as const;
|
||||
|
||||
@@ -768,11 +771,61 @@ function widenTmdbMetadataMediaTypeCheck(sqliteDb: Database.Database): void {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Search-match cache keys gained a v2 suffix when title normalization changed.
|
||||
* Remove the now-unreachable unversioned rows once rather than leaving negative
|
||||
* resolutions and other legacy search matches in long-lived installations.
|
||||
*/
|
||||
function cleanupLegacyTmdbSearchCache(sqliteDb: Database.Database): void {
|
||||
try {
|
||||
const migrationState = sqliteDb
|
||||
.prepare(`SELECT value FROM app_state WHERE key = ?`)
|
||||
.get(TMDB_SEARCH_LOOKUP_V2_CACHE_CLEANUP_MIGRATION_KEY) as
|
||||
| { value?: unknown }
|
||||
| undefined;
|
||||
|
||||
if (migrationState?.value === 'done') {
|
||||
return;
|
||||
}
|
||||
|
||||
const executeCleanup = sqliteDb.transaction(() => {
|
||||
sqliteDb
|
||||
.prepare(
|
||||
`DELETE FROM tmdb_metadata
|
||||
WHERE lookup_key LIKE 'title:%|year:%'
|
||||
AND lookup_key NOT LIKE 'title:%|year:%|v%'`
|
||||
)
|
||||
.run();
|
||||
sqliteDb
|
||||
.prepare(
|
||||
`INSERT INTO app_state (key, value, updated_at)
|
||||
VALUES (?, 'done', datetime('now'))
|
||||
ON CONFLICT(key) DO UPDATE SET
|
||||
value = excluded.value,
|
||||
updated_at = excluded.updated_at`
|
||||
)
|
||||
.run(TMDB_SEARCH_LOOKUP_V2_CACHE_CLEANUP_MIGRATION_KEY);
|
||||
});
|
||||
|
||||
executeCleanup();
|
||||
} catch (error) {
|
||||
const message =
|
||||
typeof error === 'object' && error !== null && 'message' in error
|
||||
? String((error as { message?: unknown }).message ?? error)
|
||||
: String(error);
|
||||
|
||||
console.warn(
|
||||
`Legacy TMDB search cache cleanup failed (continuing): ${message}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run migrations that may fail if already applied
|
||||
*/
|
||||
function runMigrations(sqliteDb: Database.Database): void {
|
||||
widenTmdbMetadataMediaTypeCheck(sqliteDb);
|
||||
cleanupLegacyTmdbSearchCache(sqliteDb);
|
||||
runMigrationStatements(sqliteDb, COLUMN_MIGRATION_STATEMENTS);
|
||||
ensureContentTitleFts(sqliteDb);
|
||||
deduplicateXtreamCache(sqliteDb);
|
||||
|
||||
Reference in new issue
Block a user