diff --git a/server/src/Flaro.Core/Library/FileNames.cs b/server/src/Flaro.Core/Library/FileNames.cs index 5ca8001..0e8ade1 100644 --- a/server/src/Flaro.Core/Library/FileNames.cs +++ b/server/src/Flaro.Core/Library/FileNames.cs @@ -63,8 +63,27 @@ public static partial class FileNames [GeneratedRegex(@"^\d{3,4}[pi]$|^[48]k$", RegexOptions.IgnoreCase)] private static partial Regex Auflösung(); - /// S01E02, s1e2, S01.E02, S01E02E03 — die erste Folge zählt. - [GeneratedRegex(@"\bS(?\d{1,3})[\s._-]*E(?\d{1,4})", RegexOptions.IgnoreCase)] + /// S01E02, s1e2, S01.E02, Serie_S02E18, S2024E04 — die erste Folge zählt. + /// + /// + /// + /// Kein \b vor dem S, und das hat 3445 Dateien gekostet. Eine + /// Wortgrenze sitzt zwischen einem Wortzeichen und einem Nicht-Wortzeichen + /// — der Unterstrich ist aber selbst ein Wortzeichen. In + /// The Good Doctor_S02E18_Trampolin.mkv steht zwischen r und + /// S damit keine Grenze, und der ganze Bestand aus einem + /// Mediathek-Download fiel durch. Die Rückschau verlangt stattdessen + /// ausdrücklich: kein Buchstabe, keine Ziffer davor — ein Unterstrich + /// schon. + /// + /// + /// Vier Ziffern für die Staffel. Sendungen aus Mediatheken + /// nummerieren nach Jahr: Die Anstalt_S2024E04. Mit drei Ziffern + /// fiel jede davon durch. + /// + /// + [GeneratedRegex(@"(?\d{1,4})[\s._-]*E(?\d{1,4})", + RegexOptions.IgnoreCase)] private static partial Regex StaffelUndFolge(); /// 1x02, 01x02 — die ältere Schreibweise. @@ -202,10 +221,30 @@ public static partial class FileNames var treffer = StaffelUndFolge().Match(roh); if (!treffer.Success) treffer = KreuzSchreibweise().Match(roh); - if (!treffer.Success) return null; - if (!int.TryParse(treffer.Groups["staffel"].Value, out var staffel) || - !int.TryParse(treffer.Groups["folge"].Value, out var folge)) return null; + int staffel, folge; + + if (treffer.Success) + { + if (!int.TryParse(treffer.Groups["staffel"].Value, out staffel) || + !int.TryParse(treffer.Groups["folge"].Value, out folge)) return null; + } + else + { + // **„Folge 12" ist eine Folgennummer, nur auf Deutsch.** + // + // Mediatheken und Mitschnitte benennen so: + // „Weihnachtsmann & Co. KG # Folge 12 # Strafe muss sein". + // Eine Staffel steht dort nie — die kommt, wenn überhaupt, vom + // Ordner darüber. Fehlt auch der, gilt Staffel 1: das ist die + // Übereinkunft für eine Serie ohne Staffeln, nicht geraten. + var nurFolge = NurFolgennummer().Match(roh); + if (!nurFolge.Success || !int.TryParse(nurFolge.Groups["folge"].Value, out folge)) + return null; + + staffel = StaffelAusOrdner(dateiname) ?? 1; + treffer = nurFolge; + } var davor = roh[..treffer.Index]; var danach = roh[(treffer.Index + treffer.Length)..]; @@ -352,6 +391,32 @@ public static partial class FileNames return titel.Length > 0 ? titel : null; } + /// „Folge 12", „Episode 3", „Teil 7" — eine Nummer ohne Staffel. + /// + /// + /// Mit Wortgrenze dahinter, damit „Folge 1" nicht aus „Folge 12" wird, und + /// nur bis drei Ziffern: „Folge 2024" wäre eher ein Datum. + /// + [GeneratedRegex(@"(?\d{1,3})(?!\d)", + RegexOptions.IgnoreCase)] + private static partial Regex NurFolgennummer(); + + /// Die Staffelnummer aus einem Ordner „Season 4" über der Datei. + /// + /// + /// Nur aus dem unmittelbaren Ordner. Steht dort keine Zahl, wird + /// keine erfunden — der Aufrufer entscheidet, was dann gilt. + /// + private static int? StaffelAusOrdner(string pfad) + { + var ordner = Path.GetFileName(Path.GetDirectoryName(pfad.Replace('\\', '/')) ?? ""); + var treffer = StaffelZahl().Match(ordner.Trim()); + return treffer.Success && int.TryParse(treffer.Groups["nr"].Value, out var nr) ? nr : null; + } + + [GeneratedRegex(@"^(?:staffel|season|series|s)[\s._-]*(?\d{1,3})$", RegexOptions.IgnoreCase)] + private static partial Regex StaffelZahl(); + /// „Staffel 1", „Season 02", „S3" — Ordner, die keine Serie benennen. private static bool IstStaffelordner(string name) => StaffelOrdnerName().IsMatch(name.Trim()); diff --git a/server/tests/Flaro.Core.Tests/FileNameTests.cs b/server/tests/Flaro.Core.Tests/FileNameTests.cs index 5e6567d..8ea364a 100644 --- a/server/tests/Flaro.Core.Tests/FileNameTests.cs +++ b/server/tests/Flaro.Core.Tests/FileNameTests.cs @@ -255,3 +255,100 @@ public class SerieAusDerWurzelTests Assert.That(FileNames.ParseEpisode("/media/Serien/Dark - S01E01.mkv", Wurzel)?.SeriesTitle, Is.EqualTo("Dark")); } + +/// Benennungen, die im echten Bestand vorkamen und durchfielen. +/// +/// +/// Diese Namen sind nicht ausgedacht. Jeder davon stammt aus einem +/// Protokoll, in dem 3559 von 5884 Dateien als „nicht zugeordnet" standen — +/// und aus 95 Serien wurden dadurch 58. Erfundene Testnamen hatten den Fehler +/// nicht gefunden: sie trugen alle ein Leerzeichen vor der Folgennummer. +/// +public class EchteBenennungen +{ + private const string Wurzel = "/media/Serien"; + + /// Der Unterstrich — 3445 Dateien, und die teuerste Zeile des Tages. + /// + /// + /// \b sitzt zwischen Wortzeichen und Nicht-Wortzeichen. Der + /// Unterstrich ist selbst ein Wortzeichen, also gibt es zwischen r + /// und S keine Grenze — und der ganze Mediathek-Bestand fiel durch. + /// + [Test] + public void Unterstrich_vor_der_Folgennummer() + { + var name = FileNames.ParseEpisode( + $"{Wurzel}/The Good Doctor/S02/The Good Doctor_S02E18_Trampolin.mkv", Wurzel); + + Assert.That(name, Is.Not.Null); + Assert.That(name!.SeriesTitle, Is.EqualTo("The Good Doctor")); + Assert.That(name.Season, Is.EqualTo(2)); + Assert.That(name.Episode, Is.EqualTo(18)); + Assert.That(name.EpisodeTitle, Is.EqualTo("Trampolin")); + } + + /// Mediatheken nummerieren nach Jahr: S2024E04. + [Test] + public void Jahreszahl_als_Staffel() + { + var name = FileNames.ParseEpisode( + $"{Wurzel}/Die Anstalt/Die Anstalt_S2024E04_Die Anstalt vom 11_ Juni 2024.mkv", Wurzel); + + Assert.That(name, Is.Not.Null); + Assert.That(name!.SeriesTitle, Is.EqualTo("Die Anstalt")); + Assert.That(name.Season, Is.EqualTo(2024)); + Assert.That(name.Episode, Is.EqualTo(4)); + } + + /// „Folge 12" ist eine Folgennummer, nur auf Deutsch. + [Test] + public void Folge_auf_Deutsch() + { + var name = FileNames.ParseEpisode( + $"{Wurzel}/Weihnachtsmann & Co. KG (1997)/" + + "Weihnachtsmann & Co. KG # Folge 12 # Strafe muss sein # Ganze Folge # TOGGO Serien.mp4", + Wurzel); + + Assert.That(name, Is.Not.Null); + Assert.That(name!.SeriesTitle, Is.EqualTo("Weihnachtsmann & Co. KG")); + Assert.That(name.Season, Is.EqualTo(1), "Ohne Staffelordner gilt die Übereinkunft: Staffel 1."); + Assert.That(name.Episode, Is.EqualTo(12)); + } + + /// Steht die Staffel am Ordner, wird sie nicht erfunden. + [Test] + public void Folge_mit_Staffel_aus_dem_Ordner() + { + var name = FileNames.ParseEpisode( + $"{Wurzel}/Irgendeine Serie/Season 4/Irgendeine Serie - Folge 7.mp4", Wurzel); + + Assert.That(name, Is.Not.Null); + Assert.That(name!.Season, Is.EqualTo(4)); + Assert.That(name.Episode, Is.EqualTo(7)); + } + + /// Was wirklich keine Nummer trägt, bleibt liegen. + /// + /// + /// Fünf Dateien im echten Bestand sind so: ein Titel in einem + /// Staffelordner, sonst nichts. Eine Nummer dafür zu erfinden hiesse, sie + /// an eine beliebige Stelle zu setzen — dann lieber sichtbar liegen lassen. + /// + [Test] + public void Ein_blosser_Titel_bleibt_unerkannt() + { + Assert.That( + FileNames.ParseEpisode($"{Wurzel}/Nachtstreife (2020)/Season 4/Endstation Zelle.mp4", Wurzel), + Is.Null); + } + + /// Die Gründe müssen auseinanderhalten, was Verschiedenes verlangt. + [Test] + public void Der_Grund_benennt_das_Richtige() + { + Assert.That( + FileNames.WarumNicht($"{Wurzel}/Nachtstreife (2020)/Season 4/Endstation Zelle.mp4", Wurzel), + Does.Contain("keine Folgennummer")); + } +}