diff --git a/src/main/java/de/mediathekview/mserver/base/utils/LanguageCodeUtils.java b/src/main/java/de/mediathekview/mserver/base/utils/LanguageCodeUtils.java
new file mode 100644
index 000000000..aee109f7f
--- /dev/null
+++ b/src/main/java/de/mediathekview/mserver/base/utils/LanguageCodeUtils.java
@@ -0,0 +1,108 @@
+package de.mediathekview.mserver.base.utils;
+
+import java.util.Locale;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+
+/**
+ * Bringt die Sprachcodes, die die Sender liefern, in die Form, die {@code Film.audioLanguage}
+ * zusichert: dreistelliger ISO-639-2/T-Code in Kleinschreibung.
+ *
+ *
Die Sender sind darin uneinheitlich. Die ARD schreibt dreistellig ("deu", "eng", "fra"), arte
+ * zweistellig ("de", "fr", "es"). Beide verwenden ausserdem Platzhalter, die gar keine Sprache
+ * benennen - "ov" bei der ARD, "und" und "mul" bei arte. Diese Klasse ist die eine Stelle, an der
+ * das zusammengefuehrt wird, damit im Feld nichts landet, worauf sich Clients nicht verlassen
+ * koennen.
+ */
+public final class LanguageCodeUtils {
+
+ /**
+ * Werte, die dort stehen, wo eine Sprache erwartet wird, aber keine benennen: ARDs Marker "ov"
+ * fuer die Originalfassung sowie die ISO-Platzhalter fuer unbestimmt, nicht kodiert, mehrsprachig
+ * und "kein sprachlicher Inhalt".
+ */
+ private static final Set NON_LANGUAGE_CODES = Set.of("ov", "und", "mis", "mul", "zxx");
+
+ /**
+ * ISO 639-1 auf ISO 639-2/T, beschraenkt auf die Sprachen, die in den Mediatheken vorkommen.
+ *
+ * Bewusst als Tabelle statt ueber {@link Locale}: dessen Sprachdaten haengen unter Linux an
+ * ICU und stehen nicht auf jedem Host vollstaendig zur Verfuegung. Eine Zuordnung, die je nach
+ * Rechner ein anderes Ergebnis liefert, waere fuer ein Feld in der Filmliste die schlechtere
+ * Wahl.
+ *
+ *
Die dreistelligen Codes sind die terminologische Reihe (ISO 639-2/T): "deu" statt "ger",
+ * "fra" statt "fre", "nld" statt "dut", "ces" statt "cze", "ell" statt "gre", "ron" statt "rum",
+ * "slk" statt "slo", "isl" statt "ice", "zho" statt "chi".
+ */
+ private static final Map TWO_TO_THREE_LETTER =
+ Map.ofEntries(
+ Map.entry("de", "deu"),
+ Map.entry("en", "eng"),
+ Map.entry("fr", "fra"),
+ Map.entry("es", "spa"),
+ Map.entry("it", "ita"),
+ Map.entry("pl", "pol"),
+ Map.entry("nl", "nld"),
+ Map.entry("pt", "por"),
+ Map.entry("ru", "rus"),
+ Map.entry("tr", "tur"),
+ Map.entry("ar", "ara"),
+ Map.entry("uk", "ukr"),
+ Map.entry("el", "ell"),
+ Map.entry("sv", "swe"),
+ Map.entry("da", "dan"),
+ Map.entry("no", "nor"),
+ Map.entry("fi", "fin"),
+ Map.entry("cs", "ces"),
+ Map.entry("hu", "hun"),
+ Map.entry("ro", "ron"),
+ Map.entry("sk", "slk"),
+ Map.entry("hr", "hrv"),
+ Map.entry("sr", "srp"),
+ Map.entry("sl", "slv"),
+ Map.entry("bg", "bul"),
+ Map.entry("is", "isl"),
+ Map.entry("ja", "jpn"),
+ Map.entry("zh", "zho"),
+ Map.entry("ko", "kor"),
+ Map.entry("he", "heb"),
+ Map.entry("fa", "fas"),
+ Map.entry("hi", "hin"));
+
+ private LanguageCodeUtils() {}
+
+ /**
+ * Normalisiert einen Sprachcode aus den Senderdaten.
+ *
+ * Eine etwaige Regionsangabe wird abgeschnitten ("spa-ES" wird zu "spa", "de-DE" zu "deu").
+ * Zweistellige Codes werden ueber die Tabelle zugeordnet, dreistellige durchgereicht. Alles, was
+ * keine Sprache benennt - die Platzhalter, ein leerer Wert, ein zweistelliger Code ausserhalb der
+ * Tabelle - ergibt nichts. Lieber kein Wert als einer, auf den sich Clients nicht verlassen
+ * koennen.
+ *
+ * @param rawCode der Code, wie der Sender ihn liefert.
+ * @return der dreistellige Code, oder leer.
+ */
+ public static Optional normalize(final String rawCode) {
+ if (rawCode == null || rawCode.isBlank()) {
+ return Optional.empty();
+ }
+
+ final String primary = rawCode.trim().split("-")[0].toLowerCase(Locale.ROOT);
+ if (NON_LANGUAGE_CODES.contains(primary)) {
+ return Optional.empty();
+ }
+
+ if (primary.length() == 2) {
+ return Optional.ofNullable(TWO_TO_THREE_LETTER.get(primary));
+ }
+
+ if (primary.length() == 3 && primary.chars().allMatch(Character::isLetter)) {
+ return Optional.of(primary);
+ }
+
+ return Optional.empty();
+ }
+}
diff --git a/src/main/java/de/mediathekview/mserver/crawler/ard/json/ArdFilmDeserializer.java b/src/main/java/de/mediathekview/mserver/crawler/ard/json/ArdFilmDeserializer.java
index 8b16c3611..d15882e79 100644
--- a/src/main/java/de/mediathekview/mserver/crawler/ard/json/ArdFilmDeserializer.java
+++ b/src/main/java/de/mediathekview/mserver/crawler/ard/json/ArdFilmDeserializer.java
@@ -9,6 +9,7 @@
import de.mediathekview.mserver.daten.Sender;
import de.mediathekview.mserver.base.utils.GeoLocationGuesser;
import de.mediathekview.mserver.base.utils.JsonUtils;
+import de.mediathekview.mserver.base.utils.LanguageCodeUtils;
import de.mediathekview.mserver.base.utils.UrlUtils;
import de.mediathekview.mserver.crawler.ard.ArdConstants;
import de.mediathekview.mserver.crawler.ard.ArdFilmDto;
@@ -223,19 +224,23 @@ public List deserialize(
ArdVideoInfoDto allVideoUrlsOV = new ArdVideoInfoDto();
allVideoUrlsOV.putAll(videoInfo.get().getVideoUrlsOV());
allVideoUrlsOV.setSubtitleUrl(videoInfo.get().getSubtitleUrl());
- final ArdFilmDto filmDtoOV =
- new ArdFilmDto(
- createFilm(
- id.get(),
- sender,
- topic.get(),
- title.get() + " (Originalversion)",
- description.orElse(null),
- date.orElse(null),
- duration.orElse(null),
- allVideoUrlsOV,
- geoBlocked));
- films.add(filmDtoOV);
+ allVideoUrlsOV.setOvLanguage(videoInfo.get().getOvLanguage());
+ final Film filmOV =
+ createFilm(
+ id.get(),
+ sender,
+ topic.get(),
+ title.get() + " (Originalversion)",
+ description.orElse(null),
+ date.orElse(null),
+ duration.orElse(null),
+ allVideoUrlsOV,
+ geoBlocked);
+ // Nur die Originalfassung traegt die Sprache. Die Hauptfassung bleibt leer: sie ist die
+ // uebliche Fassung des Senders, und ein Wert auf jedem Datensatz wuerde die Filmliste
+ // unnoetig aufblaehen.
+ filmOV.setAudioLanguage(videoInfo.get().getOvLanguage());
+ films.add(new ArdFilmDto(filmOV));
}
return films;
@@ -430,15 +435,26 @@ private Optional parseVideos(final JsonObject playerPageObject,
if (videoInfoOV.isEmpty() && videoInfoOVAdaptive.isPresent()) {
videoInfoOV = getResolutionsFromAdaptiveUrl(videoInfoOVAdaptive);
}
+ // Die Sprache ist hier bekannt - die Streams wurden oben getrennt nach "eng" und "fra"
+ // eingelesen. Bisher ging sie beim Zusammenfassen zur Originalversion verloren, sodass im
+ // Ergebnis nur noch der Titelzusatz "(Originalversion)" ohne Sprachangabe stand.
+ String ovLanguage = null;
if (videoInfoOV.isEmpty() && videoInfoEng.isPresent()) {
videoInfoOV = videoInfoEng;
+ ovLanguage = MARKER_VIDEO_ENG;
}
if (videoInfoOV.isEmpty() && videoInfoFra.isPresent()) {
videoInfoOV = videoInfoFra;
+ ovLanguage = MARKER_VIDEO_FRA;
}
// flaws - missing proper video marker - mainly tagesschau
if ((title.contains(" - (Originalversion)") || title.contains(" (OV)")) && videoInfoOV.isEmpty()) {
videoInfoOV = parseVideoUrls(playerPageObject, MARKER_VIDEO_CATEGORY_MAIN, MARKER_VIDEO_STANDARD, MARKER_VIDEO_MP4, "*");
+ // Dieser Zweig faengt jede Sprache ab, nicht nur die oben durchprobierten "eng" und "fra".
+ // Der Code wird deshalb aus den Daten gelesen statt aus dem zutreffenden Filter abgeleitet.
+ if (videoInfoOV.isPresent()) {
+ ovLanguage = parseAudioLanguageCode(playerPageObject, MARKER_VIDEO_CATEGORY_MAIN, MARKER_VIDEO_STANDARD, MARKER_VIDEO_MP4, "*").orElse(null);
+ }
}
if ((title.contains(" (mit Gebärdensprache)") || title.contains(" mit Gebärdensprache")) && videoInfoStandard.isPresent() && videoInfoDGS.isEmpty()) {
videoInfoDGS = videoInfoStandard;
@@ -454,6 +470,7 @@ private Optional parseVideos(final JsonObject playerPageObject,
videoInfoDGS.ifPresent(allVideoUrls::putAllDGS);
videoInfoOVAdaptive.ifPresent(x -> allVideoUrls.setAdaptivUrl(x.entrySet().stream().findFirst().get().getValue()));
videoInfoOV.ifPresent(allVideoUrls::putAllOV);
+ allVideoUrls.setOvLanguage(ovLanguage);
subtitles.ifPresent(allVideoUrls::setSubtitleUrl);
if (allVideoUrls.getVideoUrls().isEmpty() && allVideoUrls.getVideoUrlsAD().isEmpty() && allVideoUrls.getVideoUrlsDGS().isEmpty() && allVideoUrls.getVideoUrlsOV().isEmpty() ) {
@@ -559,4 +576,62 @@ private Optional