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"));
+ }
+}