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:
@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user