NuvioStreaming/src/components/player/utils/trackSelectionUtils.ts
2025-12-31 01:05:09 +05:30

232 lines
8.6 KiB
TypeScript

/**
* Track Selection Utilities
* Logic for auto-selecting audio and subtitle tracks based on user preferences
*/
import { AppSettings } from '../../../hooks/useSettings';
import { languageMap } from './playerUtils';
import { WyzieSubtitle } from './playerTypes';
interface Track {
id: number;
name: string;
language?: string;
}
/**
* Normalizes a language code or name to a standard 2-letter ISO code
*/
export const normalizeLanguageCode = (langInput?: string): string => {
if (!langInput) return '';
const normalized = langInput.toLowerCase().trim();
// If it's already a short code that we know, return it
if (languageMap[normalized]) {
// Convert 3-letter codes to 2-letter codes
const twoLetterCodes: { [key: string]: string } = {
'eng': 'en', 'spa': 'es', 'fre': 'fr', 'ger': 'de', 'ita': 'it',
'jpn': 'ja', 'kor': 'ko', 'chi': 'zh', 'rus': 'ru', 'por': 'pt',
'hin': 'hi', 'ara': 'ar', 'dut': 'nl', 'swe': 'sv', 'nor': 'no',
'fin': 'fi', 'dan': 'da', 'pol': 'pl', 'tur': 'tr', 'cze': 'cs',
'hun': 'hu', 'gre': 'el', 'tha': 'th', 'vie': 'vi'
};
return twoLetterCodes[normalized] || normalized;
}
// Check if it's a full language name
for (const [code, name] of Object.entries(languageMap)) {
if (name.toLowerCase() === normalized) {
// Return the 2-letter code
const twoLetterCodes: { [key: string]: string } = {
'eng': 'en', 'spa': 'es', 'fre': 'fr', 'ger': 'de', 'ita': 'it',
'jpn': 'ja', 'kor': 'ko', 'chi': 'zh', 'rus': 'ru', 'por': 'pt',
'hin': 'hi', 'ara': 'ar', 'dut': 'nl', 'swe': 'sv', 'nor': 'no',
'fin': 'fi', 'dan': 'da', 'pol': 'pl', 'tur': 'tr', 'cze': 'cs',
'hun': 'hu', 'gre': 'el', 'tha': 'th', 'vie': 'vi'
};
return twoLetterCodes[code] || code;
}
}
return normalized;
};
/**
* Check if a track matches the preferred language
*/
export const trackMatchesLanguage = (track: Track, preferredLang: string): boolean => {
const trackLang = normalizeLanguageCode(track.language);
const trackNameLower = (track.name || '').toLowerCase();
const prefLang = normalizeLanguageCode(preferredLang);
if (!prefLang) return false;
// Direct language code match
if (trackLang === prefLang) return true;
// Check if the track name contains the language
const langName = languageMap[prefLang] || languageMap[prefLang + 'g']; // handle 'en' -> 'eng' mapping
if (langName && trackNameLower.includes(langName.toLowerCase())) return true;
// Check for common language indicators in track name
const languagePatterns: { [key: string]: RegExp } = {
'en': /\b(english|eng|en)\b/i,
'es': /\b(spanish|spa|es|español|espanol)\b/i,
'fr': /\b(french|fre|fr|français|francais)\b/i,
'de': /\b(german|ger|de|deutsch)\b/i,
'it': /\b(italian|ita|it|italiano)\b/i,
'ja': /\b(japanese|jpn|ja|日本語)\b/i,
'ko': /\b(korean|kor|ko|한국어)\b/i,
'zh': /\b(chinese|chi|zh|中文)\b/i,
'ru': /\b(russian|rus|ru|русский)\b/i,
'pt': /\b(portuguese|por|pt|português)\b/i,
'hi': /\b(hindi|hin|hi|हिन्दी)\b/i,
'ar': /\b(arabic|ara|ar|العربية)\b/i,
};
const pattern = languagePatterns[prefLang];
if (pattern && pattern.test(trackNameLower)) return true;
return false;
};
/**
* Find the best matching audio track based on user preferences
* Returns the track ID to select, or null if no preference match found
*/
export const findBestAudioTrack = (
tracks: Track[],
preferredLanguage: string
): number | null => {
if (!tracks || tracks.length === 0) return null;
// Try to find a track matching the preferred language
const matchingTrack = tracks.find(track => trackMatchesLanguage(track, preferredLanguage));
if (matchingTrack) {
return matchingTrack.id;
}
// No match found - return first track as fallback (or null to use system default)
return null;
};
/**
* Find the best matching subtitle track based on user preferences
* Implements the priority: internal first → external fallback → first available
*
* @param internalTracks - Embedded subtitle tracks from the video
* @param externalSubtitles - Available external/addon subtitles
* @param settings - User's subtitle preferences
* @returns Object with selected track info
*/
export const findBestSubtitleTrack = (
internalTracks: Track[],
externalSubtitles: WyzieSubtitle[],
settings: {
preferredSubtitleLanguage: string;
subtitleSourcePreference: 'internal' | 'external' | 'any';
enableSubtitleAutoSelect: boolean;
}
): {
type: 'internal' | 'external' | 'none';
internalTrackId?: number;
externalSubtitle?: WyzieSubtitle;
} => {
// If auto-select is disabled, don't select anything
if (!settings.enableSubtitleAutoSelect) {
return { type: 'none' };
}
const preferredLang = settings.preferredSubtitleLanguage || 'en';
const sourcePreference = settings.subtitleSourcePreference || 'internal';
// Find matching internal track
const matchingInternalTrack = internalTracks.find(track =>
trackMatchesLanguage(track, preferredLang)
);
// Find matching external subtitle
const matchingExternalSub = externalSubtitles.find(sub => {
const subLang = normalizeLanguageCode(sub.language);
const prefLang = normalizeLanguageCode(preferredLang);
return subLang === prefLang ||
sub.language.toLowerCase().includes(preferredLang.toLowerCase()) ||
sub.display.toLowerCase().includes(languageMap[preferredLang]?.toLowerCase() || preferredLang);
});
// Apply source preference priority
if (sourcePreference === 'internal') {
// 1. Try internal track matching preferred language
if (matchingInternalTrack) {
return { type: 'internal', internalTrackId: matchingInternalTrack.id };
}
// 2. Fallback to external subtitle matching preferred language
if (matchingExternalSub) {
return { type: 'external', externalSubtitle: matchingExternalSub };
}
// 3. Fallback to first internal track if any available
if (internalTracks.length > 0) {
return { type: 'internal', internalTrackId: internalTracks[0].id };
}
// 4. Fallback to first external subtitle if any available
if (externalSubtitles.length > 0) {
return { type: 'external', externalSubtitle: externalSubtitles[0] };
}
} else if (sourcePreference === 'external') {
// 1. Try external subtitle matching preferred language
if (matchingExternalSub) {
return { type: 'external', externalSubtitle: matchingExternalSub };
}
// 2. Fallback to internal track matching preferred language
if (matchingInternalTrack) {
return { type: 'internal', internalTrackId: matchingInternalTrack.id };
}
// 3. Fallback to first external subtitle if any available
if (externalSubtitles.length > 0) {
return { type: 'external', externalSubtitle: externalSubtitles[0] };
}
// 4. Fallback to first internal track if any available
if (internalTracks.length > 0) {
return { type: 'internal', internalTrackId: internalTracks[0].id };
}
} else {
// 'any' - prefer matching language regardless of source, internal first
if (matchingInternalTrack) {
return { type: 'internal', internalTrackId: matchingInternalTrack.id };
}
if (matchingExternalSub) {
return { type: 'external', externalSubtitle: matchingExternalSub };
}
// Fallback to first available
if (internalTracks.length > 0) {
return { type: 'internal', internalTrackId: internalTracks[0].id };
}
if (externalSubtitles.length > 0) {
return { type: 'external', externalSubtitle: externalSubtitles[0] };
}
}
return { type: 'none' };
};
/**
* Find best audio track from available tracks
*/
export const autoSelectAudioTrack = (
tracks: Track[],
preferredLanguage: string
): number | null => {
if (!tracks || tracks.length === 0) return null;
// Try to find a track matching the preferred language
const matchingTrack = tracks.find(track => trackMatchesLanguage(track, preferredLanguage));
if (matchingTrack) {
return matchingTrack.id;
}
// Return null to let the player use its default
return null;
};