From 31a5763b64d0956fd30db46ed09083dfe83993df Mon Sep 17 00:00:00 2001 From: blomios Date: Thu, 8 Oct 2026 19:39:42 +0200 Subject: [PATCH] fix(live): expose row logos for projected channels Derive live channel row logos from the selected or preferred stream variant so the frontend can render logos without falling back to placeholders. Refs #45 Co-Authored-By: Claude Opus 4.8 --- public/js/services/liveChannelProjection.js | 2 +- server/services/liveProjectionService.js | 12 ++ test/t45LiveLineLogoRegression.test.js | 122 ++++++++++++++++++++ 3 files changed, 135 insertions(+), 1 deletion(-) create mode 100644 test/t45LiveLineLogoRegression.test.js diff --git a/public/js/services/liveChannelProjection.js b/public/js/services/liveChannelProjection.js index 2fc4f55..6f98d24 100644 --- a/public/js/services/liveChannelProjection.js +++ b/public/js/services/liveChannelProjection.js @@ -100,7 +100,7 @@ language, selectedVariantId: logicalChannel.selectedVariantId || `${preferredVariant.sourceId}:${preferredVariant.id}`, groupTitle: preferredVariant.groupTitle, - tvgLogo: preferredVariant.tvgLogo, + tvgLogo: logicalChannel.tvgLogo ?? preferredVariant.tvgLogo, tvgId: logicalChannel.epg?.channelId || preferredVariant.tvgId, isFavorite: mappedVariants.some(variant => variant.isFavorite), isHidden: mappedVariants.every(variant => variant.isHidden) diff --git a/server/services/liveProjectionService.js b/server/services/liveProjectionService.js index d6bd0bc..c97a135 100644 --- a/server/services/liveProjectionService.js +++ b/server/services/liveProjectionService.js @@ -486,6 +486,16 @@ function pageMetadata(total, bounds) { function listChannel(channel) { const preferredVariant = channel.preferredVariant || channel.variants[0] || null; + const variantLogo = variant => variant?.tvgLogo || variant?.streamIcon || variant?.stream_icon || null; + const selectedVariant = channel.variants.find(variant => + `${variant.sourceId}:${variant.id}` === String(channel.selectedVariantId) + ); + const logoVariant = (selectedVariant && variantLogo(selectedVariant) && selectedVariant) + || (preferredVariant && variantLogo(preferredVariant) && preferredVariant) + || channel.variants.find(variant => !variant.isHidden && variantLogo(variant)) + || channel.variants.find(variant => variant.isHidden && variantLogo(variant)) + || null; + const logo = variantLogo(logoVariant); const variants = []; const addPreview = variant => { if (variant && variants.length < 3 && !variants.some(candidate => @@ -498,6 +508,8 @@ function listChannel(channel) { return { ...channel, preferredVariant, + tvgLogo: logo, + streamIcon: logo, variantCount: channel.variants.length, variants }; diff --git a/test/t45LiveLineLogoRegression.test.js b/test/t45LiveLineLogoRegression.test.js new file mode 100644 index 0000000..2b9bbb0 --- /dev/null +++ b/test/t45LiveLineLogoRegression.test.js @@ -0,0 +1,122 @@ +const test = require('node:test'); +const assert = require('node:assert/strict'); +const Database = require('better-sqlite3'); +const liveProjection = require('../server/services/liveProjectionService'); +const { + ensureColumns, + LIVE_PROJECTION_COLUMNS, + LIVE_PROJECTION_SCHEMA_SQL +} = require('../server/db/liveProjectionSchema'); + +function createProjectionDb() { + const db = new Database(':memory:'); + db.exec(` + CREATE TABLE categories ( + id TEXT PRIMARY KEY, + source_id INTEGER NOT NULL, + category_id TEXT NOT NULL, + type TEXT NOT NULL, + name TEXT NOT NULL, + parent_id TEXT, + is_hidden INTEGER DEFAULT 0, + data JSON + ); + CREATE TABLE playlist_items ( + id TEXT PRIMARY KEY, + source_id INTEGER NOT NULL, + item_id TEXT NOT NULL, + type TEXT NOT NULL, + name TEXT NOT NULL, + category_id TEXT, + parent_id TEXT, + stream_icon TEXT, + stream_url TEXT, + container_extension TEXT, + rating REAL, + year TEXT, + added_at TEXT, + is_hidden INTEGER DEFAULT 0, + is_favorite INTEGER DEFAULT 0, + data JSON + ); + CREATE TABLE favorites ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + source_id INTEGER NOT NULL, + item_id TEXT NOT NULL, + item_type TEXT NOT NULL + ); + `); + ensureColumns(db, 'playlist_items', LIVE_PROJECTION_COLUMNS); + db.exec(LIVE_PROJECTION_SCHEMA_SQL); + db.prepare(` + INSERT INTO categories (id, source_id, category_id, type, name) + VALUES ('sports', 2, 'sports', 'live', 'Sports') + `).run(); + return db; +} + +function seedProjectedVariant(db, { itemId, name, icon, rank }) { + db.prepare(` + INSERT INTO playlist_items + (id, source_id, item_id, type, name, category_id, stream_icon, stream_url, data, + logical_channel_id, grouping_fingerprint, grouping_input_hash, quality_label, quality_rank, + variant_rank, language_code, language_confidence, language_detector, language_input_hash) + VALUES (?, 2, ?, 'live', ?, 'sports', ?, ?, ?, 'logical:bein-sports-1', + 'bein sports 1', 'group-hash', 'HD', 2, ?, 'fr', 0.97, 'laya', ?) + `).run( + `2:${itemId}`, + itemId, + name, + icon, + `https://streams.test/${itemId}.m3u8`, + JSON.stringify({ groupTitle: 'Sports', tvgId: `bein1.${itemId}` }), + rank, + `language-${itemId}` + ); +} + +test('projected live channel exposes a row logo from the selected variant stream_icon', () => { + const db = createProjectionDb(); + const preferredLogo = 'http://57.128.182.77:8080/0tv_logo/119.png'; + db.prepare(` + INSERT INTO logical_channels + (id, fingerprint, display_name, preferred_variant_id, variant_count, created_at, updated_at) + VALUES ('logical:bein-sports-1', 'bein sports 1', 'beIN Sports 1', '2:146116', 2, 1, 1) + `).run(); + seedProjectedVariant(db, { + itemId: '146116', + name: 'beIN Sports 1 HD FR', + icon: preferredLogo, + rank: 0 + }); + seedProjectedVariant(db, { + itemId: '146117', + name: 'beIN Sports 1 Backup FR', + icon: 'http://57.128.182.77:8080/0tv_logo/backup.png', + rank: 1 + }); + + try { + const page = liveProjection.getChannelPage({ + q: 'bein sports 1', + languages: ['fr'], + includeHidden: false, + page: 1, + limit: 10 + }, db); + + assert.equal(page.channels.length, 1); + const [channel] = page.channels; + assert.equal(channel.variantCount, 2); + assert.equal(channel.selectedVariantId, '2:146116'); + assert.equal(channel.preferredVariant.streamIcon, preferredLogo); + assert.equal( + channel.tvgLogo || channel.streamIcon, + preferredLogo, + 'ChannelList renders channel.tvgLogo, so the service response needs a row-level logo derived from the preferred variant stream_icon' + ); + } finally { + db.close(); + } +});