From 9955fb9b0e17972637b435081a912781be6b49de Mon Sep 17 00:00:00 2001 From: CuriousApe2020 Date: Thu, 10 Sep 2026 22:18:00 +0000 Subject: [PATCH 1/3] Sprache der Tonspur als eigenes Feld in der Filmliste Die Sprache einer Tonspur liegt beim Crawlen teilweise vor, geht beim Erzeugen des Filmliste-Eintrags aber verloren: kodiert wird nur der deutsche Titelzusatz "(Originalversion)", der die Sprache nicht nennt. Clients koennen sie danach nicht mehr rekonstruieren. Neu ist Film.audioLanguage (ISO 639-2/T, null wenn unbekannt). Woher die Sprache kommt: * Die ARD liefert je Tonspur einen Sprachcode in audio[0].languageCode. Die Streamauswahl probiert davon allerdings nur die fest verdrahteten Codes "deu", "eng" und "fra" durch; alles andere faellt in den Platzhalter "*". * Fuer "eng" und "fra" steht der Code damit ohnehin fest. * Fuer den Platzhalterzweig wird er mit parseAudioLanguageCode aus den Daten gelesen statt aus dem zutreffenden Filter abgeleitet. Damit behaelt auch eine Originalversion in einer anderen Sprache - spanisch, polnisch, tuerkisch - ihre Sprache, ohne dass dafuer je Sprache eine weitere Konstante noetig waere. * normalizeLanguageCode sichert die zugesagte Form: eine etwaige Regionsangabe wird abgeschnitten ("spa-ES" wird zu "spa"), alles was danach kein dreistelliger Buchstabencode ist - ARDs Platzhalter "ov", Leerwerte - ergibt nichts. Lieber kein Wert als einer, auf den sich Clients nicht verlassen koennen. * 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. Film.merge uebernimmt eine bekannte Sprache, wenn der eigene Stand keine hat. Sonst ginge sie verloren, sobald eine aeltere importierte Filmliste ohne das Feld mit einem frischen Crawl zusammengefuehrt wird. Serialisierung: * Neues Format: Gson serialisiert den Objektgraphen per Reflection, das Feld erscheint automatisch als benannter Schluessel; FilmlistReader liest es symmetrisch wieder ein. * Altes Format: hinten angefuegte Spalte "Sprache" als einfacher String. Bewusst kein Array - der Desktop-Client prueft beim Weiterlesen mit isExpectedStartArrayToken(), ob der naechste Token ein Datensatz ist; ein angehaengtes Array wuerde dort als Filmdatensatz interpretiert. Rueckwaertskompatibilitaet: * MediathekView Desktop 14.0.0 bis 14.5.0 liest je Datensatz genau 20 Felder und sucht danach den naechsten START_ARRAY. Ein angehaengtes Feld faellt durch diese Pruefung und wird uebersprungen - keine Exception, kein Versatz. Die Spaltenkopfzeile wertet der Client gar nicht aus (skipFieldDescriptions). * MediathekViewWeb destrukturiert positionsbasiert bis Index 16 und ignoriert weitere Felder. * FilmlistOldFormatReader liest die neue Spalte optional: Filmlisten aelterer Staende ohne die Spalte werden weiterhin gelesen. Ausserdem: FilmlistOldFormatWriterTest las die vom Writer als UTF-8 geschriebene Datei mit StandardCharsets.UTF_16 ein. Das lief nur durch, solange die Dateilaenge zufaellig gerade war; durch die zusaetzliche Spalte wurde sie ungerade und readString warf eine MalformedInputException. Korrigiert auf UTF-8, passend zum Writer. Tests: Round-Trip durch beide Formate, eine Filmliste ohne die neue Spalte, eine spanische Originalversion, ein Code mit Regionsangabe, die Gegenprobe mit ARDs Platzhalter "ov", das Verhalten von merge, sowie ein Beitrag mit deutscher und englischer Tonspur - dort darf die Hauptfassung die Sprache der Originalversion nicht erben. --- .../crawler/ard/json/ArdFilmDeserializer.java | 120 ++++- .../crawler/ard/json/ArdVideoInfoDto.java | 15 + .../de/mediathekview/mserver/daten/Film.java | 28 + .../reader/FilmlistOldFormatReader.java | 18 + .../writer/FilmlistOldFormatWriter.java | 19 + .../ArdFilmDeserializerAudioLanguageTest.java | 105 ++++ .../FilmlistOldFormatWriterTest.java | 86 ++- .../NewFilmlistAudioLanguageTest.java | 93 ++++ .../ard/ard_item_OV_eng_plus_deu.json | 500 ++++++++++++++++++ .../resources/ard/ard_item_OV_spanish.json | 405 ++++++++++++++ .../ard/ard_item_OV_spanish_region.json | 1 + .../mlib/TestFilmlistOldFormatWriter.json | 2 +- 12 files changed, 1376 insertions(+), 16 deletions(-) create mode 100644 src/test/java/de/mediathekview/mserver/crawler/ard/json/ArdFilmDeserializerAudioLanguageTest.java create mode 100644 src/test/java/de/mediathekview/mserver/filmlisten/NewFilmlistAudioLanguageTest.java create mode 100644 src/test/resources/ard/ard_item_OV_eng_plus_deu.json create mode 100644 src/test/resources/ard/ard_item_OV_spanish.json create mode 100644 src/test/resources/ard/ard_item_OV_spanish_region.json 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..16b34ebe5 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 @@ -223,19 +223,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 +434,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 +469,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 +575,82 @@ private Optional> parseVideoUrlMap(final JsonObject playerP } return Optional.of(videoInfo); } + + /** + * Liefert den tatsaechlichen Sprachcode ({@code audio[0].languageCode}) des ersten Streams, der + * zum uebergebenen Filter passt. + * + *

Die Auswahl der Streams oben probiert nur die fest verdrahteten Codes "deu", "eng" und "fra" + * durch. Eine Originalversion in einer anderen Sprache - spanisch, polnisch, tuerkisch - landet + * ueber den Platzhalter "*" trotzdem korrekt als Originalversion, ihre Sprache waere aber nicht + * bekannt. Statt sie aus dem zutreffenden Filter zu erraten, wird sie hier aus den Daten gelesen. + * + *

Die Platzhalter "OV" und "*" selbst benennen keine Sprache; ARDs eigener Platzhalter "ov" + * im Feld wird deshalb verworfen. + */ + private Optional parseAudioLanguageCode(final JsonObject playerPageObject, String streamType, String aduioType, String mimeType, String language) { + final Optional mediaCollectionObject = getMediaCollectionObject(playerPageObject); + if (mediaCollectionObject.isEmpty()) { + return Optional.empty(); + } + final Optional streams = JsonUtils.getElement(mediaCollectionObject.get(), ELEMENT_STREAMS); + if (streams.isEmpty() || !streams.get().isJsonArray() || streams.get().getAsJsonArray().isEmpty()) { + return Optional.empty(); + } + for (JsonElement streamsCategory : streams.get().getAsJsonArray()) { + final Optional streamKind = JsonUtils.getElementValueAsString(streamsCategory, ATTRIBUTE_KIND); + final Optional media = JsonUtils.getElement(streamsCategory, ELEMENT_MEDIA); + if (media.isEmpty() || !media.get().isJsonArray() || media.get().getAsJsonArray().isEmpty()) { + continue; + } + if (!streamKind.orElse("").equalsIgnoreCase(streamType)) { + continue; + } + for (JsonElement video : media.get().getAsJsonArray()) { + final Optional mime = JsonUtils.getElementValueAsString(video, ATTRIBUTE_MIME); + if (mime.isEmpty() || !mime.get().equalsIgnoreCase(mimeType)) { + continue; + } + final Optional audios = JsonUtils.getElement(video, ELEMENT_AUDIO); + if (audios.isEmpty() || !audios.get().isJsonArray() || audios.get().getAsJsonArray().isEmpty()) { + continue; + } + final Optional kind = JsonUtils.getElementValueAsString(audios.get().getAsJsonArray().get(0), ATTRIBUTE_KIND); + final Optional languageCode = JsonUtils.getElementValueAsString(audios.get().getAsJsonArray().get(0), ATTRIBUTE_ADUIO_LANG); + if (kind.isEmpty() || !kind.get().equalsIgnoreCase(aduioType)) { + continue; + } + final String code = languageCode.orElse(""); + final boolean matches = code.equalsIgnoreCase(language) + || (language.equalsIgnoreCase("*") && !code.equalsIgnoreCase(MARKER_VIDEO_DE) && !code.equalsIgnoreCase("ov")); + if (matches) { + final Optional normalized = normalizeLanguageCode(code); + if (normalized.isPresent()) { + return normalized; + } + } + } + } + return Optional.empty(); + } + + /** + * Bringt einen Sprachcode aus den Senderdaten in die Form, die das Feld zusichert: dreistelliger + * ISO-639-2/T-Code in Kleinschreibung. + * + *

Eine etwaige Regionsangabe wird abgeschnitten ("spa-ES" wird zu "spa"). Alles, was danach + * kein dreistelliger Buchstabencode ist - ARDs Platzhalter "ov", ein leerer Wert, ein + * zweistelliger Code, den MServer mangels Tabelle nicht zuordnen koennte - ergibt nichts. Lieber + * kein Wert als einer, auf den sich Clients nicht verlassen koennen. + */ + private static Optional normalizeLanguageCode(final String rawCode) { + if (rawCode == null || rawCode.isBlank()) { + return Optional.empty(); + } + final String primary = rawCode.trim().split("-")[0].toLowerCase(Locale.ROOT); + if (primary.length() != 3 || !primary.chars().allMatch(Character::isLetter)) { + return Optional.empty(); + } + return "ov".equals(primary) ? Optional.empty() : Optional.of(primary); + } } diff --git a/src/main/java/de/mediathekview/mserver/crawler/ard/json/ArdVideoInfoDto.java b/src/main/java/de/mediathekview/mserver/crawler/ard/json/ArdVideoInfoDto.java index 7da7b4d7b..1a8dfde54 100644 --- a/src/main/java/de/mediathekview/mserver/crawler/ard/json/ArdVideoInfoDto.java +++ b/src/main/java/de/mediathekview/mserver/crawler/ard/json/ArdVideoInfoDto.java @@ -21,6 +21,13 @@ public class ArdVideoInfoDto { private final Map videoUrlsDGS; private final Map videoUrlsOV; private String adaptivUrl = null; + + /** + * Sprache der Originalversion als ISO-639-2/T-Code, sofern bekannt. Die ARD liefert die Streams + * je Sprache getrennt aus ("eng", "fra"); bisher wurde diese Zuordnung beim Zusammenfassen zur + * Originalversion verworfen. + */ + private String ovLanguage = null; private Set subtitleUrl; @@ -134,6 +141,14 @@ public String getAdaptivUrl() { return adaptivUrl; } + public String getOvLanguage() { + return ovLanguage; + } + + public void setOvLanguage(String ovLanguage) { + this.ovLanguage = ovLanguage; + } + public void setAdaptivUrl(String adaptivUrl) { this.adaptivUrl = adaptivUrl; } diff --git a/src/main/java/de/mediathekview/mserver/daten/Film.java b/src/main/java/de/mediathekview/mserver/daten/Film.java index 911ef6697..37996ee13 100644 --- a/src/main/java/de/mediathekview/mserver/daten/Film.java +++ b/src/main/java/de/mediathekview/mserver/daten/Film.java @@ -14,6 +14,17 @@ public class Film extends Podcast { private Map audioDescriptions; private Map signLanguages; + /** + * Sprache der Tonspur dieses Films als ISO-639-2/T-Code ({@code "deu"}, {@code "eng"}, + * {@code "fra"}), oder {@code null}, wenn der Sender sie nicht benennt. + * + *

Relevant vor allem fuer Originalversionen: die werden bisher nur ueber den Titelzusatz + * "(Originalversion)" kenntlich gemacht, der die Sprache nicht nennt. Sie geht damit verloren, + * obwohl sie beim Crawlen teilweise vorliegt - der ARD-Crawler liest die Streams bereits getrennt + * nach "eng" und "fra" ein. + */ + private String audioLanguage; + public Film( final UUID aUuid, final Sender aSender, @@ -32,6 +43,7 @@ public Film(final Film copyObj) { audioDescriptions = copyObj.audioDescriptions; signLanguages = copyObj.signLanguages; subtitles = copyObj.subtitles; + audioLanguage = copyObj.audioLanguage; } /** DON'T USE! - ONLY FOR GSON! */ @@ -53,9 +65,25 @@ public AbstractMediaResource merge(final AbstractMediaResource || urlEntry.getValue().getFileSize() > getUrls().get(urlEntry.getKey()).getFileSize()) .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))); + // Beim Zusammenfuehren zweier Staende - etwa einer importierten aelteren Filmliste ohne das + // Feld und einem frischen Crawl - darf die einmal bekannte Sprache nicht verloren gehen. + if (audioLanguage == null && objToMergeWith instanceof Film filmToMergeWith) { + audioLanguage = filmToMergeWith.getAudioLanguage(); + } return this; } + /** + * @return Sprache der Tonspur als ISO-639-2/T-Code, oder {@code null} wenn unbekannt. + */ + public String getAudioLanguage() { + return audioLanguage; + } + + public void setAudioLanguage(final String aAudioLanguage) { + audioLanguage = aAudioLanguage; + } + public void addAllSubtitleUrls(final Set urlsToAdd) { subtitles.addAll(urlsToAdd); } diff --git a/src/main/java/de/mediathekview/mserver/filmlisten/reader/FilmlistOldFormatReader.java b/src/main/java/de/mediathekview/mserver/filmlisten/reader/FilmlistOldFormatReader.java index e9b457b73..778e7b7fe 100644 --- a/src/main/java/de/mediathekview/mserver/filmlisten/reader/FilmlistOldFormatReader.java +++ b/src/main/java/de/mediathekview/mserver/filmlisten/reader/FilmlistOldFormatReader.java @@ -155,8 +155,19 @@ private void headerColumns(JsonReader jsonReader) throws IOException { jsonReader.nextString(); // Url History jsonReader.nextString(); // Geo jsonReader.nextString(); // neu + readOptionalHeaderColumn(jsonReader); // Sprache jsonReader.endArray(); } + + /** + * Liest eine optionale, hinten angehaengte Spalte. Aeltere Filmlisten fuehren sie nicht, deshalb + * wird nur gelesen, wenn ueberhaupt noch ein Wert vor dem Array-Ende steht. + */ + private void readOptionalHeaderColumn(JsonReader jsonReader) throws IOException { + if (jsonReader.peek() == JsonToken.STRING) { + jsonReader.nextString(); + } + } private Optional readRecord(JsonReader jsonReader) throws IOException { cnt++; @@ -212,6 +223,13 @@ private Optional readRecord(JsonReader jsonReader) throws IOException { f.setGeoLocations(readRecord19Geo(jsonReader.nextString())); // f.setNeu(readRecord20Neu(jsonReader.nextString())); + // Hinten angehaengt und optional: Filmlisten aelterer MServer-Staende haben das Feld nicht. + if (jsonReader.peek() == JsonToken.STRING) { + final String sprache = jsonReader.nextString(); + if (!sprache.isEmpty()) { + f.setAudioLanguage(sprache); + } + } // jsonReader.endArray(); // diff --git a/src/main/java/de/mediathekview/mserver/filmlisten/writer/FilmlistOldFormatWriter.java b/src/main/java/de/mediathekview/mserver/filmlisten/writer/FilmlistOldFormatWriter.java index 192d30f7d..82ba29276 100644 --- a/src/main/java/de/mediathekview/mserver/filmlisten/writer/FilmlistOldFormatWriter.java +++ b/src/main/java/de/mediathekview/mserver/filmlisten/writer/FilmlistOldFormatWriter.java @@ -147,6 +147,10 @@ private void writeColumnHeader(JsonWriter jsonWriter) throws IOException { jsonWriter.value("Url History"); jsonWriter.value("Geo"); jsonWriter.value("neu"); + // Neue Felder werden HINTEN angefuegt: der Desktop-Client liest den Datensatz + // positionsbasiert und ueberspringt die Spaltenkopfzeile vollstaendig + // (FilmListReader.skipFieldDescriptions), ein angehaengtes Feld wird dort daher ignoriert. + jsonWriter.value("Sprache"); jsonWriter.endArray(); } @@ -172,9 +176,24 @@ private void writeRecord(AbstractMediaResource film, JsonWriter jsonWriter) t jsonWriter.value(""); // Url History jsonWriter.value(writeRecord19Geo(film)); jsonWriter.value(writeRecord20Neu(film)); + jsonWriter.value(writeRecord21Sprache(film)); jsonWriter.endArray(); } + /** + * Sprache der Tonspur als ISO-639-2/T-Code, leer wenn der Sender sie nicht benennt. + * + *

