feat: detect variant language with confidence scoring and player language selector (#10)

Add a channelLanguageDetection service that scores language evidence
per IPTV variant (country/language prefixes in name and group, source
metadata, original title words) and picks the highest-scored language.
Integrate detected languages into channel variant grouping and add a
language selector to the live player to switch to a variant of the
logical channel in the desired language, keeping raw streams
non-destructive.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
blomios
2026-10-02 09:33:28 +02:00
parent b8849aedda
commit b95a408071
7 changed files with 342 additions and 17 deletions

View File

@ -35,6 +35,7 @@ class VideoPlayer {
this.qualitySwitchInProgress = false;
this.originalStreamUrl = null;
this.sourceStreamInfo = null;
this.currentLogicalChannel = null;
// Settings - start with defaults, load from server async
this.settings = this.getDefaultSettings();
@ -365,6 +366,7 @@ class VideoPlayer {
const overflowMenu = document.getElementById('player-overflow-menu');
this.initQualityControl(overflowMenu);
this.initLanguageControl(overflowMenu);
btnOverflow?.addEventListener('click', (e) => {
e.stopPropagation();
@ -465,6 +467,49 @@ class VideoPlayer {
this.renderQualityControl();
}
initLanguageControl(overflowMenu) {
if (!overflowMenu || document.getElementById('player-language-control')) return;
const control = document.createElement('div');
control.id = 'player-language-control';
control.hidden = true;
control.setAttribute('role', 'group');
control.setAttribute('aria-label', 'Language');
control.style.borderTop = '1px solid var(--glass-border)';
control.innerHTML = `
<div id="player-language-label" style="padding: .55rem 1rem .25rem; font-size: .75rem; text-transform: uppercase; color: var(--color-text-muted);">Language</div>
<div id="player-language-options" role="menu" aria-labelledby="player-language-label"></div>`;
overflowMenu.append(control);
}
updateLanguageControl(logicalChannel) {
this.currentLogicalChannel = logicalChannel;
const control = document.getElementById('player-language-control');
const list = document.getElementById('player-language-options');
if (!control || !list) return;
const languages = logicalChannel?.availableLanguages || [];
control.hidden = languages.length < 2;
list.innerHTML = '';
if (control.hidden) return;
const channelList = window.app?.channelList;
const selected = channelList?.channelLanguagePreferences?.get(logicalChannel.fingerprint) ||
channelList?.preferredLanguage || 'auto';
[{ code: 'auto', label: 'Auto' }, ...languages].forEach(language => {
const button = document.createElement('button');
button.type = 'button';
button.className = 'overflow-menu-item';
button.setAttribute('role', 'menuitemradio');
button.setAttribute('aria-checked', String(selected === language.code));
button.style.justifyContent = 'space-between';
button.innerHTML = `<span>${language.label}</span><span aria-hidden="true">${selected === language.code ? '✓' : ''}</span>`;
button.addEventListener('click', async event => {
event.stopPropagation();
await window.app?.channelList?.selectLanguageForCurrent(language.code);
});
list.appendChild(button);
});
}
qualityLabel(mode) {
return ({ auto: 'Auto', source: 'Originale', '1080p': '1080p', '720p': '720p', '480p': '480p' })[mode] || mode;
}