From d5ee9a7bf8008830b73e3ca9fa43e02a265513c0 Mon Sep 17 00:00:00 2001 From: Jason Dove <1695733+jasongdove@users.noreply.github.com> Date: Tue, 16 Jun 2026 20:02:46 -0500 Subject: [PATCH] fix: use und language tag with local subtitles --- CHANGELOG.md | 1 + .../Metadata/LocalSubtitlesProviderTests.cs | 66 ++++++++++++++++++- .../Core/Metadata/LocalSubtitlesProvider.cs | 23 ++++--- 3 files changed, 77 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f8b49e330..434c21afb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Content with multiple sidecar subtitles would incorrectly have the subtitle metadata merged (like language) - More rarely, content with an embedded subtitle that has stream index zero would create invalid subtitle records - Both cases will automatically be cleaned up during the next local library scan +- Use `und` language tag with sidecar subtitles that have no language in the file name ## [26.5.1] - 2026-05-08 ### Fixed diff --git a/ErsatzTV.Scanner.Tests/Core/Metadata/LocalSubtitlesProviderTests.cs b/ErsatzTV.Scanner.Tests/Core/Metadata/LocalSubtitlesProviderTests.cs index 238c99a1e..93391f3dd 100644 --- a/ErsatzTV.Scanner.Tests/Core/Metadata/LocalSubtitlesProviderTests.cs +++ b/ErsatzTV.Scanner.Tests/Core/Metadata/LocalSubtitlesProviderTests.cs @@ -98,6 +98,8 @@ public class LocalSubtitlesProviderTests new(@"/Movies/Avatar (2009)/Avatar (2009).en.sdh.srt"), new(@"/Movies/Avatar (2009)/Avatar (2009).sdh.en.srt"), new(@"/Movies/Avatar (2009)/Avatar (2009).de.srt"), + new(@"/Movies/Avatar (2009)/Avatar (2009).srt"), + new(@"/Movies/Avatar (2009)/Avatar (2009).forced.srt"), // non-uniform (lower-case) extensions should also work new(@"/Movies/Avatar (2009)/Avatar (2009).DE.SDH.FORCED.SRT") @@ -122,14 +124,72 @@ public class LocalSubtitlesProviderTests @"/Movies/Avatar (2009)/Avatar (2009).mkv", false); - result.Count.ShouldBe(7); + result.Count.ShouldBe(9); result.Count(s => s.Language == "eng").ShouldBe(5); result.Count(s => s.Language == "deu").ShouldBe(2); - result.Count(s => s.Forced).ShouldBe(3); + result.Count(s => s.Language == "und").ShouldBe(2); + result.Count(s => s.Forced).ShouldBe(4); result.Count(s => s.SDH).ShouldBe(3); - result.Count(s => s.Codec == "subrip").ShouldBe(5); + result.Count(s => s.Codec == "subrip").ShouldBe(7); result.Count(s => s.Codec == "ass").ShouldBe(2); result.Count(s => s.Path.Contains("/Movies/Avatar (2009)")).ShouldBe(0); } + + [Test] + public void Should_Not_Mark_Forced_From_Movie_title() + { + // normally this will have a full list from the database, but we just need these two for testing + var cultures = new List + { + CultureInfo.GetCultureInfo("en-US"), + CultureInfo.GetCultureInfo("de-DE") + }; + + var fakeFiles = new List + { + new(@"/Movies/Bloodfist.III.Forced.To.Fight.1991/Bloodfist.III.Forced.To.Fight.1991.mkv"), + new(@"/Movies/Bloodfist.III.Forced.To.Fight.1991/Bloodfist.III.Forced.To.Fight.1991.eng.srt"), + new(@"/Movies/Bloodfist.III.Forced.To.Fight.1991/Bloodfist.III.Forced.To.Fight.1991.en.forced.ass"), + new(@"/Movies/Bloodfist.III.Forced.To.Fight.1991/Bloodfist.III.Forced.To.Fight.1991.forced.en.ass"), + new(@"/Movies/Bloodfist.III.Forced.To.Fight.1991/Bloodfist.III.Forced.To.Fight.1991.en.sdh.srt"), + new(@"/Movies/Bloodfist.III.Forced.To.Fight.1991/Bloodfist.III.Forced.To.Fight.1991.sdh.en.srt"), + new(@"/Movies/Bloodfist.III.Forced.To.Fight.1991/Bloodfist.III.Forced.To.Fight.1991.de.srt"), + new(@"/Movies/Bloodfist.III.Forced.To.Fight.1991/Bloodfist.III.Forced.To.Fight.1991.srt"), + new(@"/Movies/Bloodfist.III.Forced.To.Fight.1991/Bloodfist.III.Forced.To.Fight.1991.forced.srt"), + + // non-uniform (lower-case) extensions should also work + new(@"/Movies/Bloodfist.III.Forced.To.Fight.1991/Bloodfist.III.Forced.To.Fight.1991.DE.SDH.FORCED.SRT") + }; + + var fileSystem = new MockFileSystem(o => o.SimulatingOperatingSystem(SimulationMode.Linux)); + IFileSystemInitializer init = fileSystem.Initialize(); + foreach (var file in fakeFiles) + { + init.WithFile(file.Path); + } + + var provider = new LocalSubtitlesProvider( + Substitute.For(), + Substitute.For(), + fileSystem, + new LocalFileSystem(fileSystem, Substitute.For>()), + Substitute.For>()); + + List result = provider.LocateExternalSubtitles( + cultures, + @"/Movies/Bloodfist.III.Forced.To.Fight.1991/Bloodfist.III.Forced.To.Fight.1991.mkv", + false); + + result.Count.ShouldBe(9); + result.Count(s => s.Language == "eng").ShouldBe(5); + result.Count(s => s.Language == "deu").ShouldBe(2); + result.Count(s => s.Language == "und").ShouldBe(2); + result.Count(s => s.Forced).ShouldBe(4); + result.Count(s => s.SDH).ShouldBe(3); + result.Count(s => s.Codec == "subrip").ShouldBe(7); + result.Count(s => s.Codec == "ass").ShouldBe(2); + + result.Count(s => s.Path.Contains("/Movies/Bloodfist.III.Forced.To.Fight.1991")).ShouldBe(0); + } } diff --git a/ErsatzTV.Scanner/Core/Metadata/LocalSubtitlesProvider.cs b/ErsatzTV.Scanner/Core/Metadata/LocalSubtitlesProvider.cs index bc123d36a..40de1a69d 100644 --- a/ErsatzTV.Scanner/Core/Metadata/LocalSubtitlesProvider.cs +++ b/ErsatzTV.Scanner/Core/Metadata/LocalSubtitlesProvider.cs @@ -138,32 +138,35 @@ public class LocalSubtitlesProvider : ILocalSubtitlesProvider continue; } - string language = _fileSystem.Path.GetFileName(lowerFile); + string fileNameWithoutExtension = _fileSystem.Path.GetFileNameWithoutExtension(lowerFile); + string suffix = fileNameWithoutExtension[withoutExtension.Length..].ToLowerInvariant(); var forced = false; var sdh = false; - if (lowerFile.Contains(".forced.")) + if (suffix.Contains(".forced")) { forced = true; - language = language.Replace(".forced", string.Empty); + suffix = suffix.Replace(".forced", string.Empty); } - if (lowerFile.Contains(".sdh")) + if (suffix.Contains(".sdh")) { sdh = true; - language = language.Replace(".sdh", string.Empty); + suffix = suffix.Replace(".sdh", string.Empty); } - if (lowerFile.Contains(".cc.")) + if (suffix.Contains(".cc")) { sdh = true; - language = language.Replace(".cc", string.Empty); + suffix = suffix.Replace(".cc", string.Empty); } - language = language - .Replace($"{withoutExtension.ToLowerInvariant()}.", string.Empty)[..3] - .Replace(".", string.Empty); + // use und when no language is present + string language = suffix.Replace(".", string.Empty); + language = string.IsNullOrWhiteSpace(language) + ? "und" + : language[..Math.Min(3, language.Length)]; Option maybeCulture = languageCodes.Find(ci => ci.TwoLetterISOLanguageName == language || ci.ThreeLetterISOLanguageName == language);