Bewusst ein einfacher String und kein Array: der Desktop-Client prueft beim Weiterlesen mit + * {@code isExpectedStartArrayToken()}, ob der naechste Token ein Datensatz ist. Ein angehaengtes + * Array wuerde dort als Filmdatensatz interpretiert. + */ + private String writeRecord21Sprache(AbstractMediaResource in) { + if (in instanceof Film film && film.getAudioLanguage() != null) { + return film.getAudioLanguage(); + } + return ""; + } + private String writeRecord01Sender(AbstractMediaResource in, String aSender) { if (!aSender.equalsIgnoreCase(in.getSenderName())) { this.sender = in.getSenderName(); diff --git a/src/test/java/de/mediathekview/mserver/crawler/ard/json/ArdFilmDeserializerAudioLanguageTest.java b/src/test/java/de/mediathekview/mserver/crawler/ard/json/ArdFilmDeserializerAudioLanguageTest.java new file mode 100644 index 000000000..838021b07 --- /dev/null +++ b/src/test/java/de/mediathekview/mserver/crawler/ard/json/ArdFilmDeserializerAudioLanguageTest.java @@ -0,0 +1,105 @@ +package de.mediathekview.mserver.crawler.ard.json; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.MatcherAssert.assertThat; + +import com.google.gson.JsonElement; +import de.mediathekview.mserver.base.config.MServerConfigManager; +import de.mediathekview.mserver.base.messages.listener.MessageListener; +import de.mediathekview.mserver.crawler.ard.ArdCrawler; +import de.mediathekview.mserver.crawler.ard.ArdFilmDto; +import de.mediathekview.mserver.daten.Film; +import de.mediathekview.mserver.progress.listeners.SenderProgressListener; +import de.mediathekview.mserver.testhelper.JsonFileReader; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ForkJoinPool; +import org.junit.jupiter.api.Test; + +/** + * Prueft, dass die Sprache der Originalversion aus den Daten gelesen wird und nicht nur fuer die + * beiden fest verdrahteten Codes "eng" und "fra" ankommt. + */ +class ArdFilmDeserializerAudioLanguageTest { + + @Test + void originalVersionInAnyLanguageKeepsItsLanguageCode() { + // Dieselbe Sendung wie ard_item_OV.json, aber die Originalversion ist als "spa" ausgezeichnet + // statt mit ARDs Platzhalter "ov". Weder die OV- noch die eng-/fra-Abfrage greift hier; der + // Treffer kommt ueber den Platzhalter "*", und die Sprache wird aus audio[0].languageCode + // gelesen. + final Film originalVersion = findOriginalVersion("/ard/ard_item_OV_spanish.json"); + + assertThat(originalVersion, notNullValue()); + assertThat(originalVersion.getAudioLanguage(), equalTo("spa")); + } + + @Test + void originalVersionWithoutNamedLanguageStaysUnset() { + // Gegenprobe mit dem unveraenderten Fixture: dort meldet die ARD nur ihren eigenen Platzhalter + // "ov", der keine Sprache benennt. Dann darf auch nichts gesetzt werden. + final Film originalVersion = findOriginalVersion("/ard/ard_item_OV.json"); + + assertThat(originalVersion, notNullValue()); + assertThat(originalVersion.getAudioLanguage(), nullValue()); + } + + + @Test + void standardVersionMustNotInheritTheOriginalVersionLanguage() { + // Regression: createFilm hat das Feld frueher unbedingt gesetzt und wird fuer beide Filme mit + // demselben ArdVideoInfoDto aufgerufen. Dadurch bekam die deutsche Hauptfassung die Sprache + // der Originalversion - bei einem Beitrag mit deutscher und englischer Tonspur also "eng". + final List alle = allFilms("/ard/ard_item_OV_eng_plus_deu.json"); + assertThat(alle.size(), equalTo(2)); + + final Film standard = + alle.stream().filter(f -> !f.getTitel().contains("(Originalversion)")).findFirst().orElse(null); + final Film originalVersion = + alle.stream().filter(f -> f.getTitel().contains("(Originalversion)")).findFirst().orElse(null); + + assertThat(standard, notNullValue()); + assertThat(originalVersion, notNullValue()); + assertThat(standard.getAudioLanguage(), nullValue()); + assertThat(originalVersion.getAudioLanguage(), equalTo("eng")); + } + + private List allFilms(final String jsonFile) { + final JsonElement jsonElement = JsonFileReader.readJson(jsonFile); + return new ArdFilmDeserializer(createCrawler()).deserialize(jsonElement, null, null).stream() + .map(ArdFilmDto::getFilm) + .toList(); + } + + @Test + void languageCodeWithRegionIsReducedToTheLanguage() { + // Sollte die ARD den Code je mit Regionsangabe liefern, muss trotzdem der reine + // ISO-639-2/T-Code im Feld stehen - "spa-ES" also als "spa". + final Film originalVersion = findOriginalVersion("/ard/ard_item_OV_spanish_region.json"); + + assertThat(originalVersion, notNullValue()); + assertThat(originalVersion.getAudioLanguage(), equalTo("spa")); + } + + private Film findOriginalVersion(final String jsonFile) { + final JsonElement jsonElement = JsonFileReader.readJson(jsonFile); + final List films = + new ArdFilmDeserializer(createCrawler()).deserialize(jsonElement, null, null); + + return films.stream() + .map(ArdFilmDto::getFilm) + .filter(film -> film.getTitel().contains("(Originalversion)")) + .findFirst() + .orElse(null); + } + + private ArdCrawler createCrawler() { + return new ArdCrawler( + new ForkJoinPool(), + new ArrayList(), + new ArrayList(), + new MServerConfigManager("MServer-JUnit-Config.yaml")); + } +} diff --git a/src/test/java/de/mediathekview/mserver/filmlisten/FilmlistOldFormatWriterTest.java b/src/test/java/de/mediathekview/mserver/filmlisten/FilmlistOldFormatWriterTest.java index 66ae9d558..9fb2edccf 100644 --- a/src/test/java/de/mediathekview/mserver/filmlisten/FilmlistOldFormatWriterTest.java +++ b/src/test/java/de/mediathekview/mserver/filmlisten/FilmlistOldFormatWriterTest.java @@ -1,6 +1,7 @@ package de.mediathekview.mserver.filmlisten; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; import de.mediathekview.mserver.daten.Film; @@ -30,6 +31,84 @@ class FilmlistOldFormatWriterTest { @TempDir Path tempDir; + @Test + void audioLanguageSurvivesWriteAndRead() throws IOException { + // Kern des Ganzen: die Sprache muss den Weg durch das alte Format ueberstehen. Ohne das Feld + // bleibt von einer englischen Originalfassung nur der Titelzusatz "(Originalversion)" uebrig, + // der die Sprache nicht nennt. + final Filmlist input = new Filmlist(); + final Film withLanguage = buildMinimalFilm("Mit Sprache (Originalversion)"); + withLanguage.setAudioLanguage("eng"); + final Film withoutLanguage = buildMinimalFilm("Ohne Sprache"); + input.add(withLanguage); + input.add(withoutLanguage); + + final Filmlist afterRoundTrip = writeAndRead(input); + + assertEquals(2, afterRoundTrip.getFilms().size()); + final Film readWithLanguage = findByTitle(afterRoundTrip, "Mit Sprache (Originalversion)"); + final Film readWithoutLanguage = findByTitle(afterRoundTrip, "Ohne Sprache"); + assertEquals("eng", readWithLanguage.getAudioLanguage()); + // Kein Sender-Wert darf nicht als leerer String zurueckkommen, sondern gar nicht gesetzt sein. + assertNull(readWithoutLanguage.getAudioLanguage()); + } + + @Test + void filmlistWithoutLanguageColumnStillReads() throws IOException { + // Rueckwaertskompatibilitaet in die andere Richtung: eine Filmliste eines aelteren + // MServer-Standes fuehrt die Spalte nicht. Der Reader muss sie weiterhin lesen koennen. + final Path testFilePath = + new File( + Thread.currentThread() + .getContextClassLoader() + .getResource("mlib/TestFilmlistOldFormatWriter.json") + .getFile()) + .toPath(); + final String withColumn = Files.readString(testFilePath, StandardCharsets.UTF_8); + final String withoutColumn = withColumn.replace(",\"Sprache\"]", "]").replace(",\"\"]", "]"); + + final Path legacyFile = Files.createTempFile(tempDir, "LegacyFilmlist", ".json"); + Files.writeString(legacyFile, withoutColumn, StandardCharsets.UTF_8); + + final Optional read = + new FilmlistOldFormatReader().read(new FileInputStream(legacyFile.toString())); + + assertTrue(read.isPresent()); + assertTrue(read.get().getFilms().size() > 0); + read.get().getFilms().values().forEach(film -> assertNull(film.getAudioLanguage())); + } + + private Film buildMinimalFilm(final String titel) throws java.net.MalformedURLException { + final Film film = + new Film( + UUID.randomUUID(), + Sender.ARD, + titel, + "Testthema", + LocalDateTime.of(2026, 1, 1, 20, 15), + Duration.ofMinutes(90)); + film.addUrl(Resolution.NORMAL, new FilmUrl("https://example.org/v.mp4", 1L)); + film.addGeolocation(GeoLocations.GEO_NONE); + return film; + } + + private Filmlist writeAndRead(final Filmlist input) throws IOException { + final Path tempFile = Files.createTempFile(tempDir, "AudioLanguageRoundTrip", ".json"); + new FilmlistOldFormatWriter().write(input, tempFile); + final Optional read = + new FilmlistOldFormatReader().read(new FileInputStream(tempFile.toString())); + Files.deleteIfExists(tempFile); + assertTrue(read.isPresent()); + return read.get(); + } + + private Film findByTitle(final Filmlist list, final String titel) { + return list.getFilms().values().stream() + .filter(film -> titel.equals(film.getTitel())) + .findFirst() + .orElseThrow(); + } + @Test void readFilmlistOldFormatIncludingBrokenRecords() throws IOException { @@ -43,8 +122,11 @@ void readFilmlistOldFormatIncludingBrokenRecords() assertTrue( Files.exists(tempFile)); // - String actualData = Files.readString(tempFile, StandardCharsets.UTF_16).substring(100); - String expectedData = Files.readString(testFilePath, StandardCharsets.UTF_16).substring(100); + // FilmlistOldFormatWriter schreibt UTF-8 (OutputStreamWriter mit StandardCharsets.UTF_8). + // Das Einlesen als UTF-16 lief bisher nur deshalb durch, weil die Dateilaenge zufaellig + // gerade war; bei ungerader Byteanzahl wirft readString eine MalformedInputException. + String actualData = Files.readString(tempFile, StandardCharsets.UTF_8).substring(100); + String expectedData = Files.readString(testFilePath, StandardCharsets.UTF_8).substring(100); assertEquals(expectedData, actualData, "Filmlisten stimmen überein."); // Files.deleteIfExists(tempFile); diff --git a/src/test/java/de/mediathekview/mserver/filmlisten/NewFilmlistAudioLanguageTest.java b/src/test/java/de/mediathekview/mserver/filmlisten/NewFilmlistAudioLanguageTest.java new file mode 100644 index 000000000..6d0d34bae --- /dev/null +++ b/src/test/java/de/mediathekview/mserver/filmlisten/NewFilmlistAudioLanguageTest.java @@ -0,0 +1,93 @@ +package de.mediathekview.mserver.filmlisten; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import de.mediathekview.mserver.daten.Film; +import de.mediathekview.mserver.daten.FilmUrl; +import de.mediathekview.mserver.daten.Filmlist; +import de.mediathekview.mserver.daten.Resolution; +import de.mediathekview.mserver.daten.Sender; +import de.mediathekview.mserver.filmlisten.reader.FilmlistReader; +import de.mediathekview.mserver.filmlisten.writer.FilmlistWriter; +import java.io.FileInputStream; +import java.io.IOException; +import java.net.MalformedURLException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.Optional; +import java.util.UUID; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +/** Die Sprache muss auch durch das neue Format und ueber merge() hinweg erhalten bleiben. */ +class NewFilmlistAudioLanguageTest { + + @TempDir Path tempDir; + + @Test + void audioLanguageSurvivesTheNewFormat() throws IOException { + final Filmlist input = new Filmlist(); + final Film withLanguage = buildFilm("Mit Sprache (Originalversion)"); + withLanguage.setAudioLanguage("eng"); + input.add(withLanguage); + input.add(buildFilm("Ohne Sprache")); + + final Path tempFile = Files.createTempFile(tempDir, "NewFormatAudioLanguage", ".json"); + new FilmlistWriter().write(input, tempFile); + final Optional read = + new FilmlistReader().read(new FileInputStream(tempFile.toString())); + Files.deleteIfExists(tempFile); + + assertTrue(read.isPresent()); + assertEquals("eng", findByTitle(read.get(), "Mit Sprache (Originalversion)").getAudioLanguage()); + assertNull(findByTitle(read.get(), "Ohne Sprache").getAudioLanguage()); + } + + @Test + void mergeKeepsAKnownLanguage() throws MalformedURLException { + // Eine importierte aeltere Filmliste kennt das Feld nicht. Wird sie mit einem frischen Crawl + // zusammengefuehrt, darf die dort bekannte Sprache nicht verloren gehen. + final Film ausAelteremStand = buildFilm("Folge (Originalversion)"); + final Film ausFrischemCrawl = buildFilm("Folge (Originalversion)"); + ausFrischemCrawl.setAudioLanguage("fra"); + + ausAelteremStand.merge(ausFrischemCrawl); + + assertEquals("fra", ausAelteremStand.getAudioLanguage()); + } + + @Test + void mergeDoesNotOverwriteAnExistingLanguage() throws MalformedURLException { + final Film bekannt = buildFilm("Folge (Originalversion)"); + bekannt.setAudioLanguage("eng"); + final Film ohne = buildFilm("Folge (Originalversion)"); + + bekannt.merge(ohne); + + assertEquals("eng", bekannt.getAudioLanguage()); + } + + private Film buildFilm(final String titel) throws MalformedURLException { + final Film film = + new Film( + UUID.randomUUID(), + Sender.ARD, + titel, + "Testthema", + LocalDateTime.of(2026, 1, 1, 20, 15), + Duration.ofMinutes(90)); + film.addUrl(Resolution.NORMAL, new FilmUrl("https://example.org/v.mp4", 1L)); + return film; + } + + private Film findByTitle(final Filmlist list, final String titel) { + return list.getFilms().values().stream() + .filter(film -> titel.equals(film.getTitel())) + .findFirst() + .orElseThrow(); + } +} diff --git a/src/test/resources/ard/ard_item_OV_eng_plus_deu.json b/src/test/resources/ard/ard_item_OV_eng_plus_deu.json new file mode 100644 index 000000000..d8b8bb5ee --- /dev/null +++ b/src/test/resources/ard/ard_item_OV_eng_plus_deu.json @@ -0,0 +1,500 @@ +{ + "coreAssetType": "EXTRA_BONUS_CONTENT", + "fskRating": "FSK12", + "id": "player_ondemand-page-ard", + "isChildContent": false, + "personalized": false, + "links": { + "self": { + "id": "player_ondemand-page-ard", + "urlId": "player_ondemand-page-ard", + "title": "Folge 12: Der küssende Bandit (S04/E12) - (Originalversion)", + "href": "https://api.ardmediathek.de/page-gateway/pages/ard/item/Y3JpZDovL3dkci5kZS9CZWl0cmFnLXNvcGhvcmEtY2UwODAxZjMtNjIyMy00ZGUyLTg3YjMtNjE1MjJkYjJkNjVi?embedded=true&mcV6=true", + "type": "application/vnd.ard.page+json", + "partner": "ard" + } + }, + "title": "Folge 12: Der küssende Bandit (S04/E12) - (Originalversion)", + "tracking": { + "aggregationLevelId": 38, + "atiCustomVars": { + "clipTitle": "Folge 12: Der küssende Bandit (S04/E12) - (Originalversion)", + "mediaType": "video", + "lra": "ONE", + "channel": "ONE", + "show": "Murdoch Mysteries", + "contentTypes": "UT", + "mediaDistributionType": 1, + "contentId": 13320735, + "metadataId": "crid://wdr.de/Beitrag-sophora-ce0801f3-6223-4de2-87b3-61522db2d65b", + "clipLength": 2744 + }, + "chapter2": "Player", + "chapter3": "Murdoch Mysteries", + "environmentId": 511893, + "pageTitle": "Mediathek/Player/Murdoch Mysteries/Folge 12: Der küssende Bandit (S04/E12) - (Originalversion)/13320735/20240424_2050", + "szmType": "CP" + }, + "trackingPiano": { + "page_chapter1": "Player", + "page_content_id": "urn:ard:publication:0c1dc4d2e066396c", + "page_content_title": "Folge 12: Der küssende Bandit (S04/E12) - (Originalversion)", + "page_id": "player_ondemand-page-ard", + "page_institution": "ONE", + "page_institution_id": "urn:ard:institution:9edd94d68de90140", + "page_publisher": "ONE", + "page_publisher_id": "urn:ard:publisher:892e3c758ca56a9d", + "page_show": "Murdoch Mysteries", + "page_show_id": "urn:ard:show:85088a146d3740b7", + "page_title": "Folge 12: Der küssende Bandit (S04/E12) - (Originalversion)", + "site": "634408" + }, + "widgets": [ + { + "availableTo": "2024-05-24T20:50:00Z", + "binaryFeatures": [ + "OV", + "UT" + ], + "blockedByFsk": false, + "broadcastedOn": "2024-04-24T20:50:00Z", + "embeddable": false, + "geoblocked": false, + "id": "Y3JpZDovL3dkci5kZS9CZWl0cmFnLXNvcGhvcmEtY2UwODAxZjMtNjIyMy00ZGUyLTg3YjMtNjE1MjJkYjJkNjVi", + "image": { + "alt": "Detective Murdoch (Yannick Bisson) und Constable Crabtree (Jonny Harris)", + "producerName": "WDR", + "src": "https://api.ardmediathek.de/image-service/images/urn:ard:image:2a3352f634c1614c?w={width}&ch=ec0aa7bc7271383a", + "title": "Murdoch Mysteries (S04/E12) - OV" + }, + "isChildContent": false, + "maturityContentRating": "FSK12", + "mediaCollection": { + "embedded": { + "isGeoBlocked": true, + "meta": { + "availableToDateTime": "2024-05-24T20:50:00Z", + "broadcastedOnDateTime": "2024-04-24T20:50:00Z", + "clipSourceName": "ONE", + "durationSeconds": 2744, + "images": [ + { + "alt": "Detective Murdoch (Yannick Bisson) und Constable Crabtree (Jonny Harris)", + "aspectRatio": "16x9", + "imageSourceName": "WDR", + "kind": "preview", + "title": "Murdoch Mysteries (S04/E12) - OV", + "url": "https://api.ardmediathek.de/image-service/images/urn:ard:image:2a3352f634c1614c?w=960&ch=ec0aa7bc7271383a" + } + ], + "maturityContentRating": { + "age": 12, + "isBlocked": false, + "kind": "other" + }, + "publicationService": { + "id": "ard", + "name": "ONE", + "partner": "one" + }, + "synopsis": "Während sich Dr. Ogden auf ihre Hochzeit vorbereitet, muss Murdoch versuchen, den 'Küssenden Banditen' aufzuhalten, einen umstrittenen Bankräuber, der schnell zum Volkshelden aufsteigt.", + "title": "Folge 12: Der küssende Bandit (S04/E12) - (Originalversion)" + }, + "pluginData": { + "recommendation@all": { + "isAutoplay": true, + "timerSeconds": 10, + "url": "https://api.ardmediathek.de/page-gateway/compilations/ard/recommendations?contextItemId=Y3JpZDovL3dkci5kZS9CZWl0cmFnLXNvcGhvcmEtY2UwODAxZjMtNjIyMy00ZGUyLTg3YjMtNjE1MjJkYjJkNjVi" + }, + "trackingAgf@all": { + "appId": "PA02DC09C-B8D3-4098-87D2-2C023682D6D5", + "clipData": { + "assetid": "13320735", + "length": "2744", + "nol_c0": "p0,0", + "nol_c10": "p10,ONE", + "nol_c12": "p12,Content", + "nol_c15": "p15,POC_2002987622", + "nol_c16": "p16,", + "nol_c18": "p18,N", + "nol_c19": "p19,ONE", + "nol_c2": "p2,N", + "nol_c5": "p5,ARD Mediathek", + "nol_c7": "p7,crid://wdr.de/Beitrag-sophora-ce0801f3-6223-4de2-87b3-61522db2d65b", + "nol_c8": "p8,2744", + "nol_c9": "p9,Murdoch Mysteries_Folge 12: Der küssende Bandit (S04/E12) - (Originalversion)_24.04.2024 20:50", + "program": "Murdoch Mysteries", + "title": "ONE_Murdoch Mysteries_Folge 12: Der küssende Bandit (S04/E12) - (Originalversion)_24.04.2024 20:50", + "type": "content" + }, + "tracker": "AGF" + }, + "trackingAti@all": { + "config": { + "site": 511893 + }, + "isEnabled": true, + "richMedia": { + "broadcastMode": "clip", + "duration": "2744", + "mediaLabel": "Folge 12: Der küssende Bandit (S04/E12) - (Originalversion)__13320735__OV | UT", + "mediaLevel2": "{richMediaLevelTwo}", + "mediaTheme1": "ONE", + "mediaTheme2": "ONE", + "mediaTheme3": "Murdoch Mysteries", + "mediaType": "video", + "playerId": "urn:ard:publication:0c1dc4d2e066396c", + "refreshDuration": { + "0": 5, + "1": 12, + "12": 15, + "18": 30 + } + }, + "tracker": "ATI" + }, + "trackingPiano@all": { + "avContent": { + "av_broadcasting_type": "OnDemand", + "av_content": "Folge 12: Der küssende Bandit (S04/E12) - (Originalversion)", + "av_content_duration": 2744000, + "av_content_id": "urn:ard:publication:0c1dc4d2e066396c", + "av_content_type": "video", + "av_institution": "ONE", + "av_institution_id": "urn:ard:institution:9edd94d68de90140", + "av_publisher": "ONE", + "av_publisher_id": "urn:ard:publisher:892e3c758ca56a9d", + "av_show": "Murdoch Mysteries", + "av_show_id": "urn:ard:show:85088a146d3740b7" + }, + "config": { + "events": [ + "av.error", + "av.speed", + "av.quality", + "av.jumpmark", + "av.share", + "av.playermode", + "av.subtitle", + "av.language", + "av.audiodescription", + "av.signlanguage", + "av.volume.mute", + "av.recommendation" + ], + "site": 634408 + }, + "isEnabled": true + } + }, + "streams": [ + { + "kind": "main", + "kindName": "Normal", + "media": [ + { + "aspectRatio": "16:9", + "audios": [ + { + "kind": "standard", + "languageCode": "eng" + } + ], + "forcedLabel": "SD", + "hasEmbeddedSubtitles": false, + "isAdaptiveQualitySelectable": false, + "isHighDynamicRange": false, + "maxHResolutionPx": 640, + "maxVResolutionPx": 360, + "mimeType": "video/mp4", + "subtitles": [], + "url": "https://wdrmedien-a.akamaihd.net/medp/ondemand/de/fsk12/310/3103788/3103788_57016296.mp4", + "videoCodec": "H.264" + }, + { + "aspectRatio": "16:9", + "audios": [ + { + "kind": "standard", + "languageCode": "eng" + } + ], + "forcedLabel": "SD 480p", + "hasEmbeddedSubtitles": false, + "isAdaptiveQualitySelectable": false, + "isHighDynamicRange": false, + "maxHResolutionPx": 960, + "maxVResolutionPx": 540, + "mimeType": "video/mp4", + "subtitles": [], + "url": "https://wdrmedien-a.akamaihd.net/medp/ondemand/de/fsk12/310/3103788/3103788_57016297.mp4", + "videoCodec": "H.264" + }, + { + "aspectRatio": "16:9", + "audios": [ + { + "kind": "standard", + "languageCode": "eng" + } + ], + "forcedLabel": "Full HD", + "hasEmbeddedSubtitles": false, + "isAdaptiveQualitySelectable": false, + "isHighDynamicRange": false, + "maxHResolutionPx": 1920, + "maxVResolutionPx": 1080, + "mimeType": "video/mp4", + "subtitles": [], + "url": "https://wdrmedien-a.akamaihd.net/medp/ondemand/de/fsk12/310/3103788/3103788_57016294.mp4", + "videoCodec": "H.264" + }, + { + "aspectRatio": "16:9", + "audios": [ + { + "kind": "standard", + "languageCode": "eng" + } + ], + "forcedLabel": "Auto", + "hasEmbeddedSubtitles": false, + "isAdaptiveQualitySelectable": false, + "isHighDynamicRange": false, + "maxHResolutionPx": 1920, + "maxVResolutionPx": 1080, + "mimeType": "application/vnd.apple.mpegurl", + "subtitles": [], + "url": "https://wdradaptiv-vh.akamaihd.net/i/medp/ondemand/de/fsk12/310/3103788/,3103788_57016296,3103788_57016297,3103788_57016295,3103788_57016298,3103788_57016294,.mp4.csmil/master.m3u8", + "videoCodec": "H.264" + }, + { + "aspectRatio": "16:9", + "audios": [ + { + "kind": "standard", + "languageCode": "eng" + } + ], + "forcedLabel": "HD Ready", + "hasEmbeddedSubtitles": false, + "isAdaptiveQualitySelectable": false, + "isHighDynamicRange": false, + "maxHResolutionPx": 1280, + "maxVResolutionPx": 720, + "mimeType": "video/mp4", + "subtitles": [], + "url": "https://wdrmedien-a.akamaihd.net/medp/ondemand/de/fsk12/310/3103788/3103788_57016298.mp4", + "videoCodec": "H.264" + }, + { + "aspectRatio": "16:9", + "audios": [ + { + "kind": "standard", + "languageCode": "deu" + } + ], + "forcedLabel": "SD", + "hasEmbeddedSubtitles": false, + "isAdaptiveQualitySelectable": false, + "isHighDynamicRange": false, + "maxHResolutionPx": 640, + "maxVResolutionPx": 360, + "mimeType": "video/mp4", + "subtitles": [], + "url": "https://wdrmedien-a.akamaihd.net/medp/ondemand/de/fsk12/310/3103788/3103788_57016296_deu.mp4", + "videoCodec": "H.264" + }, + { + "aspectRatio": "16:9", + "audios": [ + { + "kind": "standard", + "languageCode": "deu" + } + ], + "forcedLabel": "SD 480p", + "hasEmbeddedSubtitles": false, + "isAdaptiveQualitySelectable": false, + "isHighDynamicRange": false, + "maxHResolutionPx": 960, + "maxVResolutionPx": 540, + "mimeType": "video/mp4", + "subtitles": [], + "url": "https://wdrmedien-a.akamaihd.net/medp/ondemand/de/fsk12/310/3103788/3103788_57016297_deu.mp4", + "videoCodec": "H.264" + }, + { + "aspectRatio": "16:9", + "audios": [ + { + "kind": "standard", + "languageCode": "deu" + } + ], + "forcedLabel": "Full HD", + "hasEmbeddedSubtitles": false, + "isAdaptiveQualitySelectable": false, + "isHighDynamicRange": false, + "maxHResolutionPx": 1920, + "maxVResolutionPx": 1080, + "mimeType": "video/mp4", + "subtitles": [], + "url": "https://wdrmedien-a.akamaihd.net/medp/ondemand/de/fsk12/310/3103788/3103788_57016294_deu.mp4", + "videoCodec": "H.264" + }, + { + "aspectRatio": "16:9", + "audios": [ + { + "kind": "standard", + "languageCode": "deu" + } + ], + "forcedLabel": "Auto", + "hasEmbeddedSubtitles": false, + "isAdaptiveQualitySelectable": false, + "isHighDynamicRange": false, + "maxHResolutionPx": 1920, + "maxVResolutionPx": 1080, + "mimeType": "application/vnd.apple.mpegurl", + "subtitles": [], + "url": "https://wdradaptiv-vh.akamaihd.net/i/medp/ondemand/de/fsk12/310/3103788/,3103788_57016296,3103788_57016297,3103788_57016295,3103788_57016298,3103788_57016294,_deu.mp4.csmil/master.m3u8", + "videoCodec": "H.264" + }, + { + "aspectRatio": "16:9", + "audios": [ + { + "kind": "standard", + "languageCode": "deu" + } + ], + "forcedLabel": "HD Ready", + "hasEmbeddedSubtitles": false, + "isAdaptiveQualitySelectable": false, + "isHighDynamicRange": false, + "maxHResolutionPx": 1280, + "maxVResolutionPx": 720, + "mimeType": "video/mp4", + "subtitles": [], + "url": "https://wdrmedien-a.akamaihd.net/medp/ondemand/de/fsk12/310/3103788/3103788_57016298_deu.mp4", + "videoCodec": "H.264" + } + ], + "videoLanguageCode": "" + } + ], + "subtitles": [ + { + "kind": "normal", + "languageCode": "deu", + "sources": [ + { + "kind": "ebutt", + "url": "https://api.ardmediathek.de/player-service/subtitle/ebutt/urn:ard:subtitle:d0e38dd26e6cc85e" + }, + { + "kind": "webvtt", + "url": "https://api.ardmediathek.de/player-service/subtitle/webvtt/urn:ard:subtitle:d0e38dd26e6cc85e.vtt" + } + ] + } + ] + }, + "href": "https://api.ardmediathek.de/page-gateway/mediacollectionv6/Y3JpZDovL3dkci5kZS9CZWl0cmFnLXNvcGhvcmEtY2UwODAxZjMtNjIyMy00ZGUyLTg3YjMtNjE1MjJkYjJkNjVi?isTv=false" + }, + "pagination": null, + "personalized": false, + "playerConfig": null, + "publicationService": { + "name": "ONE", + "logo": { + "title": "Logo ONE", + "alt": "Logo ONE", + "producerName": "Logo ONE", + "src": "https://api.ardmediathek.de/image-service/image-collections/urn:ard:image-collection:2213972a8d101de1/16x9?w={width}", + "aspectRatio": "16x9" + }, + "publisherType": "TV", + "partner": "one", + "id": "b3JnYW5pemF0aW9uX09ORQ", + "coreId": "urn:ard:publisher:892e3c758ca56a9d" + }, + "links": { + "self": { + "id": "player_on_demand_widget_ard", + "urlId": "player_on_demand_widget_ard", + "title": "Folge 12: Der küssende Bandit (S04/E12) - (Originalversion)", + "href": "https://api.ardmediathek.de/page-gateway/widgets/ard/editorials/player_on_demand_widget_ard/item/Y3JpZDovL3dkci5kZS9CZWl0cmFnLXNvcGhvcmEtY2UwODAxZjMtNjIyMy00ZGUyLTg3YjMtNjE1MjJkYjJkNjVi?pageNumber=0&pageSize=100&embedded=true&mcV6=true", + "type": "application/vnd.ard.widget+json", + "partner": "ard" + } + }, + "show": { + "id": "Y3JpZDovL3dkci5kZTdvbmUvbXVyZG9jaG15c3Rlcmllcw", + "title": "Murdoch Mysteries", + "image": { + "alt": "Sendungslogo \"Murdoch Mysteries\"", + "producerName": "Shaftesbury Murdoch II Inc", + "src": "https://api.ardmediathek.de/image-service/images/urn:ard:image:559937f982bde0f5?w={width}&ch=dc9ee6ab714f2b88", + "title": "Murdoch Mysteries" + }, + "availableSeasons": [ + "1", + "4" + ], + "binaryFeatures": [ + "UT", + "OV" + ], + "coreAssetType": "SEASON_SERIES" + }, + "synopsis": "Während sich Dr. Ogden auf ihre Hochzeit vorbereitet, muss Murdoch versuchen, den 'Küssenden Banditen' aufzuhalten, einen umstrittenen Bankräuber, der schnell zum Volkshelden aufsteigt.", + "title": "Folge 12: Der küssende Bandit (S04/E12) - (Originalversion)", + "trackingPiano": { + "teaser_recommended": false, + "widget_id": "player_on_demand_widget_ard", + "widget_title": "Folge 12: Der küssende Bandit (S04/E12) - (Originalversion)", + "widget_typ": "player_ondemand" + }, + "type": "player_ondemand" + }, + { + "aZContent": false, + "compilationType": "itemsOfShow", + "id": "Y3JpZDovL3dkci5kZTdvbmUvbXVyZG9jaG15c3Rlcmllcw", + "isChildContent": false, + "pagination": { + "pageNumber": 0, + "pageSize": 24, + "totalElements": 7 + }, + "personalized": false, + "links": { + "self": { + "id": "Y3JpZDovL3dkci5kZTdvbmUvbXVyZG9jaG15c3Rlcmllcw", + "urlId": "Y3JpZDovL3dkci5kZTdvbmUvbXVyZG9jaG15c3Rlcmllcw", + "title": "Murdoch Mysteries", + "href": "https://api.ardmediathek.de/page-gateway/widgets/ard/asset/Y3JpZDovL3dkci5kZTdvbmUvbXVyZG9jaG15c3Rlcmllcw?excludedAssetIds=Y3JpZDovL3dkci5kZS9CZWl0cmFnLXNvcGhvcmEtY2UwODAxZjMtNjIyMy00ZGUyLTg3YjMtNjE1MjJkYjJkNjVi&pageNumber=0&pageSize=24&embedded=true&seasoned=false&seasonNumber=&withAudiodescription=false&withOriginalWithSubtitle=false&withOriginalversion=false&single=false", + "type": "application/vnd.ard.widget+json", + "partner": "ard" + } + }, + "size": "m", + "swipeable": true, + "teasers": [], + "title": "Murdoch Mysteries", + "titleVisible": true, + "trackingPiano": { + "teaser_recommended": false, + "widget_id": "Y3JpZDovL3dkci5kZTdvbmUvbXVyZG9jaG15c3Rlcmllcw", + "widget_title": "Murdoch Mysteries", + "widget_typ": "gridlist" + }, + "type": "gridlist", + "userVisibility": "ALWAYS" + } + ] +} \ No newline at end of file diff --git a/src/test/resources/ard/ard_item_OV_spanish.json b/src/test/resources/ard/ard_item_OV_spanish.json new file mode 100644 index 000000000..50ccda464 --- /dev/null +++ b/src/test/resources/ard/ard_item_OV_spanish.json @@ -0,0 +1,405 @@ +{ + "coreAssetType": "EXTRA_BONUS_CONTENT", + "fskRating": "FSK12", + "id": "player_ondemand-page-ard", + "isChildContent": false, + "personalized": false, + "links": { + "self": { + "id": "player_ondemand-page-ard", + "urlId": "player_ondemand-page-ard", + "title": "Folge 12: Der küssende Bandit (S04/E12) - (Originalversion)", + "href": "https://api.ardmediathek.de/page-gateway/pages/ard/item/Y3JpZDovL3dkci5kZS9CZWl0cmFnLXNvcGhvcmEtY2UwODAxZjMtNjIyMy00ZGUyLTg3YjMtNjE1MjJkYjJkNjVi?embedded=true&mcV6=true", + "type": "application/vnd.ard.page+json", + "partner": "ard" + } + }, + "title": "Folge 12: Der küssende Bandit (S04/E12) - (Originalversion)", + "tracking": { + "aggregationLevelId": 38, + "atiCustomVars": { + "clipTitle": "Folge 12: Der küssende Bandit (S04/E12) - (Originalversion)", + "mediaType": "video", + "lra": "ONE", + "channel": "ONE", + "show": "Murdoch Mysteries", + "contentTypes": "UT", + "mediaDistributionType": 1, + "contentId": 13320735, + "metadataId": "crid://wdr.de/Beitrag-sophora-ce0801f3-6223-4de2-87b3-61522db2d65b", + "clipLength": 2744 + }, + "chapter2": "Player", + "chapter3": "Murdoch Mysteries", + "environmentId": 511893, + "pageTitle": "Mediathek/Player/Murdoch Mysteries/Folge 12: Der küssende Bandit (S04/E12) - (Originalversion)/13320735/20240424_2050", + "szmType": "CP" + }, + "trackingPiano": { + "page_chapter1": "Player", + "page_content_id": "urn:ard:publication:0c1dc4d2e066396c", + "page_content_title": "Folge 12: Der küssende Bandit (S04/E12) - (Originalversion)", + "page_id": "player_ondemand-page-ard", + "page_institution": "ONE", + "page_institution_id": "urn:ard:institution:9edd94d68de90140", + "page_publisher": "ONE", + "page_publisher_id": "urn:ard:publisher:892e3c758ca56a9d", + "page_show": "Murdoch Mysteries", + "page_show_id": "urn:ard:show:85088a146d3740b7", + "page_title": "Folge 12: Der küssende Bandit (S04/E12) - (Originalversion)", + "site": "634408" + }, + "widgets": [ + { + "availableTo": "2024-05-24T20:50:00Z", + "binaryFeatures": [ + "OV", + "UT" + ], + "blockedByFsk": false, + "broadcastedOn": "2024-04-24T20:50:00Z", + "embeddable": false, + "geoblocked": false, + "id": "Y3JpZDovL3dkci5kZS9CZWl0cmFnLXNvcGhvcmEtY2UwODAxZjMtNjIyMy00ZGUyLTg3YjMtNjE1MjJkYjJkNjVi", + "image": { + "alt": "Detective Murdoch (Yannick Bisson) und Constable Crabtree (Jonny Harris)", + "producerName": "WDR", + "src": "https://api.ardmediathek.de/image-service/images/urn:ard:image:2a3352f634c1614c?w={width}&ch=ec0aa7bc7271383a", + "title": "Murdoch Mysteries (S04/E12) - OV" + }, + "isChildContent": false, + "maturityContentRating": "FSK12", + "mediaCollection": { + "embedded": { + "isGeoBlocked": true, + "meta": { + "availableToDateTime": "2024-05-24T20:50:00Z", + "broadcastedOnDateTime": "2024-04-24T20:50:00Z", + "clipSourceName": "ONE", + "durationSeconds": 2744, + "images": [ + { + "alt": "Detective Murdoch (Yannick Bisson) und Constable Crabtree (Jonny Harris)", + "aspectRatio": "16x9", + "imageSourceName": "WDR", + "kind": "preview", + "title": "Murdoch Mysteries (S04/E12) - OV", + "url": "https://api.ardmediathek.de/image-service/images/urn:ard:image:2a3352f634c1614c?w=960&ch=ec0aa7bc7271383a" + } + ], + "maturityContentRating": { + "age": 12, + "isBlocked": false, + "kind": "other" + }, + "publicationService": { + "id": "ard", + "name": "ONE", + "partner": "one" + }, + "synopsis": "Während sich Dr. Ogden auf ihre Hochzeit vorbereitet, muss Murdoch versuchen, den 'Küssenden Banditen' aufzuhalten, einen umstrittenen Bankräuber, der schnell zum Volkshelden aufsteigt.", + "title": "Folge 12: Der küssende Bandit (S04/E12) - (Originalversion)" + }, + "pluginData": { + "recommendation@all": { + "isAutoplay": true, + "timerSeconds": 10, + "url": "https://api.ardmediathek.de/page-gateway/compilations/ard/recommendations?contextItemId=Y3JpZDovL3dkci5kZS9CZWl0cmFnLXNvcGhvcmEtY2UwODAxZjMtNjIyMy00ZGUyLTg3YjMtNjE1MjJkYjJkNjVi" + }, + "trackingAgf@all": { + "appId": "PA02DC09C-B8D3-4098-87D2-2C023682D6D5", + "clipData": { + "assetid": "13320735", + "length": "2744", + "nol_c0": "p0,0", + "nol_c10": "p10,ONE", + "nol_c12": "p12,Content", + "nol_c15": "p15,POC_2002987622", + "nol_c16": "p16,", + "nol_c18": "p18,N", + "nol_c19": "p19,ONE", + "nol_c2": "p2,N", + "nol_c5": "p5,ARD Mediathek", + "nol_c7": "p7,crid://wdr.de/Beitrag-sophora-ce0801f3-6223-4de2-87b3-61522db2d65b", + "nol_c8": "p8,2744", + "nol_c9": "p9,Murdoch Mysteries_Folge 12: Der küssende Bandit (S04/E12) - (Originalversion)_24.04.2024 20:50", + "program": "Murdoch Mysteries", + "title": "ONE_Murdoch Mysteries_Folge 12: Der küssende Bandit (S04/E12) - (Originalversion)_24.04.2024 20:50", + "type": "content" + }, + "tracker": "AGF" + }, + "trackingAti@all": { + "config": { + "site": 511893 + }, + "isEnabled": true, + "richMedia": { + "broadcastMode": "clip", + "duration": "2744", + "mediaLabel": "Folge 12: Der küssende Bandit (S04/E12) - (Originalversion)__13320735__OV | UT", + "mediaLevel2": "{richMediaLevelTwo}", + "mediaTheme1": "ONE", + "mediaTheme2": "ONE", + "mediaTheme3": "Murdoch Mysteries", + "mediaType": "video", + "playerId": "urn:ard:publication:0c1dc4d2e066396c", + "refreshDuration": { + "0": 5, + "1": 12, + "12": 15, + "18": 30 + } + }, + "tracker": "ATI" + }, + "trackingPiano@all": { + "avContent": { + "av_broadcasting_type": "OnDemand", + "av_content": "Folge 12: Der küssende Bandit (S04/E12) - (Originalversion)", + "av_content_duration": 2744000, + "av_content_id": "urn:ard:publication:0c1dc4d2e066396c", + "av_content_type": "video", + "av_institution": "ONE", + "av_institution_id": "urn:ard:institution:9edd94d68de90140", + "av_publisher": "ONE", + "av_publisher_id": "urn:ard:publisher:892e3c758ca56a9d", + "av_show": "Murdoch Mysteries", + "av_show_id": "urn:ard:show:85088a146d3740b7" + }, + "config": { + "events": [ + "av.error", + "av.speed", + "av.quality", + "av.jumpmark", + "av.share", + "av.playermode", + "av.subtitle", + "av.language", + "av.audiodescription", + "av.signlanguage", + "av.volume.mute", + "av.recommendation" + ], + "site": 634408 + }, + "isEnabled": true + } + }, + "streams": [ + { + "kind": "main", + "kindName": "Normal", + "media": [ + { + "aspectRatio": "16:9", + "audios": [ + { + "kind": "standard", + "languageCode": "spa" + } + ], + "forcedLabel": "SD", + "hasEmbeddedSubtitles": false, + "isAdaptiveQualitySelectable": false, + "isHighDynamicRange": false, + "maxHResolutionPx": 640, + "maxVResolutionPx": 360, + "mimeType": "video/mp4", + "subtitles": [], + "url": "https://wdrmedien-a.akamaihd.net/medp/ondemand/de/fsk12/310/3103788/3103788_57016296.mp4", + "videoCodec": "H.264" + }, + { + "aspectRatio": "16:9", + "audios": [ + { + "kind": "standard", + "languageCode": "spa" + } + ], + "forcedLabel": "SD 480p", + "hasEmbeddedSubtitles": false, + "isAdaptiveQualitySelectable": false, + "isHighDynamicRange": false, + "maxHResolutionPx": 960, + "maxVResolutionPx": 540, + "mimeType": "video/mp4", + "subtitles": [], + "url": "https://wdrmedien-a.akamaihd.net/medp/ondemand/de/fsk12/310/3103788/3103788_57016297.mp4", + "videoCodec": "H.264" + }, + { + "aspectRatio": "16:9", + "audios": [ + { + "kind": "standard", + "languageCode": "spa" + } + ], + "forcedLabel": "Full HD", + "hasEmbeddedSubtitles": false, + "isAdaptiveQualitySelectable": false, + "isHighDynamicRange": false, + "maxHResolutionPx": 1920, + "maxVResolutionPx": 1080, + "mimeType": "video/mp4", + "subtitles": [], + "url": "https://wdrmedien-a.akamaihd.net/medp/ondemand/de/fsk12/310/3103788/3103788_57016294.mp4", + "videoCodec": "H.264" + }, + { + "aspectRatio": "16:9", + "audios": [ + { + "kind": "standard", + "languageCode": "spa" + } + ], + "forcedLabel": "Auto", + "hasEmbeddedSubtitles": false, + "isAdaptiveQualitySelectable": false, + "isHighDynamicRange": false, + "maxHResolutionPx": 1920, + "maxVResolutionPx": 1080, + "mimeType": "application/vnd.apple.mpegurl", + "subtitles": [], + "url": "https://wdradaptiv-vh.akamaihd.net/i/medp/ondemand/de/fsk12/310/3103788/,3103788_57016296,3103788_57016297,3103788_57016295,3103788_57016298,3103788_57016294,.mp4.csmil/master.m3u8", + "videoCodec": "H.264" + }, + { + "aspectRatio": "16:9", + "audios": [ + { + "kind": "standard", + "languageCode": "spa" + } + ], + "forcedLabel": "HD Ready", + "hasEmbeddedSubtitles": false, + "isAdaptiveQualitySelectable": false, + "isHighDynamicRange": false, + "maxHResolutionPx": 1280, + "maxVResolutionPx": 720, + "mimeType": "video/mp4", + "subtitles": [], + "url": "https://wdrmedien-a.akamaihd.net/medp/ondemand/de/fsk12/310/3103788/3103788_57016298.mp4", + "videoCodec": "H.264" + } + ], + "videoLanguageCode": "" + } + ], + "subtitles": [ + { + "kind": "normal", + "languageCode": "deu", + "sources": [ + { + "kind": "ebutt", + "url": "https://api.ardmediathek.de/player-service/subtitle/ebutt/urn:ard:subtitle:d0e38dd26e6cc85e" + }, + { + "kind": "webvtt", + "url": "https://api.ardmediathek.de/player-service/subtitle/webvtt/urn:ard:subtitle:d0e38dd26e6cc85e.vtt" + } + ] + } + ] + }, + "href": "https://api.ardmediathek.de/page-gateway/mediacollectionv6/Y3JpZDovL3dkci5kZS9CZWl0cmFnLXNvcGhvcmEtY2UwODAxZjMtNjIyMy00ZGUyLTg3YjMtNjE1MjJkYjJkNjVi?isTv=false" + }, + "pagination": null, + "personalized": false, + "playerConfig": null, + "publicationService": { + "name": "ONE", + "logo": { + "title": "Logo ONE", + "alt": "Logo ONE", + "producerName": "Logo ONE", + "src": "https://api.ardmediathek.de/image-service/image-collections/urn:ard:image-collection:2213972a8d101de1/16x9?w={width}", + "aspectRatio": "16x9" + }, + "publisherType": "TV", + "partner": "one", + "id": "b3JnYW5pemF0aW9uX09ORQ", + "coreId": "urn:ard:publisher:892e3c758ca56a9d" + }, + "links": { + "self": { + "id": "player_on_demand_widget_ard", + "urlId": "player_on_demand_widget_ard", + "title": "Folge 12: Der küssende Bandit (S04/E12) - (Originalversion)", + "href": "https://api.ardmediathek.de/page-gateway/widgets/ard/editorials/player_on_demand_widget_ard/item/Y3JpZDovL3dkci5kZS9CZWl0cmFnLXNvcGhvcmEtY2UwODAxZjMtNjIyMy00ZGUyLTg3YjMtNjE1MjJkYjJkNjVi?pageNumber=0&pageSize=100&embedded=true&mcV6=true", + "type": "application/vnd.ard.widget+json", + "partner": "ard" + } + }, + "show": { + "id": "Y3JpZDovL3dkci5kZTdvbmUvbXVyZG9jaG15c3Rlcmllcw", + "title": "Murdoch Mysteries", + "image": { + "alt": "Sendungslogo \"Murdoch Mysteries\"", + "producerName": "Shaftesbury Murdoch II Inc", + "src": "https://api.ardmediathek.de/image-service/images/urn:ard:image:559937f982bde0f5?w={width}&ch=dc9ee6ab714f2b88", + "title": "Murdoch Mysteries" + }, + "availableSeasons": [ + "1", + "4" + ], + "binaryFeatures": [ + "UT", + "OV" + ], + "coreAssetType": "SEASON_SERIES" + }, + "synopsis": "Während sich Dr. Ogden auf ihre Hochzeit vorbereitet, muss Murdoch versuchen, den 'Küssenden Banditen' aufzuhalten, einen umstrittenen Bankräuber, der schnell zum Volkshelden aufsteigt.", + "title": "Folge 12: Der küssende Bandit (S04/E12) - (Originalversion)", + "trackingPiano": { + "teaser_recommended": false, + "widget_id": "player_on_demand_widget_ard", + "widget_title": "Folge 12: Der küssende Bandit (S04/E12) - (Originalversion)", + "widget_typ": "player_ondemand" + }, + "type": "player_ondemand" + }, + { + "aZContent": false, + "compilationType": "itemsOfShow", + "id": "Y3JpZDovL3dkci5kZTdvbmUvbXVyZG9jaG15c3Rlcmllcw", + "isChildContent": false, + "pagination": { + "pageNumber": 0, + "pageSize": 24, + "totalElements": 7 + }, + "personalized": false, + "links": { + "self": { + "id": "Y3JpZDovL3dkci5kZTdvbmUvbXVyZG9jaG15c3Rlcmllcw", + "urlId": "Y3JpZDovL3dkci5kZTdvbmUvbXVyZG9jaG15c3Rlcmllcw", + "title": "Murdoch Mysteries", + "href": "https://api.ardmediathek.de/page-gateway/widgets/ard/asset/Y3JpZDovL3dkci5kZTdvbmUvbXVyZG9jaG15c3Rlcmllcw?excludedAssetIds=Y3JpZDovL3dkci5kZS9CZWl0cmFnLXNvcGhvcmEtY2UwODAxZjMtNjIyMy00ZGUyLTg3YjMtNjE1MjJkYjJkNjVi&pageNumber=0&pageSize=24&embedded=true&seasoned=false&seasonNumber=&withAudiodescription=false&withOriginalWithSubtitle=false&withOriginalversion=false&single=false", + "type": "application/vnd.ard.widget+json", + "partner": "ard" + } + }, + "size": "m", + "swipeable": true, + "teasers": [], + "title": "Murdoch Mysteries", + "titleVisible": true, + "trackingPiano": { + "teaser_recommended": false, + "widget_id": "Y3JpZDovL3dkci5kZTdvbmUvbXVyZG9jaG15c3Rlcmllcw", + "widget_title": "Murdoch Mysteries", + "widget_typ": "gridlist" + }, + "type": "gridlist", + "userVisibility": "ALWAYS" + } + ] +} \ No newline at end of file diff --git a/src/test/resources/ard/ard_item_OV_spanish_region.json b/src/test/resources/ard/ard_item_OV_spanish_region.json new file mode 100644 index 000000000..ace2817fd --- /dev/null +++ b/src/test/resources/ard/ard_item_OV_spanish_region.json @@ -0,0 +1 @@ +{"coreAssetType": "EXTRA_BONUS_CONTENT", "fskRating": "FSK12", "id": "player_ondemand-page-ard", "isChildContent": false, "personalized": false, "links": {"self": {"id": "player_ondemand-page-ard", "urlId": "player_ondemand-page-ard", "title": "Folge 12: Der küssende Bandit (S04/E12) - (Originalversion)", "href": "https://api.ardmediathek.de/page-gateway/pages/ard/item/Y3JpZDovL3dkci5kZS9CZWl0cmFnLXNvcGhvcmEtY2UwODAxZjMtNjIyMy00ZGUyLTg3YjMtNjE1MjJkYjJkNjVi?embedded=true&mcV6=true", "type": "application/vnd.ard.page+json", "partner": "ard"}}, "title": "Folge 12: Der küssende Bandit (S04/E12) - (Originalversion)", "tracking": {"aggregationLevelId": 38, "atiCustomVars": {"clipTitle": "Folge 12: Der küssende Bandit (S04/E12) - (Originalversion)", "mediaType": "video", "lra": "ONE", "channel": "ONE", "show": "Murdoch Mysteries", "contentTypes": "UT", "mediaDistributionType": 1, "contentId": 13320735, "metadataId": "crid://wdr.de/Beitrag-sophora-ce0801f3-6223-4de2-87b3-61522db2d65b", "clipLength": 2744}, "chapter2": "Player", "chapter3": "Murdoch Mysteries", "environmentId": 511893, "pageTitle": "Mediathek/Player/Murdoch Mysteries/Folge 12: Der küssende Bandit (S04/E12) - (Originalversion)/13320735/20240424_2050", "szmType": "CP"}, "trackingPiano": {"page_chapter1": "Player", "page_content_id": "urn:ard:publication:0c1dc4d2e066396c", "page_content_title": "Folge 12: Der küssende Bandit (S04/E12) - (Originalversion)", "page_id": "player_ondemand-page-ard", "page_institution": "ONE", "page_institution_id": "urn:ard:institution:9edd94d68de90140", "page_publisher": "ONE", "page_publisher_id": "urn:ard:publisher:892e3c758ca56a9d", "page_show": "Murdoch Mysteries", "page_show_id": "urn:ard:show:85088a146d3740b7", "page_title": "Folge 12: Der küssende Bandit (S04/E12) - (Originalversion)", "site": "634408"}, "widgets": [{"availableTo": "2024-05-24T20:50:00Z", "binaryFeatures": ["OV", "UT"], "blockedByFsk": false, "broadcastedOn": "2024-04-24T20:50:00Z", "embeddable": false, "geoblocked": false, "id": "Y3JpZDovL3dkci5kZS9CZWl0cmFnLXNvcGhvcmEtY2UwODAxZjMtNjIyMy00ZGUyLTg3YjMtNjE1MjJkYjJkNjVi", "image": {"alt": "Detective Murdoch (Yannick Bisson) und Constable Crabtree (Jonny Harris)", "producerName": "WDR", "src": "https://api.ardmediathek.de/image-service/images/urn:ard:image:2a3352f634c1614c?w={width}&ch=ec0aa7bc7271383a", "title": "Murdoch Mysteries (S04/E12) - OV"}, "isChildContent": false, "maturityContentRating": "FSK12", "mediaCollection": {"embedded": {"isGeoBlocked": true, "meta": {"availableToDateTime": "2024-05-24T20:50:00Z", "broadcastedOnDateTime": "2024-04-24T20:50:00Z", "clipSourceName": "ONE", "durationSeconds": 2744, "images": [{"alt": "Detective Murdoch (Yannick Bisson) und Constable Crabtree (Jonny Harris)", "aspectRatio": "16x9", "imageSourceName": "WDR", "kind": "preview", "title": "Murdoch Mysteries (S04/E12) - OV", "url": "https://api.ardmediathek.de/image-service/images/urn:ard:image:2a3352f634c1614c?w=960&ch=ec0aa7bc7271383a"}], "maturityContentRating": {"age": 12, "isBlocked": false, "kind": "other"}, "publicationService": {"id": "ard", "name": "ONE", "partner": "one"}, "synopsis": "Während sich Dr. Ogden auf ihre Hochzeit vorbereitet, muss Murdoch versuchen, den 'Küssenden Banditen' aufzuhalten, einen umstrittenen Bankräuber, der schnell zum Volkshelden aufsteigt.", "title": "Folge 12: Der küssende Bandit (S04/E12) - (Originalversion)"}, "pluginData": {"recommendation@all": {"isAutoplay": true, "timerSeconds": 10, "url": "https://api.ardmediathek.de/page-gateway/compilations/ard/recommendations?contextItemId=Y3JpZDovL3dkci5kZS9CZWl0cmFnLXNvcGhvcmEtY2UwODAxZjMtNjIyMy00ZGUyLTg3YjMtNjE1MjJkYjJkNjVi"}, "trackingAgf@all": {"appId": "PA02DC09C-B8D3-4098-87D2-2C023682D6D5", "clipData": {"assetid": "13320735", "length": "2744", "nol_c0": "p0,0", "nol_c10": "p10,ONE", "nol_c12": "p12,Content", "nol_c15": "p15,POC_2002987622", "nol_c16": "p16,", "nol_c18": "p18,N", "nol_c19": "p19,ONE", "nol_c2": "p2,N", "nol_c5": "p5,ARD Mediathek", "nol_c7": "p7,crid://wdr.de/Beitrag-sophora-ce0801f3-6223-4de2-87b3-61522db2d65b", "nol_c8": "p8,2744", "nol_c9": "p9,Murdoch Mysteries_Folge 12: Der küssende Bandit (S04/E12) - (Originalversion)_24.04.2024 20:50", "program": "Murdoch Mysteries", "title": "ONE_Murdoch Mysteries_Folge 12: Der küssende Bandit (S04/E12) - (Originalversion)_24.04.2024 20:50", "type": "content"}, "tracker": "AGF"}, "trackingAti@all": {"config": {"site": 511893}, "isEnabled": true, "richMedia": {"broadcastMode": "clip", "duration": "2744", "mediaLabel": "Folge 12: Der küssende Bandit (S04/E12) - (Originalversion)__13320735__OV | UT", "mediaLevel2": "{richMediaLevelTwo}", "mediaTheme1": "ONE", "mediaTheme2": "ONE", "mediaTheme3": "Murdoch Mysteries", "mediaType": "video", "playerId": "urn:ard:publication:0c1dc4d2e066396c", "refreshDuration": {"0": 5, "1": 12, "12": 15, "18": 30}}, "tracker": "ATI"}, "trackingPiano@all": {"avContent": {"av_broadcasting_type": "OnDemand", "av_content": "Folge 12: Der küssende Bandit (S04/E12) - (Originalversion)", "av_content_duration": 2744000, "av_content_id": "urn:ard:publication:0c1dc4d2e066396c", "av_content_type": "video", "av_institution": "ONE", "av_institution_id": "urn:ard:institution:9edd94d68de90140", "av_publisher": "ONE", "av_publisher_id": "urn:ard:publisher:892e3c758ca56a9d", "av_show": "Murdoch Mysteries", "av_show_id": "urn:ard:show:85088a146d3740b7"}, "config": {"events": ["av.error", "av.speed", "av.quality", "av.jumpmark", "av.share", "av.playermode", "av.subtitle", "av.language", "av.audiodescription", "av.signlanguage", "av.volume.mute", "av.recommendation"], "site": 634408}, "isEnabled": true}}, "streams": [{"kind": "main", "kindName": "Normal", "media": [{"aspectRatio": "16:9", "audios": [{"kind": "standard", "languageCode": "spa-ES"}], "forcedLabel": "SD", "hasEmbeddedSubtitles": false, "isAdaptiveQualitySelectable": false, "isHighDynamicRange": false, "maxHResolutionPx": 640, "maxVResolutionPx": 360, "mimeType": "video/mp4", "subtitles": [], "url": "https://wdrmedien-a.akamaihd.net/medp/ondemand/de/fsk12/310/3103788/3103788_57016296.mp4", "videoCodec": "H.264"}, {"aspectRatio": "16:9", "audios": [{"kind": "standard", "languageCode": "spa-ES"}], "forcedLabel": "SD 480p", "hasEmbeddedSubtitles": false, "isAdaptiveQualitySelectable": false, "isHighDynamicRange": false, "maxHResolutionPx": 960, "maxVResolutionPx": 540, "mimeType": "video/mp4", "subtitles": [], "url": "https://wdrmedien-a.akamaihd.net/medp/ondemand/de/fsk12/310/3103788/3103788_57016297.mp4", "videoCodec": "H.264"}, {"aspectRatio": "16:9", "audios": [{"kind": "standard", "languageCode": "spa-ES"}], "forcedLabel": "Full HD", "hasEmbeddedSubtitles": false, "isAdaptiveQualitySelectable": false, "isHighDynamicRange": false, "maxHResolutionPx": 1920, "maxVResolutionPx": 1080, "mimeType": "video/mp4", "subtitles": [], "url": "https://wdrmedien-a.akamaihd.net/medp/ondemand/de/fsk12/310/3103788/3103788_57016294.mp4", "videoCodec": "H.264"}, {"aspectRatio": "16:9", "audios": [{"kind": "standard", "languageCode": "spa-ES"}], "forcedLabel": "Auto", "hasEmbeddedSubtitles": false, "isAdaptiveQualitySelectable": false, "isHighDynamicRange": false, "maxHResolutionPx": 1920, "maxVResolutionPx": 1080, "mimeType": "application/vnd.apple.mpegurl", "subtitles": [], "url": "https://wdradaptiv-vh.akamaihd.net/i/medp/ondemand/de/fsk12/310/3103788/,3103788_57016296,3103788_57016297,3103788_57016295,3103788_57016298,3103788_57016294,.mp4.csmil/master.m3u8", "videoCodec": "H.264"}, {"aspectRatio": "16:9", "audios": [{"kind": "standard", "languageCode": "spa-ES"}], "forcedLabel": "HD Ready", "hasEmbeddedSubtitles": false, "isAdaptiveQualitySelectable": false, "isHighDynamicRange": false, "maxHResolutionPx": 1280, "maxVResolutionPx": 720, "mimeType": "video/mp4", "subtitles": [], "url": "https://wdrmedien-a.akamaihd.net/medp/ondemand/de/fsk12/310/3103788/3103788_57016298.mp4", "videoCodec": "H.264"}], "videoLanguageCode": ""}], "subtitles": [{"kind": "normal", "languageCode": "deu", "sources": [{"kind": "ebutt", "url": "https://api.ardmediathek.de/player-service/subtitle/ebutt/urn:ard:subtitle:d0e38dd26e6cc85e"}, {"kind": "webvtt", "url": "https://api.ardmediathek.de/player-service/subtitle/webvtt/urn:ard:subtitle:d0e38dd26e6cc85e.vtt"}]}]}, "href": "https://api.ardmediathek.de/page-gateway/mediacollectionv6/Y3JpZDovL3dkci5kZS9CZWl0cmFnLXNvcGhvcmEtY2UwODAxZjMtNjIyMy00ZGUyLTg3YjMtNjE1MjJkYjJkNjVi?isTv=false"}, "pagination": null, "personalized": false, "playerConfig": null, "publicationService": {"name": "ONE", "logo": {"title": "Logo ONE", "alt": "Logo ONE", "producerName": "Logo ONE", "src": "https://api.ardmediathek.de/image-service/image-collections/urn:ard:image-collection:2213972a8d101de1/16x9?w={width}", "aspectRatio": "16x9"}, "publisherType": "TV", "partner": "one", "id": "b3JnYW5pemF0aW9uX09ORQ", "coreId": "urn:ard:publisher:892e3c758ca56a9d"}, "links": {"self": {"id": "player_on_demand_widget_ard", "urlId": "player_on_demand_widget_ard", "title": "Folge 12: Der küssende Bandit (S04/E12) - (Originalversion)", "href": "https://api.ardmediathek.de/page-gateway/widgets/ard/editorials/player_on_demand_widget_ard/item/Y3JpZDovL3dkci5kZS9CZWl0cmFnLXNvcGhvcmEtY2UwODAxZjMtNjIyMy00ZGUyLTg3YjMtNjE1MjJkYjJkNjVi?pageNumber=0&pageSize=100&embedded=true&mcV6=true", "type": "application/vnd.ard.widget+json", "partner": "ard"}}, "show": {"id": "Y3JpZDovL3dkci5kZTdvbmUvbXVyZG9jaG15c3Rlcmllcw", "title": "Murdoch Mysteries", "image": {"alt": "Sendungslogo \"Murdoch Mysteries\"", "producerName": "Shaftesbury Murdoch II Inc", "src": "https://api.ardmediathek.de/image-service/images/urn:ard:image:559937f982bde0f5?w={width}&ch=dc9ee6ab714f2b88", "title": "Murdoch Mysteries"}, "availableSeasons": ["1", "4"], "binaryFeatures": ["UT", "OV"], "coreAssetType": "SEASON_SERIES"}, "synopsis": "Während sich Dr. Ogden auf ihre Hochzeit vorbereitet, muss Murdoch versuchen, den 'Küssenden Banditen' aufzuhalten, einen umstrittenen Bankräuber, der schnell zum Volkshelden aufsteigt.", "title": "Folge 12: Der küssende Bandit (S04/E12) - (Originalversion)", "trackingPiano": {"teaser_recommended": false, "widget_id": "player_on_demand_widget_ard", "widget_title": "Folge 12: Der küssende Bandit (S04/E12) - (Originalversion)", "widget_typ": "player_ondemand"}, "type": "player_ondemand"}, {"aZContent": false, "compilationType": "itemsOfShow", "id": "Y3JpZDovL3dkci5kZTdvbmUvbXVyZG9jaG15c3Rlcmllcw", "isChildContent": false, "pagination": {"pageNumber": 0, "pageSize": 24, "totalElements": 7}, "personalized": false, "links": {"self": {"id": "Y3JpZDovL3dkci5kZTdvbmUvbXVyZG9jaG15c3Rlcmllcw", "urlId": "Y3JpZDovL3dkci5kZTdvbmUvbXVyZG9jaG15c3Rlcmllcw", "title": "Murdoch Mysteries", "href": "https://api.ardmediathek.de/page-gateway/widgets/ard/asset/Y3JpZDovL3dkci5kZTdvbmUvbXVyZG9jaG15c3Rlcmllcw?excludedAssetIds=Y3JpZDovL3dkci5kZS9CZWl0cmFnLXNvcGhvcmEtY2UwODAxZjMtNjIyMy00ZGUyLTg3YjMtNjE1MjJkYjJkNjVi&pageNumber=0&pageSize=24&embedded=true&seasoned=false&seasonNumber=&withAudiodescription=false&withOriginalWithSubtitle=false&withOriginalversion=false&single=false", "type": "application/vnd.ard.widget+json", "partner": "ard"}}, "size": "m", "swipeable": true, "teasers": [], "title": "Murdoch Mysteries", "titleVisible": true, "trackingPiano": {"teaser_recommended": false, "widget_id": "Y3JpZDovL3dkci5kZTdvbmUvbXVyZG9jaG15c3Rlcmllcw", "widget_title": "Murdoch Mysteries", "widget_typ": "gridlist"}, "type": "gridlist", "userVisibility": "ALWAYS"}]} \ No newline at end of file diff --git a/src/test/resources/mlib/TestFilmlistOldFormatWriter.json b/src/test/resources/mlib/TestFilmlistOldFormatWriter.json index f9e3a0e37..7dc5178e2 100644 --- a/src/test/resources/mlib/TestFilmlistOldFormatWriter.json +++ b/src/test/resources/mlib/TestFilmlistOldFormatWriter.json @@ -1 +1 @@ -{"Filmliste":["30.05.2023, 19:21","30.05.2023, 17:21","4","MSearch [Vers.: 4.0.1]","7bc3d924-974f-42c4-8994-9d98dde17862"],"Filmliste":["Sender","Thema","Titel","Datum","Zeit","Dauer","Größe [MB]","Beschreibung","Url","Website","Url Untertitel","Url RTMP","Url Klein","Url RTMP Klein","Url HD","Url RTMP HD","DatumL","Url History","Geo","neu"],"X":["3Sat","37 Grad","37° Totgerast (Audiodeskription)","30.05.2023","22:15:00","00:28:45","405","Wer plötzlich Angehörige bei einem Unfall verliert, wacht in einem anderen Leben auf. Antonia verlor ihre Eltern und ihre Schwester, Steffen seine Frau und seinen Sohn.","https://rodlzdf-a.akamaihd.net/none/zdf/23/05/230530_2215_sendung_37g/2/230530_2215_sendung_37g_a3a4_2360k_p35v17.mp4","https://www.3sat.de/gesellschaft/37-grad/37-totgerast-102.html","","","101|808k_p11v17.mp4","","8|nrodlzdf-a.akamaihd.net/none/zdf/23/05/230530_2215_sendung_37g/2/230530_2215_sendung_37g_a3a4_6660k_p37v17.mp4","","1685477700","","","false"],"X":["","","37°: Ein Gen verändert unser Leben (Audiodeskription)","16.05.2023","22:15:00","00:28:41","414","Eine Diagnose, die alles verändert: Milo hat einen Gendefekt. Wie wird das Leben werden mit einem Kind, das extrem viel Betreuung braucht? \"37°\" begleitet die Familie.","https://rodlzdf-a.akamaihd.net/none/zdf/23/05/230502_2215_sendung_37g/4/230502_2215_sendung_37g_a3a4_2360k_p35v17.mp4","https://www.3sat.de/gesellschaft/37-grad/37-ein-gen-veraendert-unser-leben-100.html","","","101|808k_p11v17.mp4","","8|nrodlzdf-a.akamaihd.net/none/zdf/23/05/230502_2215_sendung_37g/4/230502_2215_sendung_37g_a3a4_6660k_p37v17.mp4","","1684268100","","","false"],"X":["","","37°: Mein Tanz, mein Battle (Audiodeskription)","11.10.2022","22:15:00","00:28:42","416","Joanna und Serhat teilen dieselbe Leidenschaft: Breakdance. 2024 feiert Breaken in Paris olympische Premiere. Eine Herausforderung für die Tänzer, wirklich alles zu geben.","https://rodlzdf-a.akamaihd.net/none/zdf/22/10/221011_2215_sendung_37g/3/221011_2215_sendung_37g_a3a4_2360k_p35v15.mp4","https://www.3sat.de/gesellschaft/37-grad/37-mein-tanz-mein-battle-100.html","","","101|808k_p11v15.mp4","","8|nrodlzdf-a.akamaihd.net/none/zdf/22/10/221011_2215_sendung_37g/3/221011_2215_sendung_37g_a3a4_3360k_p36v15.mp4","","1665519300","","","false"],"X":["","37 Grad Leben","Stroke Survivor – Ich geb' nicht auf! -","30.05.2023","09:03:00","00:27:07","377","Kaum ins Leben gestartet – und dann die Diagnose Schlaganfall. Wie kommen vor allem junge Betroffene, die sich gesund ernähren und sportlich leben, damit zurecht?","https://rodlzdf-a.akamaihd.net/none/zdf/23/05/230507_0903_sendung_sgl/1/230507_0903_sendung_sgl_2360k_p35v17.mp4","https://www.3sat.de/gesellschaft/37-grad-leben/stroke-survivor-ich-gebe-nicht-auf-102.html","https://utstreaming.zdf.de/mtt/zdf/23/05/230507_0903_sendung_sgl/2/F1039138_hoh_deu_37_Leben_Stroke_Survivors_neu.xml","","96|808k_p11v17.mp4","","8|nrodlzdf-a.akamaihd.net/none/zdf/23/05/230507_0903_sendung_sgl/1/230507_0903_sendung_sgl_6660k_p37v17.mp4","","1685430180","","","false"],"X":["","","Ich lass mich taufen -","28.05.2023","09:03:00","00:27:08","390","Trotz vieler Kirchenaustritte gibt es immer wieder Menschen, die einen Weg in die Kirche finden, sich im Erwachsenenalter taufen lassen. \"37°Leben\" begleitet zwei junge Frauen bei ihrer Taufe.","https://rodlzdf-a.akamaihd.net/none/zdf/23/05/230528_0903_sendung_sgl/1/230528_0903_sendung_sgl_2360k_p35v17.mp4","https://www.3sat.de/gesellschaft/37-grad-leben/ich-lass-mich-taufen-102.html","https://utstreaming.zdf.de/mtt/zdf/23/05/230528_0903_sendung_sgl/2/F1039826_hoh_deu_37_Leben_Ich_lass_mich_taufen_280523.xml","","96|808k_p11v17.mp4","","8|nrodlzdf-a.akamaihd.net/none/zdf/23/05/230528_0903_sendung_sgl/1/230528_0903_sendung_sgl_6660k_p37v17.mp4","","1685257380","","","false"],"X":["","","Körperideale: so what? -","14.05.2023","09:03:00","00:26:58","371","Federico ist 1,63 Meter groß. Als kleiner Mann selbstbewusst zu sein und sich begehrenswert zu fühlen, ist sein Weg. Bis heute muss er sich mit verletzenden Kommentaren auseinandersetzen.","https://rodlzdf-a.akamaihd.net/none/zdf/23/05/230514_0903_sendung_sgl/1/230514_0903_sendung_sgl_2360k_p35v17.mp4","https://www.3sat.de/gesellschaft/37-grad-leben/koerperideale-so-what-102.html","https://utstreaming.zdf.de/mtt/zdf/23/05/230514_0903_sendung_sgl/2/F1039665_hoh_deu_37_Grad_Leben_Koerperideale_so_what_ap_140523.xml","","96|808k_p11v17.mp4","","8|nrodlzdf-a.akamaihd.net/none/zdf/23/05/230514_0903_sendung_sgl/1/230514_0903_sendung_sgl_6660k_p37v17.mp4","","1684047780","","","false"],"X":["","Besonders normal","Leben in Zeiten von Corona","08.07.2020","11:44:00","00:29:01","499","Menschen mit Behinderungen sind gerade in größter Sorge. Sie haben Angst vor Ansteckung und einem schweren Verlauf, vor der Einsamkeit in Isolation und einem Assistenz- und Pflegekräftemangel.","https://rodlzdf-a.akamaihd.net/none/3sat/20/05/200508__1140_sendung_besondersnormal/2/200508__1140_sendung_besondersnormal_2328k_p35v13.mp4","https://www.3sat.de/gesellschaft/besonders-normal/besonders-normal-vom-8-mai-2020-102.html","https://utstreaming.zdf.de/mtt/3sat/20/05/200508__1140_sendung_besondersnormal/2/F1026017_hoh_deu_Besonders_normal_Leben_in_Zeiten_von_Corona_080520.xml","","123|776k_p11v13.mp4","","123|3328k_p36v13.mp4","","1594201440","","","false"],"X":["","","Corona - Zwischen Sorge und Zuversicht","05.06.2020","11:30:00","00:28:56","499","Die Corona-Pandemie wirkt immer noch nach. Auch bei den Menschen mit Behinderungen und sozial Schwachen. Sie können nicht wie gewohnt arbeiten oder bangen um die berufliche Existenz.","https://rodlzdf-a.akamaihd.net/none/3sat/20/06/200605_1130_sendung_besondersnormal/2/200605_1130_sendung_besondersnormal_2328k_p35v13.mp4","https://www.3sat.de/gesellschaft/besonders-normal/besonders-normal-vom-5-juni-2020-100.html","https://utstreaming.zdf.de/mtt/3sat/20/06/200605_1130_sendung_besondersnormal/2/F1026188_hoh_deu_Besonders_normal_Corona_Zwischen_Zuversicht_und_Sorge_050620.xml","","121|776k_p11v13.mp4","","121|3328k_p36v13.mp4","","1591349400","","","false"],"X":["","","Beste Freunde","07.06.2019","12:29:00","00:29:27","509","Während die Politik und Gesellschaft über die Möglichkeiten von Inklusion und Teilhabe diskutieren, haben zwei kleine Mädchen, den Traum schon längst umgesetzt.","https://rodlzdf-a.akamaihd.net/none/3sat/19/06/190607_beste_freunde/1/190607_beste_freunde_2328k_p35v13.mp4","https://www.3sat.de/gesellschaft/besonders-normal/beste-freunde-102.html","","","91|776k_p11v13.mp4","","91|3296k_p15v13.mp4","","1559903340","","","false"],"X":["","Bier Royal","Bier Royal (1/2) - Familienkomödie, Deutschland 2019","30.01.2019","20:15:00","01:25:05","1467","München. Bier. Society. Und mittendrin eine alteingesessene Bierdynastie, in der zwei Frauen um Macht, Liebe und Anerkennung kämpfen.","https://rodlzdf-a.akamaihd.net/dach/3sat/19/12/191227_bier_royal1_fernsehfilm/2/191227_bier_royal1_fernsehfilm_2328k_p35v13.mp4","https://www.3sat.de/film/bier-royal/bier-royal-1-100.html","","","111|776k_p11v13.mp4","","111|3328k_p36v13.mp4","","1548875700","","DE-AT-CH","false"],"X":["","","Bier Royal (2/2) - Familienkomödie, Deutschland 2019","28.01.2019","20:15:00","01:27:30","1510","Die Münchner Brauerei droht im erbitterten Erbstreit unterzugehen. Gisela und Vicky müssen zusammenarbeiten, um das Unternehmen zu retten.","https://rodlzdf-a.akamaihd.net/dach/3sat/19/12/191227_bier_royal2_fernsehfilm/4/191227_bier_royal2_fernsehfilm_2328k_p35v13.mp4","https://www.3sat.de/film/bier-royal/bier-royal-2-100.html","","","111|776k_p11v13.mp4","","111|3328k_p36v13.mp4","","1548702900","","DE-AT-CH","false"],"X":["","Bilder aus Südtirol","Bilder aus Südtirol vom 20. Mai 2023","20.05.2023","09:35:00","00:23:32","327","\"Bilder aus Südtirol\" ist das Bundesländermagazin des ORF-Landesstudios Tirol.","https://rodlzdf-a.akamaihd.net/none/3sat/23/05/230520_0935_sendung_suedtirol/1/230520_0935_sendung_suedtirol_2360k_p35v17.mp4","https://www.3sat.de/gesellschaft/bilder-aus-suedtirol/bilder-aus-suedtirol-vom-20-mai-2023-100.html","","","109|808k_p11v17.mp4","","8|nrodlzdf-a.akamaihd.net/none/3sat/23/05/230520_0935_sendung_suedtirol/1/230520_0935_sendung_suedtirol_6660k_p37v17.mp4","","1684568100","","","false"],"X":["","","Bilder aus Südtirol vom 13. Mai 2023","13.05.2023","09:37:00","00:22:38","314","\"Bilder aus Südtirol\" ist das Bundesländermagazin des ORF-Landesstudios Tirol.","https://rodlzdf-a.akamaihd.net/none/3sat/23/05/230513_0935_sendung_suedtirol/1/230513_0935_sendung_suedtirol_2360k_p35v17.mp4","https://www.3sat.de/gesellschaft/bilder-aus-suedtirol/bilder-aus-suedtirol-vom-13-mai-2023-100.html","","","109|808k_p11v17.mp4","","8|nrodlzdf-a.akamaihd.net/none/3sat/23/05/230513_0935_sendung_suedtirol/1/230513_0935_sendung_suedtirol_6660k_p37v17.mp4","","1683963420","","","false"],"X":["","","Bilder aus Südtirol vom 6. Mai 2023","06.05.2023","09:39:00","00:24:21","336","\"Bilder aus Südtirol\" ist das Bundesländermagazin des ORF-Landesstudios Tirol.","https://rodlzdf-a.akamaihd.net/none/3sat/23/05/230506_0935_sendung_suedtirol/1/230506_0935_sendung_suedtirol_2360k_p35v17.mp4","https://www.3sat.de/gesellschaft/bilder-aus-suedtirol/bilder-aus-suedtirol-vom-6-mai-2023-100.html","","","109|808k_p11v17.mp4","","8|nrodlzdf-a.akamaihd.net/none/3sat/23/05/230506_0935_sendung_suedtirol/1/230506_0935_sendung_suedtirol_6660k_p37v17.mp4","","1683358740","","","false"],"X":["","","Bilder aus Südtirol vom 22. April 2023","22.04.2023","09:39:00","00:23:52","337","\"Bilder aus Südtirol\" ist das Bundesländermagazin des ORF-Landesstudios Tirol.","https://rodlzdf-a.akamaihd.net/none/3sat/23/04/230422_0935_sendung_suedtirol/1/230422_0935_sendung_suedtirol_2360k_p35v17.mp4","https://www.3sat.de/gesellschaft/bilder-aus-suedtirol/bilder-aus-suedtirol-vom-22-april-2023-100.html","","","109|808k_p11v17.mp4","","8|nrodlzdf-a.akamaihd.net/none/3sat/23/04/230422_0935_sendung_suedtirol/1/230422_0935_sendung_suedtirol_6660k_p37v17.mp4","","1682149140","","","false"],"X":["","","Bilder aus Südtirol vom 15. April 2023","15.04.2023","09:37:00","00:19:42","273","\"Bilder aus Südtirol\" ist das Bundesländermagazin des ORF-Landesstudios Tirol.","https://rodlzdf-a.akamaihd.net/none/3sat/23/04/230415_0935_sendung_suedtirol/1/230415_0935_sendung_suedtirol_2360k_p35v17.mp4","https://www.3sat.de/gesellschaft/bilder-aus-suedtirol/bilder-aus-suedtirol-vom-15-april-2023-100.html","","","109|808k_p11v17.mp4","","8|nrodlzdf-a.akamaihd.net/none/3sat/23/04/230415_0935_sendung_suedtirol/1/230415_0935_sendung_suedtirol_6660k_p37v17.mp4","","1681544220","","","false"],"X":["","","Bilder aus Südtirol vom 8. April 2023","08.04.2023","09:36:00","00:22:56","323","\"Bilder aus Südtirol\" ist das Bundesländermagazin des ORF-Landesstudios Tirol.","https://rodlzdf-a.akamaihd.net/none/3sat/23/04/230408_0935_sendung_suedtirol/1/230408_0935_sendung_suedtirol_2360k_p35v17.mp4","https://www.3sat.de/gesellschaft/bilder-aus-suedtirol/bilder-aus-suedtirol-vom-8-april-2023-100.html","","","109|808k_p11v17.mp4","","8|nrodlzdf-a.akamaihd.net/none/3sat/23/04/230408_0935_sendung_suedtirol/1/230408_0935_sendung_suedtirol_6660k_p37v17.mp4","","1680939360","","","false"],"X":["","","Bilder aus Südtirol vom 11. Dezember 2021","11.12.2021","09:35:00","00:24:35","335","\"Bilder aus Südtirol\" ist das Bundesländermagazin des ORF-Landesstudios Tirol.","https://rodlzdf-a.akamaihd.net/none/3sat/21/12/211211_0935_sendung_suedtirol/1/211211_0935_sendung_suedtirol_2360k_p35v15.mp4","https://www.3sat.de/gesellschaft/bilder-aus-suedtirol/bilder-aus-suedtirol-vom-11-dezember-2021-100.html","","","109|808k_p11v15.mp4","","109|3360k_p36v15.mp4","","1639211700","","","false"],"X":["","","Bilder aus Südtirol vom 6. November 2021","06.11.2021","09:35:00","00:23:58","333","\"Bilder aus Südtirol\" ist das Bundesländermagazin des ORF-Landesstudios Tirol.","https://rodlzdf-a.akamaihd.net/none/3sat/21/11/211106_0935_sendung_suedtirol/1/211106_0935_sendung_suedtirol_2360k_p35v15.mp4","https://www.3sat.de/gesellschaft/bilder-aus-suedtirol/bilder-aus-suedtirol-vom-6-november-2021-100.html","","","109|808k_p11v15.mp4","","109|3360k_p36v15.mp4","","1636187700","","","false"],"X":["","","Bilder aus Südtirol vom 25. Juli 2020","25.07.2020","09:35:00","00:23:41","407","\"Bilder aus Südtirol\" ist das Bundesländermagazin des ORF-Landesstudios Tirol.","https://rodlzdf-a.akamaihd.net/none/3sat/20/07/200725_0935_sendung_suedtirol/1/200725_0935_sendung_suedtirol_2328k_p35v13.mp4","https://www.3sat.de/gesellschaft/bilder-aus-suedtirol/bilder-aus-suedtirol-vom-25-juli-2020-100.html","","","109|776k_p11v13.mp4","","109|3328k_p36v13.mp4","","1595662500","","","false"],"X":["","","Bilder aus Südtirol vom 20. Juni 2020","20.06.2020","09:36:00","00:24:16","416","\"Bilder aus Südtirol\" ist das Bundesländermagazin des ORF-Landesstudios Tirol.","https://rodlzdf-a.akamaihd.net/none/3sat/20/06/200620_0935_sendung_suedtirol/1/200620_0935_sendung_suedtirol_2328k_p35v13.mp4","https://www.3sat.de/gesellschaft/bilder-aus-suedtirol/bilder-aus-suedtirol-vom-20-juni-2020-100.html","","","109|776k_p11v13.mp4","","109|3328k_p36v13.mp4","","1592638560","","","false"]} \ No newline at end of file +{"Filmliste":["30.05.2023, 19:21","30.05.2023, 17:21","4","MSearch [Vers.: 4.0.1]","7bc3d924-974f-42c4-8994-9d98dde17862"],"Filmliste":["Sender","Thema","Titel","Datum","Zeit","Dauer","Größe [MB]","Beschreibung","Url","Website","Url Untertitel","Url RTMP","Url Klein","Url RTMP Klein","Url HD","Url RTMP HD","DatumL","Url History","Geo","neu","Sprache"],"X":["3Sat","37 Grad","37° Totgerast (Audiodeskription)","30.05.2023","22:15:00","00:28:45","405","Wer plötzlich Angehörige bei einem Unfall verliert, wacht in einem anderen Leben auf. Antonia verlor ihre Eltern und ihre Schwester, Steffen seine Frau und seinen Sohn.","https://rodlzdf-a.akamaihd.net/none/zdf/23/05/230530_2215_sendung_37g/2/230530_2215_sendung_37g_a3a4_2360k_p35v17.mp4","https://www.3sat.de/gesellschaft/37-grad/37-totgerast-102.html","","","101|808k_p11v17.mp4","","8|nrodlzdf-a.akamaihd.net/none/zdf/23/05/230530_2215_sendung_37g/2/230530_2215_sendung_37g_a3a4_6660k_p37v17.mp4","","1685477700","","","false",""],"X":["","","37°: Ein Gen verändert unser Leben (Audiodeskription)","16.05.2023","22:15:00","00:28:41","414","Eine Diagnose, die alles verändert: Milo hat einen Gendefekt. Wie wird das Leben werden mit einem Kind, das extrem viel Betreuung braucht? \"37°\" begleitet die Familie.","https://rodlzdf-a.akamaihd.net/none/zdf/23/05/230502_2215_sendung_37g/4/230502_2215_sendung_37g_a3a4_2360k_p35v17.mp4","https://www.3sat.de/gesellschaft/37-grad/37-ein-gen-veraendert-unser-leben-100.html","","","101|808k_p11v17.mp4","","8|nrodlzdf-a.akamaihd.net/none/zdf/23/05/230502_2215_sendung_37g/4/230502_2215_sendung_37g_a3a4_6660k_p37v17.mp4","","1684268100","","","false",""],"X":["","","37°: Mein Tanz, mein Battle (Audiodeskription)","11.10.2022","22:15:00","00:28:42","416","Joanna und Serhat teilen dieselbe Leidenschaft: Breakdance. 2024 feiert Breaken in Paris olympische Premiere. Eine Herausforderung für die Tänzer, wirklich alles zu geben.","https://rodlzdf-a.akamaihd.net/none/zdf/22/10/221011_2215_sendung_37g/3/221011_2215_sendung_37g_a3a4_2360k_p35v15.mp4","https://www.3sat.de/gesellschaft/37-grad/37-mein-tanz-mein-battle-100.html","","","101|808k_p11v15.mp4","","8|nrodlzdf-a.akamaihd.net/none/zdf/22/10/221011_2215_sendung_37g/3/221011_2215_sendung_37g_a3a4_3360k_p36v15.mp4","","1665519300","","","false",""],"X":["","37 Grad Leben","Stroke Survivor – Ich geb' nicht auf! -","30.05.2023","09:03:00","00:27:07","377","Kaum ins Leben gestartet – und dann die Diagnose Schlaganfall. Wie kommen vor allem junge Betroffene, die sich gesund ernähren und sportlich leben, damit zurecht?","https://rodlzdf-a.akamaihd.net/none/zdf/23/05/230507_0903_sendung_sgl/1/230507_0903_sendung_sgl_2360k_p35v17.mp4","https://www.3sat.de/gesellschaft/37-grad-leben/stroke-survivor-ich-gebe-nicht-auf-102.html","https://utstreaming.zdf.de/mtt/zdf/23/05/230507_0903_sendung_sgl/2/F1039138_hoh_deu_37_Leben_Stroke_Survivors_neu.xml","","96|808k_p11v17.mp4","","8|nrodlzdf-a.akamaihd.net/none/zdf/23/05/230507_0903_sendung_sgl/1/230507_0903_sendung_sgl_6660k_p37v17.mp4","","1685430180","","","false",""],"X":["","","Ich lass mich taufen -","28.05.2023","09:03:00","00:27:08","390","Trotz vieler Kirchenaustritte gibt es immer wieder Menschen, die einen Weg in die Kirche finden, sich im Erwachsenenalter taufen lassen. \"37°Leben\" begleitet zwei junge Frauen bei ihrer Taufe.","https://rodlzdf-a.akamaihd.net/none/zdf/23/05/230528_0903_sendung_sgl/1/230528_0903_sendung_sgl_2360k_p35v17.mp4","https://www.3sat.de/gesellschaft/37-grad-leben/ich-lass-mich-taufen-102.html","https://utstreaming.zdf.de/mtt/zdf/23/05/230528_0903_sendung_sgl/2/F1039826_hoh_deu_37_Leben_Ich_lass_mich_taufen_280523.xml","","96|808k_p11v17.mp4","","8|nrodlzdf-a.akamaihd.net/none/zdf/23/05/230528_0903_sendung_sgl/1/230528_0903_sendung_sgl_6660k_p37v17.mp4","","1685257380","","","false",""],"X":["","","Körperideale: so what? -","14.05.2023","09:03:00","00:26:58","371","Federico ist 1,63 Meter groß. Als kleiner Mann selbstbewusst zu sein und sich begehrenswert zu fühlen, ist sein Weg. Bis heute muss er sich mit verletzenden Kommentaren auseinandersetzen.","https://rodlzdf-a.akamaihd.net/none/zdf/23/05/230514_0903_sendung_sgl/1/230514_0903_sendung_sgl_2360k_p35v17.mp4","https://www.3sat.de/gesellschaft/37-grad-leben/koerperideale-so-what-102.html","https://utstreaming.zdf.de/mtt/zdf/23/05/230514_0903_sendung_sgl/2/F1039665_hoh_deu_37_Grad_Leben_Koerperideale_so_what_ap_140523.xml","","96|808k_p11v17.mp4","","8|nrodlzdf-a.akamaihd.net/none/zdf/23/05/230514_0903_sendung_sgl/1/230514_0903_sendung_sgl_6660k_p37v17.mp4","","1684047780","","","false",""],"X":["","Besonders normal","Leben in Zeiten von Corona","08.07.2020","11:44:00","00:29:01","499","Menschen mit Behinderungen sind gerade in größter Sorge. Sie haben Angst vor Ansteckung und einem schweren Verlauf, vor der Einsamkeit in Isolation und einem Assistenz- und Pflegekräftemangel.","https://rodlzdf-a.akamaihd.net/none/3sat/20/05/200508__1140_sendung_besondersnormal/2/200508__1140_sendung_besondersnormal_2328k_p35v13.mp4","https://www.3sat.de/gesellschaft/besonders-normal/besonders-normal-vom-8-mai-2020-102.html","https://utstreaming.zdf.de/mtt/3sat/20/05/200508__1140_sendung_besondersnormal/2/F1026017_hoh_deu_Besonders_normal_Leben_in_Zeiten_von_Corona_080520.xml","","123|776k_p11v13.mp4","","123|3328k_p36v13.mp4","","1594201440","","","false",""],"X":["","","Corona - Zwischen Sorge und Zuversicht","05.06.2020","11:30:00","00:28:56","499","Die Corona-Pandemie wirkt immer noch nach. Auch bei den Menschen mit Behinderungen und sozial Schwachen. Sie können nicht wie gewohnt arbeiten oder bangen um die berufliche Existenz.","https://rodlzdf-a.akamaihd.net/none/3sat/20/06/200605_1130_sendung_besondersnormal/2/200605_1130_sendung_besondersnormal_2328k_p35v13.mp4","https://www.3sat.de/gesellschaft/besonders-normal/besonders-normal-vom-5-juni-2020-100.html","https://utstreaming.zdf.de/mtt/3sat/20/06/200605_1130_sendung_besondersnormal/2/F1026188_hoh_deu_Besonders_normal_Corona_Zwischen_Zuversicht_und_Sorge_050620.xml","","121|776k_p11v13.mp4","","121|3328k_p36v13.mp4","","1591349400","","","false",""],"X":["","","Beste Freunde","07.06.2019","12:29:00","00:29:27","509","Während die Politik und Gesellschaft über die Möglichkeiten von Inklusion und Teilhabe diskutieren, haben zwei kleine Mädchen, den Traum schon längst umgesetzt.","https://rodlzdf-a.akamaihd.net/none/3sat/19/06/190607_beste_freunde/1/190607_beste_freunde_2328k_p35v13.mp4","https://www.3sat.de/gesellschaft/besonders-normal/beste-freunde-102.html","","","91|776k_p11v13.mp4","","91|3296k_p15v13.mp4","","1559903340","","","false",""],"X":["","Bier Royal","Bier Royal (1/2) - Familienkomödie, Deutschland 2019","30.01.2019","20:15:00","01:25:05","1467","München. Bier. Society. Und mittendrin eine alteingesessene Bierdynastie, in der zwei Frauen um Macht, Liebe und Anerkennung kämpfen.","https://rodlzdf-a.akamaihd.net/dach/3sat/19/12/191227_bier_royal1_fernsehfilm/2/191227_bier_royal1_fernsehfilm_2328k_p35v13.mp4","https://www.3sat.de/film/bier-royal/bier-royal-1-100.html","","","111|776k_p11v13.mp4","","111|3328k_p36v13.mp4","","1548875700","","DE-AT-CH","false",""],"X":["","","Bier Royal (2/2) - Familienkomödie, Deutschland 2019","28.01.2019","20:15:00","01:27:30","1510","Die Münchner Brauerei droht im erbitterten Erbstreit unterzugehen. Gisela und Vicky müssen zusammenarbeiten, um das Unternehmen zu retten.","https://rodlzdf-a.akamaihd.net/dach/3sat/19/12/191227_bier_royal2_fernsehfilm/4/191227_bier_royal2_fernsehfilm_2328k_p35v13.mp4","https://www.3sat.de/film/bier-royal/bier-royal-2-100.html","","","111|776k_p11v13.mp4","","111|3328k_p36v13.mp4","","1548702900","","DE-AT-CH","false",""],"X":["","Bilder aus Südtirol","Bilder aus Südtirol vom 20. Mai 2023","20.05.2023","09:35:00","00:23:32","327","\"Bilder aus Südtirol\" ist das Bundesländermagazin des ORF-Landesstudios Tirol.","https://rodlzdf-a.akamaihd.net/none/3sat/23/05/230520_0935_sendung_suedtirol/1/230520_0935_sendung_suedtirol_2360k_p35v17.mp4","https://www.3sat.de/gesellschaft/bilder-aus-suedtirol/bilder-aus-suedtirol-vom-20-mai-2023-100.html","","","109|808k_p11v17.mp4","","8|nrodlzdf-a.akamaihd.net/none/3sat/23/05/230520_0935_sendung_suedtirol/1/230520_0935_sendung_suedtirol_6660k_p37v17.mp4","","1684568100","","","false",""],"X":["","","Bilder aus Südtirol vom 13. Mai 2023","13.05.2023","09:37:00","00:22:38","314","\"Bilder aus Südtirol\" ist das Bundesländermagazin des ORF-Landesstudios Tirol.","https://rodlzdf-a.akamaihd.net/none/3sat/23/05/230513_0935_sendung_suedtirol/1/230513_0935_sendung_suedtirol_2360k_p35v17.mp4","https://www.3sat.de/gesellschaft/bilder-aus-suedtirol/bilder-aus-suedtirol-vom-13-mai-2023-100.html","","","109|808k_p11v17.mp4","","8|nrodlzdf-a.akamaihd.net/none/3sat/23/05/230513_0935_sendung_suedtirol/1/230513_0935_sendung_suedtirol_6660k_p37v17.mp4","","1683963420","","","false",""],"X":["","","Bilder aus Südtirol vom 6. Mai 2023","06.05.2023","09:39:00","00:24:21","336","\"Bilder aus Südtirol\" ist das Bundesländermagazin des ORF-Landesstudios Tirol.","https://rodlzdf-a.akamaihd.net/none/3sat/23/05/230506_0935_sendung_suedtirol/1/230506_0935_sendung_suedtirol_2360k_p35v17.mp4","https://www.3sat.de/gesellschaft/bilder-aus-suedtirol/bilder-aus-suedtirol-vom-6-mai-2023-100.html","","","109|808k_p11v17.mp4","","8|nrodlzdf-a.akamaihd.net/none/3sat/23/05/230506_0935_sendung_suedtirol/1/230506_0935_sendung_suedtirol_6660k_p37v17.mp4","","1683358740","","","false",""],"X":["","","Bilder aus Südtirol vom 22. April 2023","22.04.2023","09:39:00","00:23:52","337","\"Bilder aus Südtirol\" ist das Bundesländermagazin des ORF-Landesstudios Tirol.","https://rodlzdf-a.akamaihd.net/none/3sat/23/04/230422_0935_sendung_suedtirol/1/230422_0935_sendung_suedtirol_2360k_p35v17.mp4","https://www.3sat.de/gesellschaft/bilder-aus-suedtirol/bilder-aus-suedtirol-vom-22-april-2023-100.html","","","109|808k_p11v17.mp4","","8|nrodlzdf-a.akamaihd.net/none/3sat/23/04/230422_0935_sendung_suedtirol/1/230422_0935_sendung_suedtirol_6660k_p37v17.mp4","","1682149140","","","false",""],"X":["","","Bilder aus Südtirol vom 15. April 2023","15.04.2023","09:37:00","00:19:42","273","\"Bilder aus Südtirol\" ist das Bundesländermagazin des ORF-Landesstudios Tirol.","https://rodlzdf-a.akamaihd.net/none/3sat/23/04/230415_0935_sendung_suedtirol/1/230415_0935_sendung_suedtirol_2360k_p35v17.mp4","https://www.3sat.de/gesellschaft/bilder-aus-suedtirol/bilder-aus-suedtirol-vom-15-april-2023-100.html","","","109|808k_p11v17.mp4","","8|nrodlzdf-a.akamaihd.net/none/3sat/23/04/230415_0935_sendung_suedtirol/1/230415_0935_sendung_suedtirol_6660k_p37v17.mp4","","1681544220","","","false",""],"X":["","","Bilder aus Südtirol vom 8. April 2023","08.04.2023","09:36:00","00:22:56","323","\"Bilder aus Südtirol\" ist das Bundesländermagazin des ORF-Landesstudios Tirol.","https://rodlzdf-a.akamaihd.net/none/3sat/23/04/230408_0935_sendung_suedtirol/1/230408_0935_sendung_suedtirol_2360k_p35v17.mp4","https://www.3sat.de/gesellschaft/bilder-aus-suedtirol/bilder-aus-suedtirol-vom-8-april-2023-100.html","","","109|808k_p11v17.mp4","","8|nrodlzdf-a.akamaihd.net/none/3sat/23/04/230408_0935_sendung_suedtirol/1/230408_0935_sendung_suedtirol_6660k_p37v17.mp4","","1680939360","","","false",""],"X":["","","Bilder aus Südtirol vom 11. Dezember 2021","11.12.2021","09:35:00","00:24:35","335","\"Bilder aus Südtirol\" ist das Bundesländermagazin des ORF-Landesstudios Tirol.","https://rodlzdf-a.akamaihd.net/none/3sat/21/12/211211_0935_sendung_suedtirol/1/211211_0935_sendung_suedtirol_2360k_p35v15.mp4","https://www.3sat.de/gesellschaft/bilder-aus-suedtirol/bilder-aus-suedtirol-vom-11-dezember-2021-100.html","","","109|808k_p11v15.mp4","","109|3360k_p36v15.mp4","","1639211700","","","false",""],"X":["","","Bilder aus Südtirol vom 6. November 2021","06.11.2021","09:35:00","00:23:58","333","\"Bilder aus Südtirol\" ist das Bundesländermagazin des ORF-Landesstudios Tirol.","https://rodlzdf-a.akamaihd.net/none/3sat/21/11/211106_0935_sendung_suedtirol/1/211106_0935_sendung_suedtirol_2360k_p35v15.mp4","https://www.3sat.de/gesellschaft/bilder-aus-suedtirol/bilder-aus-suedtirol-vom-6-november-2021-100.html","","","109|808k_p11v15.mp4","","109|3360k_p36v15.mp4","","1636187700","","","false",""],"X":["","","Bilder aus Südtirol vom 25. Juli 2020","25.07.2020","09:35:00","00:23:41","407","\"Bilder aus Südtirol\" ist das Bundesländermagazin des ORF-Landesstudios Tirol.","https://rodlzdf-a.akamaihd.net/none/3sat/20/07/200725_0935_sendung_suedtirol/1/200725_0935_sendung_suedtirol_2328k_p35v13.mp4","https://www.3sat.de/gesellschaft/bilder-aus-suedtirol/bilder-aus-suedtirol-vom-25-juli-2020-100.html","","","109|776k_p11v13.mp4","","109|3328k_p36v13.mp4","","1595662500","","","false",""],"X":["","","Bilder aus Südtirol vom 20. Juni 2020","20.06.2020","09:36:00","00:24:16","416","\"Bilder aus Südtirol\" ist das Bundesländermagazin des ORF-Landesstudios Tirol.","https://rodlzdf-a.akamaihd.net/none/3sat/20/06/200620_0935_sendung_suedtirol/1/200620_0935_sendung_suedtirol_2328k_p35v13.mp4","https://www.3sat.de/gesellschaft/bilder-aus-suedtirol/bilder-aus-suedtirol-vom-20-juni-2020-100.html","","","109|776k_p11v13.mp4","","109|3328k_p36v13.mp4","","1592638560","","","false",""]} \ No newline at end of file From 3180d63e850682fa7cce6c30e189226b8e22bbf7 Mon Sep 17 00:00:00 2001 From: CuriousApe2020 Date: Thu, 17 Sep 2026 21:29:28 +0000 Subject: [PATCH 2/3] Sprache der Tonspur auch fuer ZDF Baut auf dem ARD-Teil auf und fuellt dasselbe Feld Film.audioLanguage fuer das ZDF. Das ZDF fuehrt die Downloads ohnehin je Sprache (DownloadDto haelt sie als Map Sprache auf Aufloesung auf URL) und erzeugt daraus je Sprache einen eigenen Film. Den Code kennt es dabei bereits in der Form, die das Feld zusichert - ZdfConstants fuehrt LANGUAGE_ENGLISH als "eng" und LANGUAGE_FRENCH als "fra". Verwendet wurde er bisher aber nur, um einen deutschen Titelzusatz zu bilden: case ZdfConstants.LANGUAGE_ENGLISH: title += " (Englisch)"; Danach war der Code nicht mehr vorhanden. Er wird jetzt an derselben Stelle zusaetzlich als Feld gesetzt - in ZdfFilmDetailTask und ZdfFilmTask, die beide dieselbe clone/updateTitle-Paarung haben. Passend zum ARD-Teil bleibt die deutsche Fassung leer: sie ist die uebliche Fassung des Senders, und ein Wert auf jedem Datensatz wuerde die Filmliste unnoetig aufblaehen. Das entspricht dem Titel, der fuer sie ebenfalls unveraendert bleibt. Die Suffixe "-ad" und "-dgs", die das ZDF an den Sprachcode haengt, um Audiodeskription und Gebaerdensprache zu unterscheiden, werden abgeschnitten: sie benennen die Art der Tonspur, nicht ihre Sprache. Damit sind auch 3sat und phoenix abgedeckt, die dieselbe Infrastruktur nutzen. Test: englische Fassung behaelt "eng", die deutsche bleibt leer - am vorhandenen Fixture fuer mehrsprachige Beitraege. --- .../crawler/zdf/tasks/ZdfFilmDetailTask.java | 31 ++++++++++ .../crawler/zdf/tasks/ZdfFilmTask.java | 31 ++++++++++ ...dfFilmDetailTaskMultipleLanguagesTest.java | 59 +++++++++++++++++++ 3 files changed, 121 insertions(+) diff --git a/src/main/java/de/mediathekview/mserver/crawler/zdf/tasks/ZdfFilmDetailTask.java b/src/main/java/de/mediathekview/mserver/crawler/zdf/tasks/ZdfFilmDetailTask.java index e8d567e46..2ff2f7d7c 100644 --- a/src/main/java/de/mediathekview/mserver/crawler/zdf/tasks/ZdfFilmDetailTask.java +++ b/src/main/java/de/mediathekview/mserver/crawler/zdf/tasks/ZdfFilmDetailTask.java @@ -64,10 +64,41 @@ private static Film clone(final Film aFilm, final String aLanguage) { film.setWebsite(aFilm.getWebsite().orElse(null)); updateTitle(aLanguage, film); + setAudioLanguage(aLanguage, film); return film; } + /** + * Haelt die Sprache der Tonspur als Feld fest, dort wo sie bisher nur in den Titel geschrieben + * wurde. + * + *

Das ZDF fuehrt die Downloads ohnehin je Sprache und kennt den Code bereits in der Form, die + * das Feld zusichert ("deu", "eng", "fra"). Aus dem Code wurde bisher nur ein deutscher + * Titelzusatz; danach war er nicht mehr vorhanden. + * + *

Die deutsche Fassung bleibt leer - sie ist die uebliche Fassung des Senders, und ein Wert + * auf jedem Datensatz wuerde die Filmliste unnoetig aufblaehen. Das entspricht dem Titel, der + * fuer sie ebenfalls unveraendert bleibt. + * + *

Die Suffixe fuer Audiodeskription und Gebaerdensprache werden abgeschnitten: sie benennen + * die Art der Tonspur, nicht ihre Sprache. + */ + private static void setAudioLanguage(final String aLanguage, final Film aFilm) { + final String languageCode = aLanguage.split("-")[0]; + if (ZdfConstants.LANGUAGE_GERMAN.equals(languageCode)) { + return; + } + // Das Feld sichert den dreistelligen ISO-639-2/T-Code zu. Die Tracks des ZDF sind bisher + // ausnahmslos so ausgezeichnet ("deu", "eng"); sollte doch einmal etwas anderes kommen, bleibt + // das Feld lieber leer, als einen Wert zu fuehren, auf den sich Clients nicht verlassen + // koennen. + if (languageCode.length() != 3 || !languageCode.chars().allMatch(Character::isLetter)) { + return; + } + aFilm.setAudioLanguage(languageCode); + } + private static void updateTitle(final String aLanguage, final Film aFilm) { String title = aFilm.getTitel(); switch (aLanguage) { diff --git a/src/main/java/de/mediathekview/mserver/crawler/zdf/tasks/ZdfFilmTask.java b/src/main/java/de/mediathekview/mserver/crawler/zdf/tasks/ZdfFilmTask.java index a103d8900..fafbc9f69 100644 --- a/src/main/java/de/mediathekview/mserver/crawler/zdf/tasks/ZdfFilmTask.java +++ b/src/main/java/de/mediathekview/mserver/crawler/zdf/tasks/ZdfFilmTask.java @@ -83,10 +83,41 @@ private static Film clone(final Film aFilm, final String aLanguage) { film.setWebsite(aFilm.getWebsite().orElse(null)); updateTitle(aLanguage, film); + setAudioLanguage(aLanguage, film); return film; } + /** + * Haelt die Sprache der Tonspur als Feld fest, dort wo sie bisher nur in den Titel geschrieben + * wurde. + * + *

Das ZDF fuehrt die Downloads ohnehin je Sprache und kennt den Code bereits in der Form, die + * das Feld zusichert ("deu", "eng", "fra"). Aus dem Code wurde bisher nur ein deutscher + * Titelzusatz; danach war er nicht mehr vorhanden. + * + *

Die deutsche Fassung bleibt leer - sie ist die uebliche Fassung des Senders, und ein Wert + * auf jedem Datensatz wuerde die Filmliste unnoetig aufblaehen. Das entspricht dem Titel, der + * fuer sie ebenfalls unveraendert bleibt. + * + *

Die Suffixe fuer Audiodeskription und Gebaerdensprache werden abgeschnitten: sie benennen + * die Art der Tonspur, nicht ihre Sprache. + */ + private static void setAudioLanguage(final String aLanguage, final Film aFilm) { + final String languageCode = aLanguage.split("-")[0]; + if (ZdfConstants.LANGUAGE_GERMAN.equals(languageCode)) { + return; + } + // Das Feld sichert den dreistelligen ISO-639-2/T-Code zu. Die Tracks des ZDF sind bisher + // ausnahmslos so ausgezeichnet ("deu", "eng"); sollte doch einmal etwas anderes kommen, bleibt + // das Feld lieber leer, als einen Wert zu fuehren, auf den sich Clients nicht verlassen + // koennen. + if (languageCode.length() != 3 || !languageCode.chars().allMatch(Character::isLetter)) { + return; + } + aFilm.setAudioLanguage(languageCode); + } + private static void updateTitle(final String aLanguage, final Film aFilm) { String title = aFilm.getTitel(); switch (aLanguage) { diff --git a/src/test/java/de/mediathekview/mserver/crawler/zdf/tasks/ZdfFilmDetailTaskMultipleLanguagesTest.java b/src/test/java/de/mediathekview/mserver/crawler/zdf/tasks/ZdfFilmDetailTaskMultipleLanguagesTest.java index 7e37f4601..e09d4c8e8 100644 --- a/src/test/java/de/mediathekview/mserver/crawler/zdf/tasks/ZdfFilmDetailTaskMultipleLanguagesTest.java +++ b/src/test/java/de/mediathekview/mserver/crawler/zdf/tasks/ZdfFilmDetailTaskMultipleLanguagesTest.java @@ -13,10 +13,69 @@ import java.util.concurrent.ConcurrentLinkedQueue; import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.nullValue; import static org.hamcrest.MatcherAssert.assertThat; class ZdfFilmDetailTaskMultipleLanguagesTest extends ZdfTaskTestBase { + @Test + void audioLanguageIsOnlySetForAThreeLetterCode() throws Exception { + // Das Feld sichert den dreistelligen ISO-639-2/T-Code zu. Die Tracks des ZDF sind bisher + // ausnahmslos so ausgezeichnet - kaeme doch einmal ein zweistelliger Code, darf er nicht + // ungeprueft in der Filmliste landen. + final java.lang.reflect.Method setAudioLanguage = + ZdfFilmDetailTask.class.getDeclaredMethod("setAudioLanguage", String.class, Film.class); + setAudioLanguage.setAccessible(true); + + assertThat(audioLanguageFor(setAudioLanguage, "eng"), equalTo("eng")); + assertThat(audioLanguageFor(setAudioLanguage, "fra"), equalTo("fra")); + // Deutsch ist die uebliche Fassung und bleibt leer. + assertThat(audioLanguageFor(setAudioLanguage, "deu"), nullValue()); + // Die Art der Tonspur ist keine Sprache - Suffix wird abgeschnitten. + assertThat(audioLanguageFor(setAudioLanguage, "deu-ad"), nullValue()); + assertThat(audioLanguageFor(setAudioLanguage, "eng-ad"), equalTo("eng")); + // Alles, was kein dreistelliger Buchstabencode ist, ergibt nichts. + assertThat(audioLanguageFor(setAudioLanguage, "de"), nullValue()); + assertThat(audioLanguageFor(setAudioLanguage, "en"), nullValue()); + assertThat(audioLanguageFor(setAudioLanguage, "123"), nullValue()); + } + + private static String audioLanguageFor( + final java.lang.reflect.Method setAudioLanguage, final String language) throws Exception { + final Film film = + new Film( + UUID.randomUUID(), + Sender.ZDF, + "Titel", + "Thema", + LocalDateTime.of(2026, 1, 1, 20, 15), + Duration.ofMinutes(90)); + setAudioLanguage.invoke(null, language, film); + return film.getAudioLanguage(); + } + + @Test + void audioLanguageIsKeptForTheNonGermanVersion() { + // Das ZDF fuehrt die Downloads je Sprache und kennt den Code bereits als "eng". Bisher wurde + // daraus nur der Titelzusatz "(Englisch)"; der Code selbst war danach nicht mehr vorhanden. + final String filmUrl = "/content/documents/zdf/serien/hardsun/hard-sun-1-100.json"; + final String videoUrl = "/tmd/2/android_native_5/vod/ptmd/mediathek/180416_2215_sendung_hsn"; + setupSuccessfulJsonResponse(filmUrl, "/zdf/zdf_film_details_english.json"); + setupSuccessfulJsonResponse(videoUrl, "/zdf/zdf_video_details_english.json"); + setupHeadResponse(404); + + final Set actual = executeTask(filmUrl); + + final Film german = + actual.stream().filter(f -> !f.getTitel().contains("(Englisch)")).findFirst().orElseThrow(); + final Film english = + actual.stream().filter(f -> f.getTitel().contains("(Englisch)")).findFirst().orElseThrow(); + + assertThat(english.getAudioLanguage(), equalTo("eng")); + // Die deutsche Fassung bleibt leer - wie ihr Titel, der ebenfalls keinen Zusatz bekommt. + assertThat(german.getAudioLanguage(), nullValue()); + } + @Test void testGermanAndEnglish() { From b020e088d7ea8a7849a86daf8fac392b5d540f93 Mon Sep 17 00:00:00 2001 From: CuriousApe2020 Date: Thu, 17 Sep 2026 21:15:46 +0000 Subject: [PATCH 3/3] Sprache der Tonspur auch fuer arte Baut auf dem ARD-Teil auf und fuellt dasselbe Feld Film.audioLanguage fuer arte. arte liefert die Sprache je Tonspur im Listing unter videos[].versions[].audioLanguage. Ausgewertet wurde davon bisher nur der danebenstehende audioCode, um den Videotyp zu bestimmen; die Sprache blieb ungenutzt und war im Filmliste-Eintrag nur noch als Titelzusatz "(Originalversion)" sichtbar, der sie nicht benennt. ArteVideoInfoDeserializer liest den versions-Block jetzt als Zuordnung Versionscode auf Sprachcode, ArteVideoInfoDto haelt sie, und ArteDtoVideo2FilmTask setzt sie auf den Originalversions-Film - passend zum ARD-Teil traegt nur die Originalfassung die Sprache. Belegt an den vorhandenen Fixtures: VOA auf "de", VF-STF auf "fr", VE[ESP] auf "es". Fuer die reine Originalfassung (Code "VO") reicht dieser Block nicht: arte zeichnet sie dort mit "und" aus, eine mehrsprachige Fassung ("VOEU") mit "mul". Dafuer wird zusaetzlich videos[].originalLanguage gelesen, das MServer bisher gar nicht ausgewertet hat - bei einer Originalfassung ist die Sprache des Originals genau die der Tonspur. Fuer die synchronisierte Fassung waere derselbe Wert falsch: in arte_videos_1.json steht dort "fr" bei mainAudioVersion "VA", also bei der deutschen Fassung eines franzoesischen Films. Deshalb wird resolveAudioLanguage nur fuer die Originalversionen aufgerufen. Das erklaert auch den Befund zu "VO": in allen Fixtures, deren mainAudioVersion "VO" ist, fuehrt originalLanguage entweder den arte-Code MUS ("musikalisch", leerer ISO-Code) oder null. "VO" ist dort der Marker fuer Beitraege ohne Dialog, "und" also richtig und nicht lueckenhaft; fremdsprachige Originale laufen ueber VOF-STA, VOA-STF und VO-STE[...] und tragen einen echten Code. Wo arte selbst nichts benennt, bleibt das Feld leer - zu raten waere schlechter, als nichts zu sagen. Nicht am Streams-Endpunkt angesetzt: dessen Antwort (videoStreams, siehe ArteVideoLinkDeserializer) fuehrt zwar audioCode und audioLabel, aber kein audioLanguage. Das Label hilft dort ebenfalls nicht weiter - bei "VO-STE[ANG]" lautet es "Originalfassung - UT Englisch", die genannte Sprache ist also die der Untertitel, nicht die der Tonspur. Ausserdem neu: LanguageCodeUtils als gemeinsame Stelle fuer die Normalisierung. Die Sender sind darin uneinheitlich - die ARD schreibt dreistellig ("deu"), arte zweistellig ("de") -, und beide verwenden Platzhalter, die keine Sprache benennen ("ov" bei der ARD, "und" und "mul" bei arte). Die zuvor im ArdFilmDeserializer private Normalisierung ist dorthin gewandert; die Zuordnung zweistellig auf dreistellig steht als Tabelle da und nicht ueber Locale, weil dessen Sprachdaten unter Linux an ICU haengen und nicht auf jedem Host vollstaendig vorliegen. Tests: Auslesen des versions-Blocks am echten Fixture inklusive der Platzhalter, der Rueckgriff auf originalLanguage ("fr" auf "fra") samt Gegenprobe mit dem leeren ISO-Code von MUS, sowie die Normalisierung ueber beide Schreibweisen, Grossschreibung, Regionsangaben und unbrauchbare Werte. --- .../mserver/base/utils/LanguageCodeUtils.java | 108 ++++++++++++++++++ .../crawler/ard/json/ArdFilmDeserializer.java | 23 +--- .../arte/json/ArteVideoInfoDeserializer.java | 40 +++++++ .../crawler/arte/json/ArteVideoInfoDto.java | 28 +++++ .../arte/tasks/ArteDtoVideo2FilmTask.java | 55 ++++++++- .../base/utils/LanguageCodeUtilsTest.java | 57 +++++++++ .../json/ArteVideoInfoAudioLanguageTest.java | 84 ++++++++++++++ 7 files changed, 372 insertions(+), 23 deletions(-) create mode 100644 src/main/java/de/mediathekview/mserver/base/utils/LanguageCodeUtils.java create mode 100644 src/test/java/de/mediathekview/mserver/base/utils/LanguageCodeUtilsTest.java create mode 100644 src/test/java/de/mediathekview/mserver/crawler/arte/json/ArteVideoInfoAudioLanguageTest.java 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 16b34ebe5..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; @@ -624,7 +625,7 @@ private Optional parseAudioLanguageCode(final JsonObject playerPageObjec final boolean matches = code.equalsIgnoreCase(language) || (language.equalsIgnoreCase("*") && !code.equalsIgnoreCase(MARKER_VIDEO_DE) && !code.equalsIgnoreCase("ov")); if (matches) { - final Optional normalized = normalizeLanguageCode(code); + final Optional normalized = LanguageCodeUtils.normalize(code); if (normalized.isPresent()) { return normalized; } @@ -633,24 +634,4 @@ private Optional parseAudioLanguageCode(final JsonObject playerPageObjec } return Optional.empty(); } - - /** - * Bringt einen Sprachcode aus den Senderdaten in die Form, die das Feld zusichert: dreistelliger - * ISO-639-2/T-Code in Kleinschreibung. - * - *

Eine etwaige Regionsangabe wird abgeschnitten ("spa-ES" wird zu "spa"). Alles, was danach - * kein dreistelliger Buchstabencode ist - ARDs Platzhalter "ov", ein leerer Wert, ein - * zweistelliger Code, den MServer mangels Tabelle nicht zuordnen koennte - ergibt nichts. Lieber - * kein Wert als einer, auf den sich Clients nicht verlassen koennen. - */ - private static Optional normalizeLanguageCode(final String rawCode) { - if (rawCode == null || rawCode.isBlank()) { - return Optional.empty(); - } - final String primary = rawCode.trim().split("-")[0].toLowerCase(Locale.ROOT); - if (primary.length() != 3 || !primary.chars().allMatch(Character::isLetter)) { - return Optional.empty(); - } - return "ov".equals(primary) ? Optional.empty() : Optional.of(primary); - } } diff --git a/src/main/java/de/mediathekview/mserver/crawler/arte/json/ArteVideoInfoDeserializer.java b/src/main/java/de/mediathekview/mserver/crawler/arte/json/ArteVideoInfoDeserializer.java index 414d551aa..45277bdd2 100644 --- a/src/main/java/de/mediathekview/mserver/crawler/arte/json/ArteVideoInfoDeserializer.java +++ b/src/main/java/de/mediathekview/mserver/crawler/arte/json/ArteVideoInfoDeserializer.java @@ -2,6 +2,8 @@ import java.lang.reflect.Type; import java.util.ArrayList; +import java.util.Map; +import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Optional; @@ -41,6 +43,12 @@ public class ArteVideoInfoDeserializer implements JsonDeserializer parseVideoInfoElement(final JsonElement arr ); arteRestVideoInfoDto.setSubtitleLinks(arteRestSubtitleLinkDto); + arteRestVideoInfoDto.setAudioLanguagesByCode(parseAudioLanguages(arrayElement)); + arteRestVideoInfoDto.setOriginalLanguage( + JsonUtils.getElementValueAsString(arrayElement, TAG_ORIGINAL_LANGUAGE).orElse(null)); return Optional.of(arteRestVideoInfoDto); } + /** + * Liest die Sprache der Tonspur je Versionscode aus {@code versions[]}. + * + *

Bisher wurde dieser Block nicht ausgewertet. Der Versionscode allein ("VOF", "VOA", "VO") + * landet spaeter nur als Titelzusatz "(Originalversion)" in der Filmliste, der die Sprache nicht + * benennt - obwohl arte sie hier daneben mitliefert. + * + * @param arrayElement der Eintrag aus {@code videos[]}. + * @return Zuordnung Versionscode auf Sprachcode; leer, wenn der Block fehlt. + */ + private Map parseAudioLanguages(final JsonElement arrayElement) { + if (!arrayElement.getAsJsonObject().has(TAG_VERSIONS) + || !arrayElement.getAsJsonObject().get(TAG_VERSIONS).isJsonArray()) { + return Map.of(); + } + + final Map audioLanguages = new HashMap<>(); + for (JsonElement version : arrayElement.getAsJsonObject().get(TAG_VERSIONS).getAsJsonArray()) { + final Optional audioCode = + JsonUtils.getElementValueAsString(version, TAG_VERSIONS_AUDIO_CODE); + final Optional audioLanguage = + JsonUtils.getElementValueAsString(version, TAG_VERSIONS_AUDIO_LANGUAGE); + if (audioCode.isPresent() && audioLanguage.isPresent()) { + audioLanguages.putIfAbsent(audioCode.get(), audioLanguage.get()); + } + } + return audioLanguages; + } + } diff --git a/src/main/java/de/mediathekview/mserver/crawler/arte/json/ArteVideoInfoDto.java b/src/main/java/de/mediathekview/mserver/crawler/arte/json/ArteVideoInfoDto.java index b58ab6e18..6aa63c92f 100644 --- a/src/main/java/de/mediathekview/mserver/crawler/arte/json/ArteVideoInfoDto.java +++ b/src/main/java/de/mediathekview/mserver/crawler/arte/json/ArteVideoInfoDto.java @@ -1,6 +1,7 @@ package de.mediathekview.mserver.crawler.arte.json; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Optional; @@ -39,6 +40,19 @@ public class ArteVideoInfoDto extends CrawlerUrlDTO { private Optional pageIndex; private List videoLinks; private List subtitleLinks; + /** + * Sprache der Tonspur je arte-Versionscode, so wie arte sie im Listing unter + * {@code videos[].versions[]} liefert (z.B. "VOF" auf "fr"). Die Werte sind unveraendert + * uebernommen und enthalten daher auch arte-eigene Platzhalter wie "und" und "mul". + */ + private Map audioLanguagesByCode = Map.of(); + /** + * Sprache des Originals, so wie arte sie im Listing unter {@code videos[].originalLanguage} + * fuehrt (dort als zweistelliger Code in {@code iso6391Code}). Unveraendert uebernommen, also + * ggf. leer oder ein Platzhalter wie "mul". Gilt fuer das Werk, nicht fuer die einzelne + * Tonspur - eine deutsch synchronisierte Fassung eines franzoesischen Films fuehrt hier "fr". + */ + private String originalLanguage; // ONLY for unit tests public ArteVideoInfoDto(Optional id, Optional programId,Optional kind, Optional language) { @@ -174,6 +188,20 @@ public void setVideoLinks(List input) { videoLinks = input; } + public Map getAudioLanguagesByCode() { + return audioLanguagesByCode; + } + public void setAudioLanguagesByCode(Map input) { + audioLanguagesByCode = input == null ? Map.of() : input; + } + + public Optional getOriginalLanguage() { + return Optional.ofNullable(originalLanguage); + } + public void setOriginalLanguage(String input) { + originalLanguage = input; + } + public List getSubtitleLinks() { return subtitleLinks; } diff --git a/src/main/java/de/mediathekview/mserver/crawler/arte/tasks/ArteDtoVideo2FilmTask.java b/src/main/java/de/mediathekview/mserver/crawler/arte/tasks/ArteDtoVideo2FilmTask.java index 575e72540..e870391c6 100644 --- a/src/main/java/de/mediathekview/mserver/crawler/arte/tasks/ArteDtoVideo2FilmTask.java +++ b/src/main/java/de/mediathekview/mserver/crawler/arte/tasks/ArteDtoVideo2FilmTask.java @@ -16,6 +16,8 @@ import de.mediathekview.mserver.crawler.arte.ArteVideoType; import de.mediathekview.mserver.crawler.arte.ArteRestVideoTypeMapper; +import de.mediathekview.mserver.crawler.arte.json.ArteVideoLinkDto; +import de.mediathekview.mserver.base.utils.LanguageCodeUtils; import de.mediathekview.mserver.crawler.arte.json.ArteVideoInfoDto; import de.mediathekview.mserver.crawler.basic.AbstractCrawler; import de.mediathekview.mserver.crawler.basic.AbstractRecursiveConverterTask; @@ -64,10 +66,16 @@ protected void parse(ArteVideoInfoDto aElement) { Map originalVersionSubs = buildVideoUrls(aElement, ArteVideoType.ORIGINAL_WITH_SUBTITLE); if (originalVersion.size() > 0) { Film film = buildFilmBody(aElement); - addFilm(buildFilmBody(aElement), film.getTitel()+ " (Originalversion)", originalVersion, originalVersionSubs); + Film originalVersionFilm = buildFilmBody(aElement); + originalVersionFilm.setAudioLanguage( + resolveAudioLanguage(aElement, ArteVideoType.ORIGINAL).orElse(null)); + addFilm(originalVersionFilm, film.getTitel()+ " (Originalversion)", originalVersion, originalVersionSubs); } else if (originalVersionSubs.size() > 0) { // es gibt nur FR und FR mit UT dann nehmen wir FR mit UT Film film = buildFilmBody(aElement); - addFilm(buildFilmBody(aElement), film.getTitel()+ " (Originalversion mit Untertitel)", originalVersionSubs, null); + Film originalVersionFilm = buildFilmBody(aElement); + originalVersionFilm.setAudioLanguage( + resolveAudioLanguage(aElement, ArteVideoType.ORIGINAL_WITH_SUBTITLE).orElse(null)); + addFilm(originalVersionFilm, film.getTitel()+ " (Originalversion mit Untertitel)", originalVersionSubs, null); } // ARTE provides subs as a new video Map videoSub = buildVideoUrls(aElement, ArteVideoType.SUBTITLE_INCLUDED); @@ -191,6 +199,49 @@ protected Map buildVideoUrls(ArteVideoInfoDto aElement, Art return urls; } + + /** + * Liest die Sprache der Tonspur fuer den ersten Videolink, der zum uebergebenen Typ gehoert. + * + *

arte liefert sie im Listing unter {@code videos[].versions[].audioLanguage}, je + * Versionscode. Ausgewertet wurde bisher nur der Versionscode selbst, um den Videotyp zu + * bestimmen; die Sprache blieb ungenutzt und war im Filmliste-Eintrag nur noch als Titelzusatz + * "(Originalversion)" sichtbar, der sie nicht benennt. + * + *

Bei einer reinen Originalfassung (Code "VO") hilft dieser Block allein nicht weiter: arte + * zeichnet sie dort mit {@code und} aus, eine mehrsprachige Fassung ("VOEU") mit {@code mul}. + * Fuer diesen Fall wird auf {@code videos[].originalLanguage} zurueckgegriffen - die Sprache des + * Originals ist bei einer Originalfassung genau die der Tonspur. Fuer die synchronisierte Fassung + * waere derselbe Wert falsch, weshalb diese Methode nur fuer die Originalversionen aufgerufen + * wird. + * + *

Bleibt auch das ohne Ergebnis, bleibt das Feld leer. Zu raten waere schlechter, als nichts + * zu sagen. + * + * @param aElement die Videoinformationen des Beitrags. + * @param type der gesuchte Videotyp. + * @return die Sprache als ISO-639-2/T-Code, oder leer. + */ + protected Optional resolveAudioLanguage(ArteVideoInfoDto aElement, ArteVideoType type) { + for (ArteVideoLinkDto entry : aElement.getVideoLinks()) { + if (entry.getAudioCode().isEmpty()) { + continue; + } + final String audioCode = entry.getAudioCode().get(); + final Optional audioTypeCode = + ArteRestVideoTypeMapper.map(crawler.getSender(), audioCode); + if (audioTypeCode.isEmpty() || !audioTypeCode.get().equals(type)) { + continue; + } + final Optional language = + LanguageCodeUtils.normalize(aElement.getAudioLanguagesByCode().get(audioCode)); + if (language.isPresent()) { + return language; + } + } + return LanguageCodeUtils.normalize(aElement.getOriginalLanguage().orElse(null)); + } + protected Map builRawVideoUrls(ArteVideoInfoDto aElement, ArteVideoType type) { final Map urls = new EnumMap<>(Resolution.class); aElement.getVideoLinks().forEach( entry -> { diff --git a/src/test/java/de/mediathekview/mserver/base/utils/LanguageCodeUtilsTest.java b/src/test/java/de/mediathekview/mserver/base/utils/LanguageCodeUtilsTest.java new file mode 100644 index 000000000..bbac63ac4 --- /dev/null +++ b/src/test/java/de/mediathekview/mserver/base/utils/LanguageCodeUtilsTest.java @@ -0,0 +1,57 @@ +package de.mediathekview.mserver.base.utils; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.MatcherAssert.assertThat; + +import java.util.Optional; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; + +class LanguageCodeUtilsTest { + + @ParameterizedTest + @CsvSource({ + // arte liefert zweistellig + "de, deu", + "fr, fra", + "es, spa", + "it, ita", + "pl, pol", + // die ARD dreistellig + "deu, deu", + "eng, eng", + "fra, fra", + // Grossschreibung und Regionsangaben duerfen nichts aendern + "DE, deu", + "de-DE, deu", + "spa-ES, spa", + "EN, eng" + }) + void knownCodesBecomeTheThreeLetterForm(final String input, final String expected) { + assertThat(LanguageCodeUtils.normalize(input), equalTo(Optional.of(expected))); + } + + @ParameterizedTest + @ValueSource( + strings = { + "ov", // ARDs Marker fuer die Originalfassung + "und", // arte bei Code "VO" - unbestimmt + "mul", // arte bei Code "VOEU" - mehrsprachig + "mis", + "zxx", + "OV", + "UND" + }) + void placeholdersNameNoLanguage(final String input) { + assertThat(LanguageCodeUtils.normalize(input), equalTo(Optional.empty())); + } + + @ParameterizedTest + @NullAndEmptySource + @ValueSource(strings = {" ", "x", "abcd", "12", "123", "d-e"}) + void unusableValuesYieldNothing(final String input) { + assertThat(LanguageCodeUtils.normalize(input), equalTo(Optional.empty())); + } +} diff --git a/src/test/java/de/mediathekview/mserver/crawler/arte/json/ArteVideoInfoAudioLanguageTest.java b/src/test/java/de/mediathekview/mserver/crawler/arte/json/ArteVideoInfoAudioLanguageTest.java new file mode 100644 index 000000000..8d05b4663 --- /dev/null +++ b/src/test/java/de/mediathekview/mserver/crawler/arte/json/ArteVideoInfoAudioLanguageTest.java @@ -0,0 +1,84 @@ +package de.mediathekview.mserver.crawler.arte.json; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasEntry; + +import com.google.gson.JsonElement; +import de.mediathekview.mserver.base.utils.LanguageCodeUtils; +import de.mediathekview.mserver.crawler.basic.PagedElementListDTO; +import de.mediathekview.mserver.testhelper.JsonFileReader; +import java.util.Map; +import java.util.Optional; +import org.junit.jupiter.api.Test; + +/** + * arte liefert die Sprache der Tonspur im Listing unter {@code videos[].versions[].audioLanguage}. + * Bisher wurde dieser Block nicht ausgewertet. + */ +class ArteVideoInfoAudioLanguageTest { + + @Test + void audioLanguagesAreReadFromTheVersionsBlock() { + final Map audioLanguages = + firstVideo("/arte/arte_film_1_videos.json").getAudioLanguagesByCode(); + + assertThat(audioLanguages, notNullValue()); + assertThat(audioLanguages, hasEntry("VOA", "de")); + assertThat(audioLanguages, hasEntry("VF-STF", "fr")); + // Die mehrsprachige Fassung traegt arte-seitig den Platzhalter "mul". + assertThat(audioLanguages, hasEntry("VOEU[ALL]-STE[ANG]", "mul")); + } + + @Test + void normalizingTurnsArtesCodesIntoTheFieldsForm() { + final Map audioLanguages = + firstVideo("/arte/arte_film_1_videos.json").getAudioLanguagesByCode(); + + assertThat(LanguageCodeUtils.normalize(audioLanguages.get("VOA")), equalTo(Optional.of("deu"))); + assertThat(LanguageCodeUtils.normalize(audioLanguages.get("VF-STF")), equalTo(Optional.of("fra"))); + // "mul" benennt keine einzelne Sprache - das Feld bleibt leer, statt zu raten. + assertThat( + LanguageCodeUtils.normalize(audioLanguages.get("VOEU[ALL]-STE[ANG]")), + equalTo(Optional.empty())); + } + + @Test + void aVideoWithoutVersionsBlockYieldsAnEmptyMapInsteadOfFailing() { + assertThat( + firstVideo("/arte/arte_film_2_videos.json").getAudioLanguagesByCode(), notNullValue()); + } + + /** + * Fuer die reine Originalfassung (Code "VO") traegt der versions-Block nur den Platzhalter + * {@code und}. arte fuehrt die Sprache des Originals daneben in {@code originalLanguage}. + */ + @Test + void theOriginalLanguageIsReadAsFallbackForTheVoPlaceholder() { + final ArteVideoInfoDto video = firstVideo("/arte/arte_film_5_videos.json"); + + assertThat(video.getOriginalLanguage(), equalTo(Optional.of("fr"))); + assertThat(LanguageCodeUtils.normalize(video.getOriginalLanguage().get()), equalTo(Optional.of("fra"))); + } + + /** + * Wo arte das Original selbst nicht benennt - bei einer rein musikalischen Fassung steht dort der + * arte-Code "MUS" mit leerem ISO-Code - bleibt auch der Rueckgriff ohne Ergebnis. + */ + @Test + void anUnnamedOriginalLanguageStaysEmpty() { + final ArteVideoInfoDto video = firstVideo("/arte/arte_film_6_videos.json"); + + assertThat( + LanguageCodeUtils.normalize(video.getOriginalLanguage().orElse(null)), + equalTo(Optional.empty())); + } + + private ArteVideoInfoDto firstVideo(final String jsonFile) { + final JsonElement jsonElement = JsonFileReader.readJson(jsonFile); + final PagedElementListDTO result = + new ArteVideoInfoDeserializer().deserialize(jsonElement, null, null); + return result.getElements().stream().findFirst().orElseThrow(); + } +}