diff --git a/internal/catalog/detail.go b/internal/catalog/detail.go index a8ab3458..759e57de 100644 --- a/internal/catalog/detail.go +++ b/internal/catalog/detail.go @@ -3453,8 +3453,13 @@ func buildVersionSubtitleTracks(file *models.MediaFile) []VersionSubtitleTrack { FileName: sub.FileName, }) } - for _, sub := range file.ExternalSubtitles { + for i, sub := range file.ExternalSubtitles { tracks = append(tracks, VersionSubtitleTrack{ + // Combined-index identity: externals occupy 0..n-1 in the playback + // selection space (session subtitle_urls, ResolveSubtitlePolicyV3). + // Without this every external serialized index 0 and clients keying + // on index got duplicates. + Index: i, Language: sub.Language, Codec: sub.Format, Title: firstNonEmpty(sub.Title, filepath.Base(sub.Path)), diff --git a/internal/catalog/detail_subtitle_tracks_test.go b/internal/catalog/detail_subtitle_tracks_test.go new file mode 100644 index 00000000..5ddc2c21 --- /dev/null +++ b/internal/catalog/detail_subtitle_tracks_test.go @@ -0,0 +1,44 @@ +package catalog + +import ( + "testing" + + "github.com/Silo-Server/silo-server/internal/models" +) + +// External subtitles must carry their combined-index identity (externals +// occupy 0..n-1 in the playback selection space; see +// ResolveSubtitlePolicyV3 / session subtitle_urls). Before this test they all +// serialized the zero value, so any file with two external subs — or one +// external plus an embedded stream index 0 — published duplicate indexes and +// clients keying rows on index crashed or selected the wrong track. +func TestBuildVersionSubtitleTracksAssignsUniqueExternalIndexes(t *testing.T) { + file := &models.MediaFile{ + SubtitleTracks: []models.SubtitleTrack{ + {Index: 2, Codec: "subrip", Language: "en"}, + {Index: 3, Codec: "hdmv_pgs_subtitle", Language: "de"}, + }, + ExternalSubtitles: []models.ExternalSubtitle{ + {Path: "/media/movie.en.srt", Format: "srt", Language: "en"}, + {Path: "/media/movie.nl.srt", Format: "srt", Language: "nl"}, + }, + } + + tracks := buildVersionSubtitleTracks(file) + if len(tracks) != 4 { + t.Fatalf("tracks len = %d, want 4", len(tracks)) + } + + // Embedded entries keep their ffprobe stream indexes (existing contract). + if tracks[0].Index != 2 || tracks[1].Index != 3 { + t.Fatalf("embedded indexes = %d,%d, want 2,3", tracks[0].Index, tracks[1].Index) + } + + // Externals carry their combined-space ordinal, not the zero value. + if tracks[2].Index != 0 || !tracks[2].External { + t.Fatalf("first external = index %d external %v, want 0/true", tracks[2].Index, tracks[2].External) + } + if tracks[3].Index != 1 || !tracks[3].External { + t.Fatalf("second external = index %d external %v, want 1/true", tracks[3].Index, tracks[3].External) + } +}