2026-05-22 20:26:11 -04:00
|
|
|
package playback
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"strings"
|
|
|
|
|
|
|
|
|
|
"github.com/Silo-Server/silo-server/internal/lang"
|
|
|
|
|
"github.com/Silo-Server/silo-server/internal/models"
|
|
|
|
|
"github.com/Silo-Server/silo-server/internal/userstore"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// OriginalLanguageSentinel is the value stored in audio language preference
|
|
|
|
|
// columns to mean "use the media item's original language." It is resolved
|
|
|
|
|
// to a concrete language code in the playback handler before reaching
|
|
|
|
|
// SelectAudioTrack.
|
|
|
|
|
const OriginalLanguageSentinel = "original"
|
|
|
|
|
|
|
|
|
|
// AudioTrackPreference holds a per-series audio track preference.
|
|
|
|
|
type AudioTrackPreference struct {
|
|
|
|
|
AudioTrackIndex int
|
|
|
|
|
AudioLanguage string
|
|
|
|
|
TrackSignature *userstore.AudioTrackSignature
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func langMatch(a, b string) bool {
|
|
|
|
|
if a == "" || b == "" {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
return lang.Canonical(a) == lang.Canonical(b)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// SelectAudioTrack determines which audio track to use based on preferences.
|
|
|
|
|
//
|
|
|
|
|
// Priority:
|
|
|
|
|
// 1. Series preference exact track signature
|
|
|
|
|
// 2. Series preference index (if track exists at that index with matching language)
|
|
|
|
|
// 3. Series preference language (first track matching that language)
|
|
|
|
|
// 4. Profile preferred language (first track matching)
|
|
|
|
|
// 5. File's default track (first track with Default: true)
|
|
|
|
|
// 6. First track (index 0)
|
|
|
|
|
func SelectAudioTrack(tracks []models.AudioTrack, preferredLang string, seriesPref *AudioTrackPreference) int {
|
|
|
|
|
if len(tracks) == 0 {
|
|
|
|
|
return 0
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 1. Series preference: try exact signature match first.
|
|
|
|
|
if seriesPref != nil {
|
|
|
|
|
if idx := findExactAudioTrack(tracks, seriesPref.TrackSignature); idx >= 0 {
|
|
|
|
|
return idx
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 2. Series preference: try exact index+language match.
|
|
|
|
|
if seriesPref.AudioTrackIndex >= 0 && seriesPref.AudioTrackIndex < len(tracks) {
|
|
|
|
|
if langMatch(tracks[seriesPref.AudioTrackIndex].Language, seriesPref.AudioLanguage) {
|
|
|
|
|
return seriesPref.AudioTrackIndex
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 3. Series preference: fall back to language match.
|
|
|
|
|
if seriesPref.AudioLanguage != "" {
|
|
|
|
|
for i, t := range tracks {
|
|
|
|
|
if langMatch(t.Language, seriesPref.AudioLanguage) {
|
|
|
|
|
return i
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 4. Profile language preference.
|
|
|
|
|
if preferredLang != "" {
|
|
|
|
|
for i, t := range tracks {
|
|
|
|
|
if langMatch(t.Language, preferredLang) {
|
|
|
|
|
return i
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 5. File's default track.
|
|
|
|
|
for i, t := range tracks {
|
|
|
|
|
if t.Default {
|
|
|
|
|
return i
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 6. First track.
|
|
|
|
|
return 0
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-10 05:21:26 -07:00
|
|
|
// MatchAudioTrackAcrossVersions maps a selection made against one file's
|
|
|
|
|
// audio inventory onto another version of the same content. Track ordering is
|
|
|
|
|
// not stable across encodes, so carrying the raw ordinal can select a different
|
|
|
|
|
// language. Prefer the stable signature, then the selected language, and
|
|
|
|
|
// finally the effective file's default track.
|
|
|
|
|
func MatchAudioTrackAcrossVersions(
|
|
|
|
|
requestedTracks []models.AudioTrack,
|
|
|
|
|
effectiveTracks []models.AudioTrack,
|
|
|
|
|
requestedIndex int,
|
|
|
|
|
) int {
|
|
|
|
|
if len(effectiveTracks) == 0 {
|
|
|
|
|
return 0
|
|
|
|
|
}
|
|
|
|
|
if len(requestedTracks) == 0 {
|
|
|
|
|
return SelectAudioTrack(effectiveTracks, "", nil)
|
|
|
|
|
}
|
|
|
|
|
if requestedIndex < 0 || requestedIndex >= len(requestedTracks) {
|
|
|
|
|
requestedIndex = SelectAudioTrack(requestedTracks, "", nil)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
selected := requestedTracks[requestedIndex]
|
|
|
|
|
return SelectAudioTrack(effectiveTracks, "", &AudioTrackPreference{
|
|
|
|
|
AudioTrackIndex: requestedIndex,
|
|
|
|
|
AudioLanguage: selected.Language,
|
|
|
|
|
TrackSignature: AudioTrackSignatureFromTrack(selected),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-22 20:26:11 -04:00
|
|
|
// BrowserSupportsAudioCodec returns true if the given audio codec can be
|
|
|
|
|
// played natively by web browsers without transcoding.
|
|
|
|
|
func BrowserSupportsAudioCodec(codec string) bool {
|
|
|
|
|
switch strings.ToLower(codec) {
|
|
|
|
|
case "aac", "mp3", "opus", "vorbis", "flac":
|
|
|
|
|
return true
|
|
|
|
|
default:
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
}
|