Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>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<String> 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.
*
* <p>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.
*
* <p>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<String, String> 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.
*
* <p>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<String> 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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -223,19 +224,23 @@ public List<ArdFilmDto> 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;
Expand Down Expand Up @@ -430,15 +435,26 @@ private Optional<ArdVideoInfoDto> 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;
Expand All @@ -454,6 +470,7 @@ private Optional<ArdVideoInfoDto> 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() ) {
Expand Down Expand Up @@ -559,4 +576,62 @@ private Optional<Map<Integer, String>> parseVideoUrlMap(final JsonObject playerP
}
return Optional.of(videoInfo);
}

/**
* Liefert den tatsaechlichen Sprachcode ({@code audio[0].languageCode}) des ersten Streams, der
* zum uebergebenen Filter passt.
*
* <p>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.
*
* <p>Die Platzhalter "OV" und "*" selbst benennen keine Sprache; ARDs eigener Platzhalter "ov"
* im Feld wird deshalb verworfen.
*/
private Optional<String> parseAudioLanguageCode(final JsonObject playerPageObject, String streamType, String aduioType, String mimeType, String language) {
final Optional<JsonObject> mediaCollectionObject = getMediaCollectionObject(playerPageObject);
if (mediaCollectionObject.isEmpty()) {
return Optional.empty();
}
final Optional<JsonElement> 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<String> streamKind = JsonUtils.getElementValueAsString(streamsCategory, ATTRIBUTE_KIND);
final Optional<JsonElement> 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<String> mime = JsonUtils.getElementValueAsString(video, ATTRIBUTE_MIME);
if (mime.isEmpty() || !mime.get().equalsIgnoreCase(mimeType)) {
continue;
}
final Optional<JsonElement> audios = JsonUtils.getElement(video, ELEMENT_AUDIO);
if (audios.isEmpty() || !audios.get().isJsonArray() || audios.get().getAsJsonArray().isEmpty()) {
continue;
}
final Optional<String> kind = JsonUtils.getElementValueAsString(audios.get().getAsJsonArray().get(0), ATTRIBUTE_KIND);
final Optional<String> 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<String> normalized = LanguageCodeUtils.normalize(code);
if (normalized.isPresent()) {
return normalized;
}
}
}
}
return Optional.empty();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ public class ArdVideoInfoDto {
private final Map<Resolution, String> videoUrlsDGS;
private final Map<Resolution, String> 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<String> subtitleUrl;

Expand Down Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -41,6 +43,12 @@ public class ArteVideoInfoDeserializer implements JsonDeserializer<PagedElementL
private static final String TAG_SUBTITLES_FILENAME = "filename";

private static final String TAG_VIDEO_INFO = "videos";

private static final String[] TAG_ORIGINAL_LANGUAGE = {"originalLanguage", "iso6391Code"};

private static final String TAG_VERSIONS = "versions";
private static final String TAG_VERSIONS_AUDIO_CODE = "audioCode";
private static final String TAG_VERSIONS_AUDIO_LANGUAGE = "audioLanguage";

private static final String TAG_FIRST_BROADCAST_DATE = "firstBroadcastDate";
private static final String TAG_ID = "id";
Expand Down Expand Up @@ -147,8 +155,40 @@ protected Optional<ArteVideoInfoDto> 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[]}.
*
* <p>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<String, String> parseAudioLanguages(final JsonElement arrayElement) {
if (!arrayElement.getAsJsonObject().has(TAG_VERSIONS)
|| !arrayElement.getAsJsonObject().get(TAG_VERSIONS).isJsonArray()) {
return Map.of();
}

final Map<String, String> audioLanguages = new HashMap<>();
for (JsonElement version : arrayElement.getAsJsonObject().get(TAG_VERSIONS).getAsJsonArray()) {
final Optional<String> audioCode =
JsonUtils.getElementValueAsString(version, TAG_VERSIONS_AUDIO_CODE);
final Optional<String> audioLanguage =
JsonUtils.getElementValueAsString(version, TAG_VERSIONS_AUDIO_LANGUAGE);
if (audioCode.isPresent() && audioLanguage.isPresent()) {
audioLanguages.putIfAbsent(audioCode.get(), audioLanguage.get());
}
}
return audioLanguages;
}

}
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -39,6 +40,19 @@ public class ArteVideoInfoDto extends CrawlerUrlDTO {
private Optional<String> pageIndex;
private List<ArteVideoLinkDto> videoLinks;
private List<ArteSubtitleLinkDto> 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<String, String> 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<String> id, Optional<String> programId,Optional<String> kind, Optional<String> language) {
Expand Down Expand Up @@ -174,6 +188,20 @@ public void setVideoLinks(List<ArteVideoLinkDto> input) {
videoLinks = input;
}

public Map<String, String> getAudioLanguagesByCode() {
return audioLanguagesByCode;
}
public void setAudioLanguagesByCode(Map<String, String> input) {
audioLanguagesByCode = input == null ? Map.of() : input;
}

public Optional<String> getOriginalLanguage() {
return Optional.ofNullable(originalLanguage);
}
public void setOriginalLanguage(String input) {
originalLanguage = input;
}

public List<ArteSubtitleLinkDto> getSubtitleLinks() {
return subtitleLinks;
}
Expand Down
Loading