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
Expand Up @@ -223,19 +223,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 +434,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 +469,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 +575,82 @@ 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 = 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.
*
* <p>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<String> 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);
}
}
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
28 changes: 28 additions & 0 deletions src/main/java/de/mediathekview/mserver/daten/Film.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,17 @@ public class Film extends Podcast {
private Map<Resolution, FilmUrl> audioDescriptions;
private Map<Resolution, FilmUrl> 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.
*
* <p>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,
Expand All @@ -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! */
Expand All @@ -53,9 +65,25 @@ public AbstractMediaResource<FilmUrl> merge(final AbstractMediaResource<FilmUrl>
|| 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<URL> urlsToAdd) {
subtitles.addAll(urlsToAdd);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Film> readRecord(JsonReader jsonReader) throws IOException {
cnt++;
Expand Down Expand Up @@ -212,6 +223,13 @@ private Optional<Film> 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();
//
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

Expand All @@ -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.
*
* <p>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();
Expand Down
Loading