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 <noreply@anthropic.com>
This commit is contained in:
@ -100,7 +100,7 @@
|
|||||||
language,
|
language,
|
||||||
selectedVariantId: logicalChannel.selectedVariantId || `${preferredVariant.sourceId}:${preferredVariant.id}`,
|
selectedVariantId: logicalChannel.selectedVariantId || `${preferredVariant.sourceId}:${preferredVariant.id}`,
|
||||||
groupTitle: preferredVariant.groupTitle,
|
groupTitle: preferredVariant.groupTitle,
|
||||||
tvgLogo: preferredVariant.tvgLogo,
|
tvgLogo: logicalChannel.tvgLogo ?? preferredVariant.tvgLogo,
|
||||||
tvgId: logicalChannel.epg?.channelId || preferredVariant.tvgId,
|
tvgId: logicalChannel.epg?.channelId || preferredVariant.tvgId,
|
||||||
isFavorite: mappedVariants.some(variant => variant.isFavorite),
|
isFavorite: mappedVariants.some(variant => variant.isFavorite),
|
||||||
isHidden: mappedVariants.every(variant => variant.isHidden)
|
isHidden: mappedVariants.every(variant => variant.isHidden)
|
||||||
|
|||||||
@ -486,6 +486,16 @@ function pageMetadata(total, bounds) {
|
|||||||
|
|
||||||
function listChannel(channel) {
|
function listChannel(channel) {
|
||||||
const preferredVariant = channel.preferredVariant || channel.variants[0] || null;
|
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 variants = [];
|
||||||
const addPreview = variant => {
|
const addPreview = variant => {
|
||||||
if (variant && variants.length < 3 && !variants.some(candidate =>
|
if (variant && variants.length < 3 && !variants.some(candidate =>
|
||||||
@ -498,6 +508,8 @@ function listChannel(channel) {
|
|||||||
return {
|
return {
|
||||||
...channel,
|
...channel,
|
||||||
preferredVariant,
|
preferredVariant,
|
||||||
|
tvgLogo: logo,
|
||||||
|
streamIcon: logo,
|
||||||
variantCount: channel.variants.length,
|
variantCount: channel.variants.length,
|
||||||
variants
|
variants
|
||||||
};
|
};
|
||||||
|
|||||||
122
test/t45LiveLineLogoRegression.test.js
Normal file
122
test/t45LiveLineLogoRegression.test.js
Normal file
@ -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();
|
||||||
|
}
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user