mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-03-20 09:47:31 +00:00
232 lines
8.6 KiB
TypeScript
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;
|
|
};
|