From e86c0a6e953fd9765dcb6ceed8a821bbf327e3ad Mon Sep 17 00:00:00 2001 From: Scarriffle Date: Fri, 2 Oct 2026 21:05:34 +0200 Subject: [PATCH] =?UTF-8?q?Ein=20Download=20riss=20ab,=20galt=20als=20fert?= =?UTF-8?q?ig=20=E2=80=94=20und=20setzte=20nie=20fort?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dein Bericht: bei rund 3 GB abgebrochen, beim Fortsetzen wieder bei null, und später „plötzlich bei 60 Prozent fertig". Beide Hälften standen so im Code. **Fertig war sie nicht, sie war halb.** if expected > 0, Double(written) < Double(expected) * 0.5 { verwerfen } Alles über der Hälfte galt als vollständig. Diese Regel war nie für diesen Fall gedacht, sondern für die **hochgerechnete** Größe der Transcoder-Stufen (Bitrate × Dauer) — dort ist Abweichung normal. Beim Original nennt der Server die Dateigröße in den Metadaten, und da ist jedes fehlende Byte ein Abriss. Das unterscheidet jetzt `expectedIsExact`, gesetzt beim Planen und mitgeführt am Eintrag. Dazu merkt sich der Koordinator, für welche Titel der Server eine `Content-Length` geschickt hat — auch das ist eine Zusage. **„Fortsetzen" war eine Zusage, die der Code nicht halten konnte.** `NSURLSessionDownloadTaskResumeData` kam im ganzen Projekt nicht vor; den Wiederaufnahme-Datensatz gab es nur beim Pausieren von Hand. Nach einem Netzfehler war er weg, und der Knopf daneben hieß trotzdem „Fortsetzen". Jetzt wird er aus dem Fehler gerettet, und wo es ihn nicht gibt, heißt der Knopf „Erneut versuchen". **Und es merkte niemand, wenn das WLAN zurückkam.** `RootView` reicht das jetzt weiter — nur für Einträge mit Wiederaufnahme-Datensatz, damit ein 404 sich nicht im Kreis versucht. **Rate und Restzeit** stehen in der Zeile: „12 MB / 20 MB · 3,4 MB/s · gleich fertig". `DownloadRate` ist ein reiner Wertetyp mit Uhr von außen — geglättet, weil rohe Differenzen um ein Vielfaches springen, und ohne Zahl, solange nichts gemessen ist. Neun Tests. **Zwei Dinge, die erst die Messung gezeigt hat** — und die beide nicht in der App liegen: Erstens: reißt eine Übertragung ab, **die eine `Content-Length` angekündigt hatte**, wiederholt die Hintergrundsitzung von iOS sie selbst — stumm, ohne den Delegaten zu fragen, und **jedes Mal ab Byte null**. Gemessen: sieben Anläufe, jeder die vollen 12 MB. Das ist genau dein „fing wieder bei null an", und dagegen hilft kein Wiederaufnahme-Datensatz. Zweitens: ein Fortsetzen braucht eine Prüfmarke vom Server. Ohne `ETag` oder `Last-Modified` kann iOS sein `If-Range` nicht stellen und lädt alles noch einmal — gemessen, 20 MB zweimal. Mit Marke kam die zweite Anfrage als `Range: bytes=5636096-` und holte nur den Rest. Die Attrappe kann das jetzt alles nachstellen: eine beliebig große Datei, die auf Wunsch mittendrin abreißt, mit oder ohne angekündigte Länge, und gedrosselt. Ohne sie ist nichts davon beweisbar — und genau daran ist der Fehler vorbeigekommen. Im Simulator gegen die Attrappe: 12 von 20 MB → „Fehlgeschlagen: Übertragung unvollständig", nicht Haken. Vollständig → Haken. iOS und tvOS bauen, 489 Tests grün. Co-Authored-By: Claude Opus 5 --- Flixy/Resources/Localizable.xcstrings | 9808 +++++++++-------- Flixy/Session/DownloadService.swift | 5 + Flixy/Views/Downloads/DownloadsView.swift | 54 +- Flixy/Views/Navigation/RootView.swift | 13 + .../Sources/FlixyDownloads/DownloadRate.swift | 102 + .../Engine/DownloadCoordinator.swift | 131 +- .../FlixyDownloads/Model/DownloadItem.swift | 15 +- .../DownloadCoordinatorTests.swift | 16 + .../DownloadRateTests.swift | 118 + .../DownloadRulesTests.swift | 60 +- Tools/JellyfinStub/server.py | 130 +- 11 files changed, 5549 insertions(+), 4903 deletions(-) create mode 100644 Packages/FlixyKit/Sources/FlixyDownloads/DownloadRate.swift create mode 100644 Packages/FlixyKit/Tests/FlixyDownloadsTests/DownloadRateTests.swift diff --git a/Flixy/Resources/Localizable.xcstrings b/Flixy/Resources/Localizable.xcstrings index df25801..f355e58 100644 --- a/Flixy/Resources/Localizable.xcstrings +++ b/Flixy/Resources/Localizable.xcstrings @@ -1,7575 +1,7629 @@ { - "sourceLanguage" : "en", - "strings" : { - " · " : { - "comment" : "A separator between different parts of the episode metadata.", - "isCommentAutoGenerated" : true + "sourceLanguage": "en", + "strings": { + " · ": { + "comment": "A separator between different parts of the episode metadata.", + "isCommentAutoGenerated": true }, - "%@" : { - "comment" : "A number in a list.", - "isCommentAutoGenerated" : true + "%@": { + "comment": "A number in a list.", + "isCommentAutoGenerated": true }, - "%@ → %@" : { - "comment" : "A line in a diagnostic trace that shows a network request path and its result. The first argument is the network request path. The second argument is the network response result.", - "isCommentAutoGenerated" : true, - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "%1$@ → %2$@" + "%@ GB": { + "comment": "A picker option in the settings view that lets the user select the maximum amount of storage space that can be used for downloaded content. The value is given in gigabytes.", + "isCommentAutoGenerated": true + }, + "%@ → %@": { + "comment": "A line in a diagnostic trace that shows a network request path and its result. The first argument is the network request path. The second argument is the network response result.", + "isCommentAutoGenerated": true, + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "%1$@ → %2$@" } } } }, - "%@ GB" : { - "comment" : "A picker option in the settings view that lets the user select the maximum amount of storage space that can be used for downloaded content. The value is given in gigabytes.", - "isCommentAutoGenerated" : true + "4K": { + "comment": "A label for a 4K video.", + "isCommentAutoGenerated": true + }, + "Full HD": { + "comment": "A label for a media resolution.", + "isCommentAutoGenerated": true + }, + "HD": { + "comment": "A label for a media resolution.", + "isCommentAutoGenerated": true }, - "4K" : { - "comment" : "A label for a 4K video.", - "isCommentAutoGenerated" : true + "SD": { + "comment": "A label for a 720p movie.", + "isCommentAutoGenerated": true + }, + "account.hint": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Öffnet Einstellungen und Abmelden" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "Opens settings and sign out" + } + } + } }, - "account.hint" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Öffnet Einstellungen und Abmelden" + "account.settings": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Einstellungen" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Opens settings and sign out" + "en": { + "stringUnit": { + "state": "translated", + "value": "Settings" } } } }, - "account.settings" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Einstellungen" + "action.addFavourite": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Zu Favoriten" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Settings" + "en": { + "stringUnit": { + "state": "translated", + "value": "Add to favourites" } } } }, - "action.addFavourite" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Zu Favoriten" + "action.apply": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Übernehmen" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Add to favourites" + "en": { + "stringUnit": { + "state": "translated", + "value": "Apply" } } } }, - "action.apply" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Übernehmen" + "action.cancel": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Abbrechen" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Apply" + "en": { + "stringUnit": { + "state": "translated", + "value": "Cancel" } } } }, - "action.cancel" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Abbrechen" + "action.clearRating": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Bewertung entfernen" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Cancel" + "en": { + "stringUnit": { + "state": "translated", + "value": "Clear rating" } } } }, - "action.clearRating" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bewertung entfernen" + "action.close": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Schließen" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Clear rating" + "en": { + "stringUnit": { + "state": "translated", + "value": "Close" } } } }, - "action.close" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Schließen" + "action.delete": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Löschen" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Close" + "en": { + "stringUnit": { + "state": "translated", + "value": "Delete" } } } }, - "action.delete" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Löschen" + "action.done": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Fertig" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Delete" + "en": { + "stringUnit": { + "state": "translated", + "value": "Done" } } } }, - "action.done" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Fertig" + "action.markUnwatched": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Als ungesehen markieren" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Done" + "en": { + "stringUnit": { + "state": "translated", + "value": "Mark as unwatched" } } } }, - "action.markUnwatched" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Als ungesehen markieren" + "action.markWatched": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Als gesehen markieren" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mark as unwatched" + "en": { + "stringUnit": { + "state": "translated", + "value": "Mark as watched" } } } }, - "action.markWatched" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Als gesehen markieren" + "action.rate": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Bewerten" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mark as watched" + "en": { + "stringUnit": { + "state": "translated", + "value": "Rate" } } } }, - "action.rate" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bewerten" + "action.removeFavourite": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Aus Favoriten entfernen" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Rate" + "en": { + "stringUnit": { + "state": "translated", + "value": "Remove from favourites" } } } }, - "action.removeFavourite" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Aus Favoriten entfernen" + "action.removeFromContinue": { + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Aus „Weiterschauen“ entfernen" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Remove from favourites" + "en": { + "stringUnit": { + "state": "translated", + "value": "Remove from Continue Watching" } } } }, - "action.removeFromContinue" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Aus „Weiterschauen“ entfernen" + "action.retry": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Erneut versuchen" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Remove from Continue Watching" + "en": { + "stringUnit": { + "state": "translated", + "value": "Try again" } } } }, - "action.retry" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Erneut versuchen" + "action.signOut": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Abmelden" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Try again" + "en": { + "stringUnit": { + "state": "translated", + "value": "Sign out" } } } }, - "action.signOut" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Abmelden" + "appearance.accent": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Akzentfarbe" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sign out" + "en": { + "stringUnit": { + "state": "translated", + "value": "Accent color" } } } }, - "appearance.accent" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Akzentfarbe" + "appearance.accentExplanation": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Die Farbe erscheint auf Knöpfen, Fortschrittsbalken und der ausgewählten Zeile in der Seitenleiste." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Accent color" + "en": { + "stringUnit": { + "state": "translated", + "value": "The color appears on buttons, progress bars and the selected row in the sidebar." } } } }, - "appearance.accentExplanation" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Die Farbe erscheint auf Knöpfen, Fortschrittsbalken und der ausgewählten Zeile in der Seitenleiste." + "appearance.adjusted %@": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Die Farbe wurde auf %@ aufgehellt, damit sie auf dem dunklen Hintergrund lesbar bleibt." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "The color appears on buttons, progress bars and the selected row in the sidebar." + "en": { + "stringUnit": { + "state": "translated", + "value": "Lightened to %@ so it stays readable on the dark background." } } } }, - "appearance.adjusted %@" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Die Farbe wurde auf %@ aufgehellt, damit sie auf dem dunklen Hintergrund lesbar bleibt." + "appearance.cardScale": { + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Kachelgröße" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Lightened to %@ so it stays readable on the dark background." + "en": { + "stringUnit": { + "state": "translated", + "value": "Tile size" } } } }, - "appearance.cardScale" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Kachelgröße" + "appearance.cardScaleExplanation": { + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Wie groß Plakate und Vorschaubilder erscheinen — auf der Startseite und in den Mediatheken. Die Einstellung gilt nur auf diesem Gerät." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tile size" + "en": { + "stringUnit": { + "state": "translated", + "value": "How large posters and thumbnails appear — on the home screen and in libraries. This setting applies to this device only." } } } }, - "appearance.cardScaleExplanation" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Wie groß Plakate und Vorschaubilder erscheinen — auf der Startseite und in den Mediatheken. Die Einstellung gilt nur auf diesem Gerät." + "appearance.customColor": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Eigene Farbe" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "How large posters and thumbnails appear — on the home screen and in libraries. This setting applies to this device only." + "en": { + "stringUnit": { + "state": "translated", + "value": "Custom color" } } } }, - "appearance.customColor" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Eigene Farbe" + "appearance.hexPlaceholder": { + "comment": "Beispiel für eine Hex-Farbe", + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "#1DB954" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Custom color" + "en": { + "stringUnit": { + "state": "translated", + "value": "#1DB954" } } } }, - "appearance.hexPlaceholder" : { - "comment" : "Beispiel für eine Hex-Farbe", - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "#1DB954" + "appearance.invalidHex": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Das ist keine gültige Farbe. Erwartet wird etwas wie #1DB954." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "#1DB954" + "en": { + "stringUnit": { + "state": "translated", + "value": "That's not a valid color. Try something like #1DB954." } } } }, - "appearance.invalidHex" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Das ist keine gültige Farbe. Erwartet wird etwas wie #1DB954." + "appearance.navigation": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Navigation" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "That's not a valid color. Try something like #1DB954." + "en": { + "stringUnit": { + "state": "translated", + "value": "Navigation" } } } }, - "appearance.navigation" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Navigation" + "appearance.navigationExplanation": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Seitlich fährt die Leiste über den Inhalt und gibt ihn danach wieder frei. Oben stehen alle Einträge gleichzeitig da und nehmen dauerhaft eine Zeile ein." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Navigation" + "en": { + "stringUnit": { + "state": "translated", + "value": "At the side, the bar slides over the content and gets out of the way again. At the top, every entry is visible at once and takes up a row permanently." } } } }, - "appearance.navigationExplanation" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Seitlich fährt die Leiste über den Inhalt und gibt ihn danach wieder frei. Oben stehen alle Einträge gleichzeitig da und nehmen dauerhaft eine Zeile ein." + "appearance.navigationSide": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Seitlich" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "At the side, the bar slides over the content and gets out of the way again. At the top, every entry is visible at once and takes up a row permanently." + "en": { + "stringUnit": { + "state": "translated", + "value": "At the side" } } } }, - "appearance.navigationSide" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Seitlich" + "appearance.navigationTop": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Oben" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "At the side" + "en": { + "stringUnit": { + "state": "translated", + "value": "At the top" } } } }, - "appearance.navigationTop" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Oben" + "appearance.preview": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Vorschau" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "At the top" + "en": { + "stringUnit": { + "state": "translated", + "value": "Preview" } } } }, - "appearance.preview" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vorschau" + "appearance.title": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Erscheinungsbild" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Preview" + "en": { + "stringUnit": { + "state": "translated", + "value": "Appearance" } } } }, - "appearance.title" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Erscheinungsbild" + "connecting.searching": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Server werden gesucht …" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Appearance" + "en": { + "stringUnit": { + "state": "translated", + "value": "Looking for servers…" } } } }, - "connecting.searching" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Server werden gesucht …" + "connecting.to %@": { + "comment": "Platzhalter: Servername", + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Verbinde mit %@ …" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Looking for servers…" + "en": { + "stringUnit": { + "state": "translated", + "value": "Connecting to %@…" } } } }, - "connecting.to %@" : { - "comment" : "Platzhalter: Servername", - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Verbinde mit %@ …" + "detail.cast": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Besetzung" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Connecting to %@…" + "en": { + "stringUnit": { + "state": "translated", + "value": "Cast" } } } }, - "detail.cast" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Besetzung" + "detail.episodeCount %lld": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "%lld Folgen" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Cast" + "en": { + "stringUnit": { + "state": "translated", + "value": "%lld episodes" } } } }, - "detail.episodeCount %lld" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lld Folgen" + "detail.episodes": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Folgen" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lld episodes" + "en": { + "stringUnit": { + "state": "translated", + "value": "Episodes" } } } }, - "detail.episodes" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Folgen" + "detail.playFromStart": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Von vorn abspielen" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Episodes" + "en": { + "stringUnit": { + "state": "translated", + "value": "Play from start" } } } }, - "detail.playFromStart" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Von vorn abspielen" + "detail.seasons": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Staffeln" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Play from start" + "en": { + "stringUnit": { + "state": "translated", + "value": "Seasons" } } } }, - "detail.seasons" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Staffeln" + "detail.toSeason": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Zur Staffel" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Seasons" + "en": { + "stringUnit": { + "state": "translated", + "value": "Go to season" } } } }, - "detail.toSeason" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Zur Staffel" + "detail.toSeries": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Zur Serie" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Go to season" + "en": { + "stringUnit": { + "state": "translated", + "value": "Go to series" } } } }, - "detail.toSeries" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Zur Serie" + "detail.unwatchedCount %lld": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "%lld ungesehen" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Go to series" + "en": { + "stringUnit": { + "state": "translated", + "value": "%lld unwatched" } } } }, - "detail.unwatchedCount %lld" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lld ungesehen" + "detail.versions": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Fassung" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lld unwatched" + "en": { + "stringUnit": { + "state": "translated", + "value": "Version" } } } }, - "detail.versions" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Fassung" + "diagnostics.connection": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Verbindung" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Version" + "en": { + "stringUnit": { + "state": "translated", + "value": "Connection" } } } }, - "diagnostics.connection" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Verbindung" + "diagnostics.connectionUsed": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Verbindung" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Connection" + "en": { + "stringUnit": { + "state": "translated", + "value": "Connection" } } } }, - "diagnostics.connectionUsed" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Verbindung" + "diagnostics.copy": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Bericht kopieren" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Connection" + "en": { + "stringUnit": { + "state": "translated", + "value": "Copy report" } } } }, - "diagnostics.copy" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bericht kopieren" + "diagnostics.copyHint": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Enthält keinen Token. Kannst du unbesorgt weitergeben." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Copy report" + "en": { + "stringUnit": { + "state": "translated", + "value": "Contains no token. Safe to share." } } } }, - "diagnostics.copyHint" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Enthält keinen Token. Kannst du unbesorgt weitergeben." + "diagnostics.lastAttempt": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Letzter Abspielversuch" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Contains no token. Safe to share." + "en": { + "stringUnit": { + "state": "translated", + "value": "Last playback attempt" } } } }, - "diagnostics.lastAttempt" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Letzter Abspielversuch" + "diagnostics.liveTV": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Live-TV-Antwort ansehen" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Last playback attempt" + "en": { + "stringUnit": { + "state": "translated", + "value": "Inspect Live TV response" } } } }, - "diagnostics.liveTV" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Live-TV-Antwort ansehen" + "diagnostics.liveTVHint": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Zeigt den Aufbau der Serverantwort — nur Schlüssel, Anzahl und Sendernamen, keine Zugangsdaten. Hilft dabei zu finden, wo die Sender stehen." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Inspect Live TV response" + "en": { + "stringUnit": { + "state": "translated", + "value": "Shows the structure of the server's response — keys, counts and channel names only, no credentials. Helps locate where the channels are." } } } }, - "diagnostics.liveTVHint" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Zeigt den Aufbau der Serverantwort — nur Schlüssel, Anzahl und Sendernamen, keine Zugangsdaten. Hilft dabei zu finden, wo die Sender stehen." + "diagnostics.method": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Weg" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Shows the structure of the server's response — keys, counts and channel names only, no credentials. Helps locate where the channels are." + "en": { + "stringUnit": { + "state": "translated", + "value": "Method" } } } }, - "diagnostics.method" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Weg" + "diagnostics.noServer": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Keine Serververbindung." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Method" + "en": { + "stringUnit": { + "state": "translated", + "value": "No server connection." } } } }, - "diagnostics.noServer" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Keine Serververbindung." + "diagnostics.nothingYet": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Noch nichts abgespielt." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "No server connection." + "en": { + "stringUnit": { + "state": "translated", + "value": "Nothing played yet." } } } }, - "diagnostics.nothingYet" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Noch nichts abgespielt." + "diagnostics.relayWarning": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Über die Plex-Vermittlung. Stark gedrosselt — große Dateien laufen darüber kaum an." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nothing played yet." + "en": { + "stringUnit": { + "state": "translated", + "value": "Via the Plex relay. Heavily throttled — large files barely start over it." } } } }, - "diagnostics.relayWarning" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Über die Plex-Vermittlung. Stark gedrosselt — große Dateien laufen darüber kaum an." + "diagnostics.source": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Quelle (ohne Token)" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Via the Plex relay. Heavily throttled — large files barely start over it." + "en": { + "stringUnit": { + "state": "translated", + "value": "Source (token removed)" } } } }, - "diagnostics.source" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Quelle (ohne Token)" + "diagnostics.steps": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Ablauf" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Source (token removed)" + "en": { + "stringUnit": { + "state": "translated", + "value": "Timeline" } } } }, - "diagnostics.steps" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ablauf" + "diagnostics.title": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Diagnose" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Timeline" + "en": { + "stringUnit": { + "state": "translated", + "value": "Diagnostics" } } } }, - "diagnostics.title" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Diagnose" + "diagnostics.title.label": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Titel" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Diagnostics" + "en": { + "stringUnit": { + "state": "translated", + "value": "Title" } } } }, - "diagnostics.title.label" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Titel" + "downloads.active": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Läuft gerade" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Title" + "en": { + "stringUnit": { + "state": "translated", + "value": "In progress" } } } }, - "downloads.active" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Läuft gerade" + "downloads.allEpisodes": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Alle Folgen laden" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "In progress" + "en": { + "stringUnit": { + "state": "translated", + "value": "Download all episodes" } } } }, - "downloads.allEpisodes" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Alle Folgen laden" + "downloads.available": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Offline verfügbar" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Download all episodes" + "en": { + "stringUnit": { + "state": "translated", + "value": "Available offline" } } } }, - "downloads.available" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Offline verfügbar" + "downloads.batch.queued %lld": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "%lld Folgen in die Warteschlange gelegt." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Available offline" + "en": { + "stringUnit": { + "state": "translated", + "value": "Queued %lld episodes." } } } }, - "downloads.batch.queued %lld" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lld Folgen in die Warteschlange gelegt." + "downloads.batch.title": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Downloads" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Queued %lld episodes." + "en": { + "stringUnit": { + "state": "translated", + "value": "Downloads" } } } }, - "downloads.batch.title" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Downloads" + "downloads.batch.unavailable %lld": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "%lld Folgen haben keine abspielbare Datei auf dem Server." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Downloads" + "en": { + "stringUnit": { + "state": "translated", + "value": "%lld episodes have no playable file on the server." } } } }, - "downloads.batch.unavailable %lld" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lld Folgen haben keine abspielbare Datei auf dem Server." + "downloads.cancel": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Download abbrechen" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lld episodes have no playable file on the server." + "en": { + "stringUnit": { + "state": "translated", + "value": "Cancel download" } } } }, - "downloads.cancel" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Download abbrechen" + "downloads.download": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Herunterladen" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Cancel download" + "en": { + "stringUnit": { + "state": "translated", + "value": "Download" } } } }, - "downloads.download" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Herunterladen" + "downloads.empty.description": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Lade Filme und Folgen herunter, um sie ohne Verbindung anzusehen." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Download" + "en": { + "stringUnit": { + "state": "translated", + "value": "Download movies and episodes to watch them without a connection." } } } }, - "downloads.empty.description" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Lade Filme und Folgen herunter, um sie ohne Verbindung anzusehen." + "downloads.empty.title": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Nichts offline" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Download movies and episodes to watch them without a connection." + "en": { + "stringUnit": { + "state": "translated", + "value": "Nothing offline" } } } }, - "downloads.empty.title" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nichts offline" + "downloads.failed": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Fehlgeschlagen" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nothing offline" + "en": { + "stringUnit": { + "state": "translated", + "value": "Failed" } } } }, - "downloads.failed" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Fehlgeschlagen" + "downloads.hold.offline": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Keine Verbindung" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Failed" + "en": { + "stringUnit": { + "state": "translated", + "value": "No connection" } } } }, - "downloads.hold.offline" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Keine Verbindung" + "downloads.hold.offline.description": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Ohne Verbindung geht nichts weiter. Angefangene Downloads laufen weiter, sobald das Gerät wieder am Netz ist." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "No connection" + "en": { + "stringUnit": { + "state": "translated", + "value": "Nothing transfers without a connection. Started downloads continue once the device is back online." } } } }, - "downloads.hold.offline.description" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ohne Verbindung geht nichts weiter. Angefangene Downloads laufen weiter, sobald das Gerät wieder am Netz ist." + "downloads.hold.wifi": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Wartet auf WLAN" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nothing transfers without a connection. Started downloads continue once the device is back online." + "en": { + "stringUnit": { + "state": "translated", + "value": "Waiting for Wi-Fi" } } } }, - "downloads.hold.wifi" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Wartet auf WLAN" + "downloads.hold.wifi.description": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "„Nur über WLAN“ ist eingeschaltet und das Gerät hängt gerade am Mobilfunk. Sobald WLAN da ist, geht es von selbst weiter." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Waiting for Wi-Fi" + "en": { + "stringUnit": { + "state": "translated", + "value": "“Wi-Fi only” is on and the device is on cellular. Transfers resume by themselves once Wi-Fi is back." } } } }, - "downloads.hold.wifi.description" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "„Nur über WLAN“ ist eingeschaltet und das Gerät hängt gerade am Mobilfunk. Sobald WLAN da ist, geht es von selbst weiter." + "downloads.movies": { + "comment": "Überschrift für heruntergeladene Filme", + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Filme" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "“Wi-Fi only” is on and the device is on cellular. Transfers resume by themselves once Wi-Fi is back." + "en": { + "stringUnit": { + "state": "translated", + "value": "Movies" } } } }, - "downloads.movies" : { - "comment" : "Überschrift für heruntergeladene Filme", - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Filme" + "downloads.pause": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Anhalten" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Movies" + "en": { + "stringUnit": { + "state": "translated", + "value": "Pause" } } } }, - "downloads.pause" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Anhalten" + "downloads.paused": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Angehalten" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Pause" + "en": { + "stringUnit": { + "state": "translated", + "value": "Paused" } } } }, - "downloads.paused" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Angehalten" + "downloads.perSecond %@": { + "comment": "Tempo einer laufenden Übertragung", + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "%@/s" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Paused" + "en": { + "stringUnit": { + "state": "translated", + "value": "%@/s" } } } }, - "downloads.quality.approx %@ %@" : { - "comment" : "Platzhalter: Stufenname, geschätzte Größe", - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "%@ · etwa %@" + "downloads.quality.approx %@ %@": { + "comment": "Platzhalter: Stufenname, geschätzte Größe", + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "%@ · etwa %@" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "%@ · about %@" + "en": { + "stringUnit": { + "state": "translated", + "value": "%@ · about %@" } } } }, - "downloads.quality.hint" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Niedrigere Stufen rechnet der Server unterwegs herunter. Das dauert länger, spart aber deutlich Platz." + "downloads.quality.hint": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Niedrigere Stufen rechnet der Server unterwegs herunter. Das dauert länger, spart aber deutlich Platz." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Lower levels are transcoded by the server. That takes longer but saves a lot of space." + "en": { + "stringUnit": { + "state": "translated", + "value": "Lower levels are transcoded by the server. That takes longer but saves a lot of space." } } } }, - "downloads.quality.title" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "In welcher Qualität?" + "downloads.quality.title": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "In welcher Qualität?" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Which quality?" + "en": { + "stringUnit": { + "state": "translated", + "value": "Which quality?" } } } }, - "downloads.queued" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Wartet" + "downloads.queued": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Wartet" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Queued" + "en": { + "stringUnit": { + "state": "translated", + "value": "Queued" } } } }, - "downloads.refused.exists" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Dieser Titel steht bereits in der Warteschlange." + "downloads.refused.exists": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Dieser Titel steht bereits in der Warteschlange." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "This title is already queued." + "en": { + "stringUnit": { + "state": "translated", + "value": "This title is already queued." } } } }, - "downloads.refused.limit %@ %@" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Das Speicherlimit ist erreicht: %@ von %@ belegt." + "downloads.refused.limit %@ %@": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Das Speicherlimit ist erreicht: %@ von %@ belegt." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Storage limit reached: %@ of %@ used." + "en": { + "stringUnit": { + "state": "translated", + "value": "Storage limit reached: %@ of %@ used." } } } }, - "downloads.refused.noMedia" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Zu diesem Titel liegt keine Datei auf dem Server." + "downloads.refused.noMedia": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Zu diesem Titel liegt keine Datei auf dem Server." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "There's no file for this title on the server." + "en": { + "stringUnit": { + "state": "translated", + "value": "There's no file for this title on the server." } } } }, - "downloads.refused.noPart" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Die Datei auf dem Server hat keinen abrufbaren Teil. Möglicherweise wurde sie noch nicht analysiert." + "downloads.refused.noPart": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Die Datei auf dem Server hat keinen abrufbaren Teil. Möglicherweise wurde sie noch nicht analysiert." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "The file on the server has no retrievable part. It may not have been analyzed yet." + "en": { + "stringUnit": { + "state": "translated", + "value": "The file on the server has no retrievable part. It may not have been analyzed yet." } } } }, - "downloads.refused.noServer" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Keine Verbindung zum Server." + "downloads.refused.noServer": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Keine Verbindung zum Server." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "No connection to the server." + "en": { + "stringUnit": { + "state": "translated", + "value": "No connection to the server." } } } }, - "downloads.refused.noURL" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Die Download-Adresse ließ sich nicht bilden." + "downloads.refused.noURL": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Die Download-Adresse ließ sich nicht bilden." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "The download address couldn't be built." + "en": { + "stringUnit": { + "state": "translated", + "value": "The download address couldn't be built." } } } }, - "downloads.refused.space %@ %@" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Zu wenig Platz auf dem Gerät: %@ nötig, %@ frei." + "downloads.refused.space %@ %@": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Zu wenig Platz auf dem Gerät: %@ nötig, %@ frei." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Not enough space on this device: %@ needed, %@ free." + "en": { + "stringUnit": { + "state": "translated", + "value": "Not enough space on this device: %@ needed, %@ free." } } } }, - "downloads.refused.title" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Download nicht möglich" + "downloads.refused.title": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Download nicht möglich" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Can't download" + "en": { + "stringUnit": { + "state": "translated", + "value": "Can't download" } } } }, - "downloads.removeConfirm" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Download entfernen?" + "downloads.remaining %@": { + "comment": "Geschätzte Restzeit eines Downloads", + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "noch %@" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Remove download?" + "en": { + "stringUnit": { + "state": "translated", + "value": "%@ left" } } } }, - "downloads.resume" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Fortsetzen" + "downloads.remaining.underMinute": { + "comment": "Restzeit unter einer Minute — eine Zahl wäre hier genauer, als die Schätzung ist", + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "gleich fertig" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Resume" + "en": { + "stringUnit": { + "state": "translated", + "value": "almost done" } } } }, - "downloads.season" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Staffel laden" + "downloads.removeConfirm": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Download entfernen?" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Download season" + "en": { + "stringUnit": { + "state": "translated", + "value": "Remove download?" } } } }, - "downloads.used" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Belegt" + "downloads.resume": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Fortsetzen" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Used" + "en": { + "stringUnit": { + "state": "translated", + "value": "Resume" } } } }, - "downloads.wifiOnly" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nur über WLAN laden" + "downloads.season": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Staffel laden" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Download over Wi-Fi only" + "en": { + "stringUnit": { + "state": "translated", + "value": "Download season" } } } }, - "episode.long %lld %lld" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Staffel %lld · Episode %lld" + "downloads.used": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Belegt" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Season %lld · Episode %lld" + "en": { + "stringUnit": { + "state": "translated", + "value": "Used" } } } }, - "episode.number %lld" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Folge %lld" + "downloads.wifiOnly": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Nur über WLAN laden" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Episode %lld" + "en": { + "stringUnit": { + "state": "translated", + "value": "Download over Wi-Fi only" } } } }, - "episode.short %lld %lld" : { - "comment" : "Nur auf Kacheln, wo eine Zeile breit ist. Wo Platz ist — Player, Aufmacher am Fernseher — steht episode.long.", - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "S%lld E%lld" + "episode.long %lld %lld": { + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Staffel %lld · Episode %lld" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "S%lld E%lld" + "en": { + "stringUnit": { + "state": "translated", + "value": "Season %lld · Episode %lld" } } } }, - "error.noServers" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "An deinem Plex-Konto hängt kein Server." + "episode.number %lld": { + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Folge %lld" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "There's no server on your Plex account." + "en": { + "stringUnit": { + "state": "translated", + "value": "Episode %lld" } } } }, - "error.notFound" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Dieser Titel ist auf dem Server nicht mehr vorhanden." + "episode.short %lld %lld": { + "comment": "Nur auf Kacheln, wo eine Zeile breit ist. Wo Platz ist — Player, Aufmacher am Fernseher — steht episode.long.", + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "S%lld E%lld" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "This title is no longer on the server." + "en": { + "stringUnit": { + "state": "translated", + "value": "S%lld E%lld" } } } }, - "error.server %lld" : { - "comment" : "Platzhalter: HTTP-Statuscode", - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Der Server hat mit Fehler %lld geantwortet." + "error.noServers": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "An deinem Plex-Konto hängt kein Server." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "The server responded with error %lld." + "en": { + "stringUnit": { + "state": "translated", + "value": "There's no server on your Plex account." } } } }, - "error.serverUnreachable" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Der Server ist nicht erreichbar. Prüfe, ob er läuft und ob du im selben Netzwerk bist." + "error.notFound": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Dieser Titel ist auf dem Server nicht mehr vorhanden." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "The server can't be reached. Check that it's running and that you're on the same network." + "en": { + "stringUnit": { + "state": "translated", + "value": "This title is no longer on the server." } } } }, - "error.title" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Das hat nicht geklappt" + "error.server %lld": { + "comment": "Platzhalter: HTTP-Statuscode", + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Der Server hat mit Fehler %lld geantwortet." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Something went wrong" + "en": { + "stringUnit": { + "state": "translated", + "value": "The server responded with error %lld." } } } }, - "error.unauthorized" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Die Anmeldung ist abgelaufen. Melde dich erneut an." + "error.serverUnreachable": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Der Server ist nicht erreichbar. Prüfe, ob er läuft und ob du im selben Netzwerk bist." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Your sign-in has expired. Please sign in again." + "en": { + "stringUnit": { + "state": "translated", + "value": "The server can't be reached. Check that it's running and that you're on the same network." } } } }, - "error.unexpectedResponse" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Die Antwort des Servers war unerwartet. Möglicherweise ist eine neuere Plex-Version installiert." + "error.title": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Das hat nicht geklappt" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "The server's response was unexpected. It may be running a newer version of Plex." + "en": { + "stringUnit": { + "state": "translated", + "value": "Something went wrong" } } } }, - "filter.any" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Alle" + "error.unauthorized": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Die Anmeldung ist abgelaufen. Melde dich erneut an." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Any" + "en": { + "stringUnit": { + "state": "translated", + "value": "Your sign-in has expired. Please sign in again." } } } }, - "filter.filters" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Filter" + "error.unexpectedResponse": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Die Antwort des Servers war unerwartet. Möglicherweise ist eine neuere Plex-Version installiert." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Filters" + "en": { + "stringUnit": { + "state": "translated", + "value": "The server's response was unexpected. It may be running a newer version of Plex." } } } }, - "filter.genre" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Genre" + "filter.any": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Alle" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Genre" + "en": { + "stringUnit": { + "state": "translated", + "value": "Any" } } } }, - "filter.librarySettings" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Diese Mediathek" + "filter.filters": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Filter" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "This library" + "en": { + "stringUnit": { + "state": "translated", + "value": "Filters" } } } }, - "filter.reset" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Zurücksetzen" + "filter.genre": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Genre" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Reset" + "en": { + "stringUnit": { + "state": "translated", + "value": "Genre" } } } }, - "filter.resolution" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Auflösung" + "filter.librarySettings": { + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Diese Mediathek" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Resolution" + "en": { + "stringUnit": { + "state": "translated", + "value": "This library" } } } }, - "filter.sort" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sortierung" + "filter.reset": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Zurücksetzen" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sort by" + "en": { + "stringUnit": { + "state": "translated", + "value": "Reset" } } } }, - "filter.title" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Filter" + "filter.resolution": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Auflösung" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Filters" + "en": { + "stringUnit": { + "state": "translated", + "value": "Resolution" } } } }, - "filter.unwatchedOnly" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nur Ungesehene" + "filter.sort": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Sortierung" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Unwatched only" + "en": { + "stringUnit": { + "state": "translated", + "value": "Sort by" } } } }, - "Full HD" : { - "comment" : "A label for a media resolution.", - "isCommentAutoGenerated" : true + "filter.title": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Filter" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "Filters" + } + } + } }, - "HD" : { - "comment" : "A label for a media resolution.", - "isCommentAutoGenerated" : true + "filter.unwatchedOnly": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Nur Ungesehene" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "Unwatched only" + } + } + } }, - "hero.details" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Details" + "hero.details": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Details" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Details" + "en": { + "stringUnit": { + "state": "translated", + "value": "Details" } } } }, - "hero.next" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nächster Vorschlag" + "hero.next": { + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Nächster Vorschlag" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Next suggestion" + "en": { + "stringUnit": { + "state": "translated", + "value": "Next suggestion" } } } }, - "hero.play" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Abspielen" + "hero.play": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Abspielen" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Play" + "en": { + "stringUnit": { + "state": "translated", + "value": "Play" } } } }, - "hero.previous" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Voriger Vorschlag" + "hero.previous": { + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Voriger Vorschlag" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Previous suggestion" + "en": { + "stringUnit": { + "state": "translated", + "value": "Previous suggestion" } } } }, - "hero.resume" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Weiterschauen" + "hero.resume": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Weiterschauen" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Resume" + "en": { + "stringUnit": { + "state": "translated", + "value": "Resume" } } } }, - "home.combineContinue" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Weiterschauen und Als Nächstes zusammenlegen" + "home.combineContinue": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Weiterschauen und Als Nächstes zusammenlegen" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Merge Continue and Next Up" + "en": { + "stringUnit": { + "state": "translated", + "value": "Merge Continue and Next Up" } } } }, - "home.empty.description" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sobald dein Server Mediatheken enthält, erscheinen sie hier." + "home.empty.description": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Sobald dein Server Mediatheken enthält, erscheinen sie hier." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Once your server has libraries, they'll show up here." + "en": { + "stringUnit": { + "state": "translated", + "value": "Once your server has libraries, they'll show up here." } } } }, - "home.empty.title" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Noch nichts da" + "home.empty.title": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Noch nichts da" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nothing here yet" + "en": { + "stringUnit": { + "state": "translated", + "value": "Nothing here yet" } } } }, - "home.hero" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Aufmacher" + "home.hero": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Aufmacher" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Featured" + "en": { + "stringUnit": { + "state": "translated", + "value": "Featured" } } } }, - "home.heroCount" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Titel im Wechsel" + "home.heroCount": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Titel im Wechsel" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Titles in rotation" + "en": { + "stringUnit": { + "state": "translated", + "value": "Titles in rotation" } } } }, - "home.heroExplanation" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Der Aufmacher wandert alle sieben Sekunden weiter und hört damit auf, sobald du ihn berührst." + "home.heroExplanation": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Der Aufmacher wandert alle sieben Sekunden weiter und hört damit auf, sobald du ihn berührst." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "The featured area moves on every seven seconds and stops as soon as you touch it." + "en": { + "stringUnit": { + "state": "translated", + "value": "The featured area moves on every seven seconds and stops as soon as you touch it." } } } }, - "home.heroHeight" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Höhe des Aufmachers" + "home.heroHeight": { + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Höhe des Aufmachers" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Featured height" + "en": { + "stringUnit": { + "state": "translated", + "value": "Featured height" } } } }, - "home.heroLibraries" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mediatheken für den Aufmacher" + "home.heroLibraries": { + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Mediatheken für den Aufmacher" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Libraries for the hero" + "en": { + "stringUnit": { + "state": "translated", + "value": "Libraries for the hero" } } } }, - "home.heroLibrariesExplanation" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ohne Auswahl kommt alles infrage. Ist etwas gewählt, speist sich der Aufmacher nur noch aus diesen Mediatheken — Weiterschauen und Favoriten bleiben dann draussen, weil der Server nicht sagt, aus welcher Mediathek ein angefangener Titel stammt." + "home.heroLibrariesExplanation": { + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Ohne Auswahl kommt alles infrage. Ist etwas gewählt, speist sich der Aufmacher nur noch aus diesen Mediatheken — Weiterschauen und Favoriten bleiben dann draussen, weil der Server nicht sagt, aus welcher Mediathek ein angefangener Titel stammt." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "With nothing selected, everything is eligible. Once you select libraries, the hero is drawn only from them — Continue Watching and Favourites stay out, because the server does not say which library a part-watched title came from." + "en": { + "stringUnit": { + "state": "translated", + "value": "With nothing selected, everything is eligible. Once you select libraries, the hero is drawn only from them — Continue Watching and Favourites stay out, because the server does not say which library a part-watched title came from." } } } }, - "home.heroLibrariesNoMatch" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Für die gewählten Mediatheken gibt es derzeit keine eigene Reihe auf der Startseite. Die Auswahl greift deshalb nicht — der Aufmacher zeigt alles." + "home.heroLibrariesNoMatch": { + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Für die gewählten Mediatheken gibt es derzeit keine eigene Reihe auf der Startseite. Die Auswahl greift deshalb nicht — der Aufmacher zeigt alles." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "The selected libraries currently have no row of their own on the home screen, so the selection has no effect — the hero shows everything." + "en": { + "stringUnit": { + "state": "translated", + "value": "The selected libraries currently have no row of their own on the home screen, so the selection has no effect — the hero shows everything." } } } }, - "home.heroSource" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Inhalt" + "home.heroSource": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Inhalt" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Content" + "en": { + "stringUnit": { + "state": "translated", + "value": "Content" } } } }, - "home.heroSource.continueFirst" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Angefangenes zuerst" + "home.heroSource.continueFirst": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Angefangenes zuerst" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Started titles first" + "en": { + "stringUnit": { + "state": "translated", + "value": "Started titles first" } } } }, - "home.heroSource.latestOnly" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nur Neuzugänge" + "home.heroSource.latestOnly": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Nur Neuzugänge" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "New arrivals only" + "en": { + "stringUnit": { + "state": "translated", + "value": "New arrivals only" } } } }, - "home.hideWatched" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Gesehenes aus Neuzugängen ausblenden" + "home.hideWatched": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Gesehenes aus Neuzugängen ausblenden" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Hide watched titles from new arrivals" + "en": { + "stringUnit": { + "state": "translated", + "value": "Hide watched titles from new arrivals" } } } }, - "home.rowsHeader" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Reihen" + "home.rowsHeader": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Reihen" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Rows" + "en": { + "stringUnit": { + "state": "translated", + "value": "Rows" } } } }, - "homeLayout.empty" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Der Server liefert derzeit keine Reihen." + "homeLayout.empty": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Der Server liefert derzeit keine Reihen." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "The server isn't providing any rows right now." + "en": { + "stringUnit": { + "state": "translated", + "value": "The server isn't providing any rows right now." } } } }, - "homeLayout.explanation" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Schalte Reihen aus, die du nicht brauchst, und sortiere sie über „Bearbeiten“ um. Neue Reihen vom Server erscheinen am Ende." + "homeLayout.explanation": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Schalte Reihen aus, die du nicht brauchst, und sortiere sie über „Bearbeiten“ um. Neue Reihen vom Server erscheinen am Ende." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Turn off rows you don't need and reorder them via Edit. New rows from the server appear at the end." + "en": { + "stringUnit": { + "state": "translated", + "value": "Turn off rows you don't need and reorder them via Edit. New rows from the server appear at the end." } } } }, - "homeLayout.title" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Startseite" + "homeLayout.title": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Startseite" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Home screen" + "en": { + "stringUnit": { + "state": "translated", + "value": "Home screen" } } } }, - "item.watched" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Gesehen" + "item.watched": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Gesehen" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Watched" + "en": { + "stringUnit": { + "state": "translated", + "value": "Watched" } } } }, - "languages.audio" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tonspur" + "languages.audio": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Tonspur" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Audio track" + "en": { + "stringUnit": { + "state": "translated", + "value": "Audio track" } } } }, - "languages.audioExplanation" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Gibt es beim Abspielen eine Tonspur in dieser Sprache, wird sie von selbst gewählt. Sonst bleibt es bei der Spur, die der Server voranstellt." + "languages.audioExplanation": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Gibt es beim Abspielen eine Tonspur in dieser Sprache, wird sie von selbst gewählt. Sonst bleibt es bei der Spur, die der Server voranstellt." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "If a track in this language exists, it is selected automatically. Otherwise the server's own first track is used." + "en": { + "stringUnit": { + "state": "translated", + "value": "If a track in this language exists, it is selected automatically. Otherwise the server's own first track is used." } } } }, - "languages.audioHeader" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ton" + "languages.audioHeader": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Ton" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Audio" + "en": { + "stringUnit": { + "state": "translated", + "value": "Audio" } } } }, - "languages.avoidAudioDescription" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Audiodeskription meiden" + "languages.avoidAudioDescription": { + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Audiodeskription meiden" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Avoid audio description" + "en": { + "stringUnit": { + "state": "translated", + "value": "Avoid audio description" } } } }, - "languages.avoidAudioDescriptionExplanation" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Spuren, die eine Szenenbeschreibung für Sehbehinderte enthalten, werden nur genommen, wenn es keine andere in dieser Sprache gibt. Erkannt wird das am Namen der Spur — der Server kennzeichnet es nicht." + "languages.avoidAudioDescriptionExplanation": { + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Spuren, die eine Szenenbeschreibung für Sehbehinderte enthalten, werden nur genommen, wenn es keine andere in dieser Sprache gibt. Erkannt wird das am Namen der Spur — der Server kennzeichnet es nicht." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tracks that describe the scene for blind viewers are only used when there is no other track in that language. They are recognised by name — the server doesn't flag them." + "en": { + "stringUnit": { + "state": "translated", + "value": "Tracks that describe the scene for blind viewers are only used when there is no other track in that language. They are recognised by name — the server doesn't flag them." } } } }, - "languages.fallbackAdd" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sprache hinzufügen" + "languages.fallbackAdd": { + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Sprache hinzufügen" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Add language" + "en": { + "stringUnit": { + "state": "translated", + "value": "Add language" } } } }, - "languages.fallbackEmpty" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Keine Ausweichsprachen" + "languages.fallbackEmpty": { + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Keine Ausweichsprachen" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "No fallback languages" + "en": { + "stringUnit": { + "state": "translated", + "value": "No fallback languages" } } } }, - "languages.fallbackExplanation" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Gilt nur, wenn ein Titel die gewünschte Tonsprache gar nicht hat. Dann wird von oben nach unten gesucht." + "languages.fallbackExplanation": { + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Gilt nur, wenn ein Titel die gewünschte Tonsprache gar nicht hat. Dann wird von oben nach unten gesucht." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Applies only when a title doesn't have your preferred audio language at all. Then it works down the list." + "en": { + "stringUnit": { + "state": "translated", + "value": "Applies only when a title doesn't have your preferred audio language at all. Then it works down the list." } } } }, - "languages.fallbackHeader" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Rangliste" + "languages.fallbackHeader": { + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Rangliste" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Fallback order" + "en": { + "stringUnit": { + "state": "translated", + "value": "Fallback order" } } } }, - "languages.forThisLibrary" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tonsprache für diese Mediathek" + "languages.forThisLibrary": { + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Tonsprache für diese Mediathek" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Audio language for this library" + "en": { + "stringUnit": { + "state": "translated", + "value": "Audio language for this library" } } } }, - "languages.forThisLibraryExplanation" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Gilt für alle Titel dieser Mediathek. Eine Serie kann es überstimmen, und was du im Player von Hand wählst, überstimmt beides." + "languages.forThisLibraryExplanation": { + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Gilt für alle Titel dieser Mediathek. Eine Serie kann es überstimmen, und was du im Player von Hand wählst, überstimmt beides." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Applies to every title in this library. A series can override it, and what you pick by hand in the player overrides both." + "en": { + "stringUnit": { + "state": "translated", + "value": "Applies to every title in this library. A series can override it, and what you pick by hand in the player overrides both." } } } }, - "languages.forThisSeries" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tonsprache für diese Serie" + "languages.forThisSeries": { + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Tonsprache für diese Serie" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Audio language for this series" + "en": { + "stringUnit": { + "state": "translated", + "value": "Audio language for this series" } } } }, - "languages.headerExplanation" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Welche Tonspur und welche Untertitel beim Abspielen von selbst gewählt werden." + "languages.headerExplanation": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Welche Tonspur und welche Untertitel beim Abspielen von selbst gewählt werden." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Which audio track and which subtitles are chosen automatically during playback." + "en": { + "stringUnit": { + "state": "translated", + "value": "Which audio track and which subtitles are chosen automatically during playback." } } } }, - "languages.inherit" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Wie allgemein" + "languages.inherit": { + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Wie allgemein" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "As set generally" + "en": { + "stringUnit": { + "state": "translated", + "value": "As set generally" } } } }, - "languages.serverDefault" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Wie der Server vorgibt" + "languages.serverDefault": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Wie der Server vorgibt" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "As the server prefers" + "en": { + "stringUnit": { + "state": "translated", + "value": "As the server prefers" } } } }, - "languages.subtitles" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Untertitel" + "languages.subtitles": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Untertitel" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Subtitles" + "en": { + "stringUnit": { + "state": "translated", + "value": "Subtitles" } } } }, - "languages.subtitlesExplanation" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Untertitel werden nur eingeschaltet, wenn es sie in dieser Sprache gibt. „Aus“ heißt: gar keine, bis du sie im Player selbst einschaltest." + "languages.subtitlesExplanation": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Untertitel werden nur eingeschaltet, wenn es sie in dieser Sprache gibt. „Aus“ heißt: gar keine, bis du sie im Player selbst einschaltest." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Subtitles are only switched on when they exist in this language. “Off” means none until you turn them on in the player." + "en": { + "stringUnit": { + "state": "translated", + "value": "Subtitles are only switched on when they exist in this language. “Off” means none until you turn them on in the player." } } } }, - "languages.subtitlesHeader" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Untertitel" + "languages.subtitlesHeader": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Untertitel" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Subtitles" + "en": { + "stringUnit": { + "state": "translated", + "value": "Subtitles" } } } }, - "languages.subtitlesOff" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Aus" + "languages.subtitlesOff": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Aus" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Off" + "en": { + "stringUnit": { + "state": "translated", + "value": "Off" } } } }, - "languages.title" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ton & Untertitel" + "languages.title": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Ton & Untertitel" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Audio & subtitles" + "en": { + "stringUnit": { + "state": "translated", + "value": "Audio & subtitles" } } } }, - "library.allLoaded %@" : { - "comment" : "A footer in the library view that shows the number of items in the library.", - "isCommentAutoGenerated" : true + "library.allLoaded %@": { + "comment": "A footer in the library view that shows the number of items in the library.", + "isCommentAutoGenerated": true }, - "library.allLoaded %lld" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Alle %lld Titel geladen" + "library.allLoaded %lld": { + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Alle %lld Titel geladen" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "All %lld titles loaded" + "en": { + "stringUnit": { + "state": "translated", + "value": "All %lld titles loaded" } } } }, - "library.clearFilters" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Filter zurücksetzen" + "library.clearFilters": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Filter zurücksetzen" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Clear filters" + "en": { + "stringUnit": { + "state": "translated", + "value": "Clear filters" } } } }, - "library.empty.description" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mit den aktuellen Filtern bleibt nichts übrig." + "library.empty.description": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Mit den aktuellen Filtern bleibt nichts übrig." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nothing matches the current filters." + "en": { + "stringUnit": { + "state": "translated", + "value": "Nothing matches the current filters." } } } }, - "library.empty.title" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Keine Titel" + "library.empty.title": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Keine Titel" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "No titles" + "en": { + "stringUnit": { + "state": "translated", + "value": "No titles" } } } }, - "library.filters" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Filter" + "library.filters": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Filter" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Filters" + "en": { + "stringUnit": { + "state": "translated", + "value": "Filters" } } } }, - "library.kind.liveTV" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Live-TV" + "library.kind.liveTV": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Live-TV" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Live TV" + "en": { + "stringUnit": { + "state": "translated", + "value": "Live TV" } } } }, - "library.kind.movie" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Filme" + "library.kind.movie": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Filme" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Movies" + "en": { + "stringUnit": { + "state": "translated", + "value": "Movies" } } } }, - "library.kind.music" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Musik" + "library.kind.music": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Musik" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Music" + "en": { + "stringUnit": { + "state": "translated", + "value": "Music" } } } }, - "library.kind.other" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sonstiges" + "library.kind.other": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Sonstiges" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Other" + "en": { + "stringUnit": { + "state": "translated", + "value": "Other" } } } }, - "library.kind.photo" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Fotos" + "library.kind.photo": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Fotos" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Photos" + "en": { + "stringUnit": { + "state": "translated", + "value": "Photos" } } } }, - "library.kind.show" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Serien" + "library.kind.show": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Serien" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "TV shows" + "en": { + "stringUnit": { + "state": "translated", + "value": "TV shows" } } } }, - "library.loadMore" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Weitere laden" + "library.loadMore": { + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Weitere laden" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Load more" + "en": { + "stringUnit": { + "state": "translated", + "value": "Load more" } } } }, - "library.loadMoreFailed" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Das Nachladen ist hängen geblieben — es fehlen noch Titel." + "library.loadMoreFailed": { + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Das Nachladen ist hängen geblieben — es fehlen noch Titel." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loading more stalled — some titles are still missing." + "en": { + "stringUnit": { + "state": "translated", + "value": "Loading more stalled — some titles are still missing." } } } }, - "library.reload" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Neu laden" + "library.reload": { + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Neu laden" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Reload" + "en": { + "stringUnit": { + "state": "translated", + "value": "Reload" } } } }, - "library.scan" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mediathek durchsuchen" + "library.scan": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Mediathek durchsuchen" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Scan library" + "en": { + "stringUnit": { + "state": "translated", + "value": "Scan library" } } } }, - "library.search" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Suchen" + "library.search": { + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Suchen" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Search" + "en": { + "stringUnit": { + "state": "translated", + "value": "Search" } } } }, - "library.title" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mediatheken" + "library.title": { + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Mediatheken" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Libraries" + "en": { + "stringUnit": { + "state": "translated", + "value": "Libraries" } } } }, - "library.toggleLayout" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ansicht wechseln" + "library.toggleLayout": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Ansicht wechseln" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Switch layout" + "en": { + "stringUnit": { + "state": "translated", + "value": "Switch layout" } } } }, - "libraryHub.empty" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Auf diesem Server ist keine Mediathek zu sehen." + "libraryHub.empty": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Auf diesem Server ist keine Mediathek zu sehen." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "No library is visible on this server." + "en": { + "stringUnit": { + "state": "translated", + "value": "No library is visible on this server." } } } }, - "liveTV.alreadyScheduled" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Zur Aufnahme vorgemerkt" + "liveTV.alreadyScheduled": { + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Zur Aufnahme vorgemerkt" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Scheduled to record" + "en": { + "stringUnit": { + "state": "translated", + "value": "Scheduled to record" } } } }, - "liveTV.anyChannel" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Auf allen Sendern" + "liveTV.anyChannel": { + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Auf allen Sendern" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "On any channel" + "en": { + "stringUnit": { + "state": "translated", + "value": "On any channel" } } } }, - "liveTV.cancelTimer" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Absagen" + "liveTV.cancelTimer": { + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Absagen" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Cancel" + "en": { + "stringUnit": { + "state": "translated", + "value": "Cancel" } } } }, - "liveTV.channelCount %@" : { - "comment" : "A label displaying the number of channels. The argument is the number of channels.", - "isCommentAutoGenerated" : true + "liveTV.channelCount %@": { + "comment": "A label displaying the number of channels. The argument is the number of channels.", + "isCommentAutoGenerated": true }, - "liveTV.channelCount %lld" : { - "comment" : "Platzhalter: Anzahl Sender", - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lld Sender" + "liveTV.channelCount %lld": { + "comment": "Platzhalter: Anzahl Sender", + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "%lld Sender" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lld channels" + "en": { + "stringUnit": { + "state": "translated", + "value": "%lld channels" } } } }, - "liveTV.deleteRecording.confirm" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Aufnahme löschen?" + "liveTV.deleteRecording.confirm": { + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Aufnahme löschen?" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Delete recording?" + "en": { + "stringUnit": { + "state": "translated", + "value": "Delete recording?" } } } }, - "liveTV.gap" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Keine Angaben" + "liveTV.gap": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Keine Angaben" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "No data" + "en": { + "stringUnit": { + "state": "translated", + "value": "No data" } } } }, - "liveTV.mode" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ansicht" + "liveTV.mode": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Ansicht" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "View" + "en": { + "stringUnit": { + "state": "translated", + "value": "View" } } } }, - "liveTV.mode.guide" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Programm" + "liveTV.mode.guide": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Programm" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Guide" + "en": { + "stringUnit": { + "state": "translated", + "value": "Guide" } } } }, - "liveTV.mode.now" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Jetzt" + "liveTV.mode.now": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Jetzt" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Now" + "en": { + "stringUnit": { + "state": "translated", + "value": "Now" } } } }, - "liveTV.next %@ %@" : { - "comment" : "Platzhalter: Uhrzeit, Sendungstitel", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Danach %@ · %@" + "liveTV.next %@ %@": { + "comment": "Platzhalter: Uhrzeit, Sendungstitel", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Danach %@ · %@" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Next %@ · %@" + "en": { + "stringUnit": { + "state": "translated", + "value": "Next %@ · %@" } } } }, - "liveTV.noChannels.description" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Der Empfänger ist eingerichtet, liefert aber keine Sender." + "liveTV.noChannels.description": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Der Empfänger ist eingerichtet, liefert aber keine Sender." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "The tuner is set up but isn't providing any channels." + "en": { + "stringUnit": { + "state": "translated", + "value": "The tuner is set up but isn't providing any channels." } } } }, - "liveTV.noChannels.title" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Keine Sender" + "liveTV.noChannels.title": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Keine Sender" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "No channels" + "en": { + "stringUnit": { + "state": "translated", + "value": "No channels" } } } }, - "liveTV.noProgram" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Kein Programm verfügbar" + "liveTV.noProgram": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Kein Programm verfügbar" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "No guide data" + "en": { + "stringUnit": { + "state": "translated", + "value": "No guide data" } } } }, - "liveTV.noStream" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Der Server hat keine Adresse für diesen Sender geliefert." + "liveTV.noStream": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Der Server hat keine Adresse für diesen Sender geliefert." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "The server did not return a stream for this channel." + "en": { + "stringUnit": { + "state": "translated", + "value": "The server did not return a stream for this channel." } } } }, - "liveTV.notConfigured.description" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Live-TV braucht eine Fernsehkarte oder einen Netzwerk-Tuner am Plex-Server. Richte ihn in den Servereinstellungen ein, dann erscheinen die Sender hier." + "liveTV.notConfigured.description": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Live-TV braucht eine Fernsehkarte oder einen Netzwerk-Tuner am Plex-Server. Richte ihn in den Servereinstellungen ein, dann erscheinen die Sender hier." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Live TV needs a tuner card or network tuner on your Plex server. Set one up in the server settings and channels will appear here." + "en": { + "stringUnit": { + "state": "translated", + "value": "Live TV needs a tuner card or network tuner on your Plex server. Set one up in the server settings and channels will appear here." } } } }, - "liveTV.notConfigured.title" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Kein Empfänger eingerichtet" + "liveTV.notConfigured.title": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Kein Empfänger eingerichtet" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "No tuner set up" + "en": { + "stringUnit": { + "state": "translated", + "value": "No tuner set up" } } } }, - "liveTV.now" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Jetzt" + "liveTV.now": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Jetzt" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Now" + "en": { + "stringUnit": { + "state": "translated", + "value": "Now" } } } }, - "liveTV.record" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Aufnehmen" + "liveTV.record": { + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Aufnehmen" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Record" + "en": { + "stringUnit": { + "state": "translated", + "value": "Record" } } } }, - "liveTV.recording.inProgress" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nimmt auf" + "liveTV.recordSeries": { + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Serie aufnehmen" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Recording" + "en": { + "stringUnit": { + "state": "translated", + "value": "Record series" } } } }, - "liveTV.recordings" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Aufnahmen" + "liveTV.recording.inProgress": { + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Nimmt auf" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Recordings" + "en": { + "stringUnit": { + "state": "translated", + "value": "Recording" } } } }, - "liveTV.recordings.empty.description" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Im Programm eine Sendung antippen und „Aufnehmen“ wählen — sie steht dann hier." + "liveTV.recordings": { + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Aufnahmen" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tap a programme in the guide and choose “Record” — it will show up here." + "en": { + "stringUnit": { + "state": "translated", + "value": "Recordings" } } } }, - "liveTV.recordings.empty.title" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Noch nichts aufgenommen" + "liveTV.recordings.empty.description": { + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Im Programm eine Sendung antippen und „Aufnehmen“ wählen — sie steht dann hier." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nothing recorded yet" + "en": { + "stringUnit": { + "state": "translated", + "value": "Tap a programme in the guide and choose “Record” — it will show up here." } } } }, - "liveTV.recordSeries" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Serie aufnehmen" + "liveTV.recordings.empty.title": { + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Noch nichts aufgenommen" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Record series" + "en": { + "stringUnit": { + "state": "translated", + "value": "Nothing recorded yet" } } } }, - "liveTV.runningNow" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Läuft gerade" + "liveTV.runningNow": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Läuft gerade" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "On now" + "en": { + "stringUnit": { + "state": "translated", + "value": "On now" } } } }, - "liveTV.scheduled" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Geplant" + "liveTV.scheduled": { + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Geplant" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Scheduled" + "en": { + "stringUnit": { + "state": "translated", + "value": "Scheduled" } } } }, - "liveTV.selection" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sender auswählen" + "liveTV.selection": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Sender auswählen" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Choose channels" + "en": { + "stringUnit": { + "state": "translated", + "value": "Choose channels" } } } }, - "liveTV.selection.all" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Alle" + "liveTV.selection.all": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Alle" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "All" + "en": { + "stringUnit": { + "state": "translated", + "value": "All" } } } }, - "liveTV.selection.hint" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ohne Auswahl werden alle Sender gezeigt." + "liveTV.selection.hint": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Ohne Auswahl werden alle Sender gezeigt." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "With nothing selected, all channels are shown." + "en": { + "stringUnit": { + "state": "translated", + "value": "With nothing selected, all channels are shown." } } } }, - "liveTV.series" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ganze Reihe" + "liveTV.series": { + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Ganze Reihe" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Whole series" + "en": { + "stringUnit": { + "state": "translated", + "value": "Whole series" } } } }, - "liveTV.timeRange %@ %@" : { - "comment" : "Platzhalter: Anfangs- und Endzeit", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "%@ – %@" + "liveTV.timeRange %@ %@": { + "comment": "Platzhalter: Anfangs- und Endzeit", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "%@ – %@" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "%@ – %@" + "en": { + "stringUnit": { + "state": "translated", + "value": "%@ – %@" } } } }, - "liveTV.tune" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Einschalten" + "liveTV.tune": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Einschalten" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Watch" + "en": { + "stringUnit": { + "state": "translated", + "value": "Watch" } } } }, - "liveTV.tuneFailed" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sender lässt sich nicht einschalten" + "liveTV.tuneFailed": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Sender lässt sich nicht einschalten" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Cannot tune channel" + "en": { + "stringUnit": { + "state": "translated", + "value": "Cannot tune channel" } } } }, - "miniPlayer.close" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Wiedergabe beenden" + "miniPlayer.close": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Wiedergabe beenden" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Stop playback" + "en": { + "stringUnit": { + "state": "translated", + "value": "Stop playback" } } } }, - "miniPlayer.expand" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Auf Vollbild vergrößern" + "miniPlayer.expand": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Auf Vollbild vergrößern" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Expand to full screen" + "en": { + "stringUnit": { + "state": "translated", + "value": "Expand to full screen" } } } }, - "miniPlayer.resize" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Größe ändern" + "miniPlayer.resize": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Größe ändern" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Resize" + "en": { + "stringUnit": { + "state": "translated", + "value": "Resize" } } } }, - "placeholder.description" : { - "comment" : "Platzhalter-Bildschirme", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Dieser Bereich entsteht in einer späteren Phase." + "placeholder.description": { + "comment": "Platzhalter-Bildschirme", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Dieser Bereich entsteht in einer späteren Phase." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "This area is coming in a later phase." + "en": { + "stringUnit": { + "state": "translated", + "value": "This area is coming in a later phase." } } } }, - "player.airPlay" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Auf Gerät übertragen" + "player.airPlay": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Auf Gerät übertragen" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Play on another device" + "en": { + "stringUnit": { + "state": "translated", + "value": "Play on another device" } } } }, - "player.audioTracks" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tonspur" + "player.audioTracks": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Tonspur" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Audio" + "en": { + "stringUnit": { + "state": "translated", + "value": "Audio" } } } }, - "player.back10" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "10 Sekunden zurück" + "player.back10": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "10 Sekunden zurück" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Back 10 seconds" + "en": { + "stringUnit": { + "state": "translated", + "value": "Back 10 seconds" } } } }, - "player.buffering" : { - "comment" : "Sprachausgabe für den Abspielknopf, solange auf Daten gewartet wird", - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Lädt …" + "player.buffering": { + "comment": "Sprachausgabe für den Abspielknopf, solange auf Daten gewartet wird", + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Lädt …" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loading…" + "en": { + "stringUnit": { + "state": "translated", + "value": "Loading…" } } } }, - "player.close" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Player schließen" + "player.close": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Player schließen" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Close player" + "en": { + "stringUnit": { + "state": "translated", + "value": "Close player" } } } }, - "player.exitFullScreen" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vollbild beenden" + "player.exitFullScreen": { + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Vollbild beenden" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Exit full screen" + "en": { + "stringUnit": { + "state": "translated", + "value": "Exit full screen" } } } }, - "player.fillVideo" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bild füllen" + "player.fillVideo": { + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Bild füllen" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Fill screen" + "en": { + "stringUnit": { + "state": "translated", + "value": "Fill screen" } } } }, - "player.fitVideo" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bild einpassen" + "player.fitVideo": { + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Bild einpassen" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Fit to screen" + "en": { + "stringUnit": { + "state": "translated", + "value": "Fit to screen" } } } }, - "player.forgetRememberedTrack" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nicht mehr merken" + "player.forgetRememberedTrack": { + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Nicht mehr merken" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Stop remembering" + "en": { + "stringUnit": { + "state": "translated", + "value": "Stop remembering" } } } }, - "player.forward10" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "10 Sekunden vor" + "player.forward10": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "10 Sekunden vor" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Forward 10 seconds" + "en": { + "stringUnit": { + "state": "translated", + "value": "Forward 10 seconds" } } } }, - "player.fullScreen" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vollbild" + "player.fullScreen": { + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Vollbild" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Enter full screen" + "en": { + "stringUnit": { + "state": "translated", + "value": "Enter full screen" } } } }, - "player.jellyfinPending" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Die Wiedergabe von Jellyfin ist noch nicht fertig — sie ist der nächste Schritt." + "player.jellyfinPending": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Die Wiedergabe von Jellyfin ist noch nicht fertig — sie ist der nächste Schritt." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Playback from Jellyfin isn't finished yet — it's the next step." + "en": { + "stringUnit": { + "state": "translated", + "value": "Playback from Jellyfin isn't finished yet — it's the next step." } } } }, - "player.loadFailed" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Die Datei ließ sich nicht öffnen." + "player.loadFailed": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Die Datei ließ sich nicht öffnen." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "The file couldn't be opened." + "en": { + "stringUnit": { + "state": "translated", + "value": "The file couldn't be opened." } } } }, - "player.minimize" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Verkleinern" + "player.minimize": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Verkleinern" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Minimize" + "en": { + "stringUnit": { + "state": "translated", + "value": "Minimize" } } } }, - "player.nextEpisode" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nächste Folge" + "player.nextEpisode": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Nächste Folge" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Next episode" + "en": { + "stringUnit": { + "state": "translated", + "value": "Next episode" } } } }, - "player.noPlayableFile" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Zu diesem Titel gibt es keine abspielbare Datei auf dem Server." + "player.noPlayableFile": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Zu diesem Titel gibt es keine abspielbare Datei auf dem Server." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "There's no playable file for this title on the server." + "en": { + "stringUnit": { + "state": "translated", + "value": "There's no playable file for this title on the server." } } } }, - "player.pause" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Pause" + "player.pause": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Pause" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Pause" + "en": { + "stringUnit": { + "state": "translated", + "value": "Pause" } } } }, - "player.play" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Abspielen" + "player.play": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Abspielen" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Play" + "en": { + "stringUnit": { + "state": "translated", + "value": "Play" } } } }, - "player.playbackFailed" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Die Wiedergabe wurde unterbrochen." + "player.playbackFailed": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Die Wiedergabe wurde unterbrochen." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Playback was interrupted." + "en": { + "stringUnit": { + "state": "translated", + "value": "Playback was interrupted." } } } }, - "player.position" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Position" + "player.position": { + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Position" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Position" + "en": { + "stringUnit": { + "state": "translated", + "value": "Position" } } } }, - "player.preparing" : { - "comment" : "Hinweis, während der Player die Datei öffnet", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Wird vorbereitet …" + "player.preparing": { + "comment": "Hinweis, während der Player die Datei öffnet", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Wird vorbereitet …" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Preparing…" + "en": { + "stringUnit": { + "state": "translated", + "value": "Preparing…" } } } }, - "player.quality" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Qualität" + "player.quality": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Qualität" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Quality" + "en": { + "stringUnit": { + "state": "translated", + "value": "Quality" } } } }, - "player.rebuilding" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Stream wird neu aufgebaut …" + "player.rebuilding": { + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Stream wird neu aufgebaut …" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Rebuilding stream …" + "en": { + "stringUnit": { + "state": "translated", + "value": "Rebuilding stream …" } } } }, - "player.rebuilds" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Stream wird neu aufgebaut" + "player.rebuilds": { + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Stream wird neu aufgebaut" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Stream will be rebuilt" + "en": { + "stringUnit": { + "state": "translated", + "value": "Stream will be rebuilt" } } } }, - "player.relayWarning" : { - "comment" : "Warnung im Player, wenn die Verbindung über die Plex-Vermittlung läuft", - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Die Verbindung läuft über die Vermittlung von Plex und ist stark gedrosselt. Große Dateien laufen darüber kaum an." + "player.relayWarning": { + "comment": "Warnung im Player, wenn die Verbindung über die Plex-Vermittlung läuft", + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Die Verbindung läuft über die Vermittlung von Plex und ist stark gedrosselt. Große Dateien laufen darüber kaum an." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "You're connected through the Plex relay, which is heavily throttled. Large files barely start over it." + "en": { + "stringUnit": { + "state": "translated", + "value": "You're connected through the Plex relay, which is heavily throttled. Large files barely start over it." } } } }, - "player.rememberedTrack" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Für diese Serie gemerkt" + "player.rememberedTrack": { + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Für diese Serie gemerkt" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Remembered for this series" + "en": { + "stringUnit": { + "state": "translated", + "value": "Remembered for this series" } } } }, - "player.rememberedTrackExplanation %@" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Seit du „%@“ gewählt hast, wird diese Spur in jeder Folge genommen." + "player.rememberedTrackExplanation %@": { + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Seit du „%@“ gewählt hast, wird diese Spur in jeder Folge genommen." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Since you picked “%@”, that track is used for every episode." + "en": { + "stringUnit": { + "state": "translated", + "value": "Since you picked “%@”, that track is used for every episode." } } } }, - "player.restoring" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Stelle wird wiederhergestellt" + "player.restoring": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Stelle wird wiederhergestellt" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Returning to your position" + "en": { + "stringUnit": { + "state": "translated", + "value": "Returning to your position" } } } }, - "player.rotate" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Drehen" + "player.rotate": { + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Drehen" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Rotate" + "en": { + "stringUnit": { + "state": "translated", + "value": "Rotate" } } } }, - "player.skipCredits" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Abspann überspringen" + "player.skipCredits": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Abspann überspringen" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Skip credits" + "en": { + "stringUnit": { + "state": "translated", + "value": "Skip credits" } } } }, - "player.skipIntro" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Intro überspringen" + "player.skipIntro": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Intro überspringen" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Skip intro" + "en": { + "stringUnit": { + "state": "translated", + "value": "Skip intro" } } } }, - "player.stalled" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Es kommen keine Daten mehr. Der Stream ist abgerissen." + "player.stalled": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Es kommen keine Daten mehr. Der Stream ist abgerissen." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "No more data is arriving. The stream stopped." + "en": { + "stringUnit": { + "state": "translated", + "value": "No more data is arriving. The stream stopped." } } } }, - "player.stalledUnlimited" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Es kommen keine Daten mehr. Die Qualität steht auf „Original\", der Server überträgt also die Datei unverändert — dafür reicht diese Verbindung nicht. Unter Einstellungen → Wiedergabe eine kleinere Stufe wählen." + "player.stalledUnlimited": { + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Es kommen keine Daten mehr. Die Qualität steht auf „Original\", der Server überträgt also die Datei unverändert — dafür reicht diese Verbindung nicht. Unter Einstellungen → Wiedergabe eine kleinere Stufe wählen." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "No more data is arriving. Quality is set to “Original”, so the server sends the file unchanged — this connection cannot carry that. Pick a smaller step under Settings → Playback." + "en": { + "stringUnit": { + "state": "translated", + "value": "No more data is arriving. Quality is set to “Original”, so the server sends the file unchanged — this connection cannot carry that. Pick a smaller step under Settings → Playback." } } } }, - "player.stop" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Beenden" + "player.stop": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Beenden" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Stop" + "en": { + "stringUnit": { + "state": "translated", + "value": "Stop" } } } }, - "player.subtitlesOff" : { - "comment" : "Eintrag, der die Untertitel abschaltet", - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Aus" + "player.subtitleTracks": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Untertitel" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Off" + "en": { + "stringUnit": { + "state": "translated", + "value": "Subtitles" } } } }, - "player.subtitleTracks" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Untertitel" + "player.subtitlesOff": { + "comment": "Eintrag, der die Untertitel abschaltet", + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Aus" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Subtitles" + "en": { + "stringUnit": { + "state": "translated", + "value": "Off" } } } }, - "player.tracks" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ton und Untertitel" + "player.tracks": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Ton und Untertitel" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Audio and subtitles" + "en": { + "stringUnit": { + "state": "translated", + "value": "Audio and subtitles" } } } }, - "player.transcodeRefused" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Der Server hat die Wiedergabe abgelehnt." + "player.transcodeRefused": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Der Server hat die Wiedergabe abgelehnt." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "The server refused playback." + "en": { + "stringUnit": { + "state": "translated", + "value": "The server refused playback." } } } }, - "quality.audioOnly" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nur Ton" + "quality.audioOnly": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Nur Ton" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Audio only" + "en": { + "stringUnit": { + "state": "translated", + "value": "Audio only" } } } }, - "quality.audioOnly.hint" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Plex kennt für Filme keinen reinen Tonstrom. Diese Stufe überträgt das Bild in der kleinsten Auflösung und blendet es aus." + "quality.audioOnly.hint": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Plex kennt für Filme keinen reinen Tonstrom. Diese Stufe überträgt das Bild in der kleinsten Auflösung und blendet es aus." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Plex has no audio-only stream for video. This setting sends the picture at the smallest size and hides it." + "en": { + "stringUnit": { + "state": "translated", + "value": "Plex has no audio-only stream for video. This setting sends the picture at the smallest size and hides it." } } } }, - "quality.audioShort" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ton" + "quality.audioShort": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Ton" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Audio" + "en": { + "stringUnit": { + "state": "translated", + "value": "Audio" } } } }, - "quality.automatic" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Automatisch" + "quality.autoShort": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Auto" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Automatic" + "en": { + "stringUnit": { + "state": "translated", + "value": "Auto" } } } }, - "quality.autoShort" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Auto" + "quality.automatic": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Automatisch" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Auto" + "en": { + "stringUnit": { + "state": "translated", + "value": "Automatic" } } } }, - "quality.current" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Läuft gerade" + "quality.current": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Läuft gerade" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Currently playing" + "en": { + "stringUnit": { + "state": "translated", + "value": "Currently playing" } } } }, - "quality.current.bitrate" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bitrate" + "quality.current.bitrate": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Bitrate" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bitrate" + "en": { + "stringUnit": { + "state": "translated", + "value": "Bitrate" } } } }, - "quality.current.buffer" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Puffer" + "quality.current.buffer": { + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Puffer" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Buffer" + "en": { + "stringUnit": { + "state": "translated", + "value": "Buffer" } } } }, - "quality.current.dropped" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Verworfene Bilder" + "quality.current.dropped": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Verworfene Bilder" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Dropped frames" + "en": { + "stringUnit": { + "state": "translated", + "value": "Dropped frames" } } } }, - "quality.current.hint" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Gemessen, nicht gewünscht: die Stufe sagt, was erlaubt ist — hier steht, was ankommt." + "quality.current.hint": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Gemessen, nicht gewünscht: die Stufe sagt, was erlaubt ist — hier steht, was ankommt." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Measured, not requested: the setting caps the rate, this is what arrives." + "en": { + "stringUnit": { + "state": "translated", + "value": "Measured, not requested: the setting caps the rate, this is what arrives." } } } }, - "quality.current.noLadder" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Dieser Stream hat nur eine Stufe — es gibt nichts, worauf der Player ausweichen könnte. Bei Rücklern hilft hier nur eine kleinere Qualität von Hand." + "quality.current.noLadder": { + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Dieser Stream hat nur eine Stufe — es gibt nichts, worauf der Player ausweichen könnte. Bei Rücklern hilft hier nur eine kleinere Qualität von Hand." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "This stream has a single variant — there is nothing for the player to fall back to. If it stutters, only a manually chosen lower quality helps." + "en": { + "stringUnit": { + "state": "translated", + "value": "This stream has a single variant — there is nothing for the player to fall back to. If it stutters, only a manually chosen lower quality helps." } } } }, - "quality.current.prefetch" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Eigener Vorrat" + "quality.current.prefetch": { + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Eigener Vorrat" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Own prefetch" + "en": { + "stringUnit": { + "state": "translated", + "value": "Own prefetch" } } } }, - "quality.current.resolution" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Auflösung" + "quality.current.resolution": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Auflösung" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Resolution" + "en": { + "stringUnit": { + "state": "translated", + "value": "Resolution" } } } }, - "quality.current.stalls" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Rückler" + "quality.current.stalls": { + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Rückler" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Stalls" + "en": { + "stringUnit": { + "state": "translated", + "value": "Stalls" } } } }, - "quality.current.variants" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Stufen in der Playlist" + "quality.current.variants": { + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Stufen in der Playlist" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Variants in playlist" + "en": { + "stringUnit": { + "state": "translated", + "value": "Variants in playlist" } } } }, - "quality.everyRung" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Alle Stufen" + "quality.everyRung": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Alle Stufen" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "All steps" + "en": { + "stringUnit": { + "state": "translated", + "value": "All steps" } } } }, - "quality.fullHD" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "1080p" + "quality.fullHD": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "1080p" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "1080p" + "en": { + "stringUnit": { + "state": "translated", + "value": "1080p" } } } }, - "quality.hd" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "720p" + "quality.hd": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "720p" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "720p" + "en": { + "stringUnit": { + "state": "translated", + "value": "720p" } } } }, - "quality.high" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Hoch (12 Mbit/s)" + "quality.high": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Hoch (12 Mbit/s)" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "High (12 Mbps)" + "en": { + "stringUnit": { + "state": "translated", + "value": "High (12 Mbps)" } } } }, - "quality.kilobits %lld" : { - "comment" : "Platzhalter: Kilobit je Sekunde", - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lld kbps" + "quality.kilobits %lld": { + "comment": "Platzhalter: Kilobit je Sekunde", + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "%lld kbps" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lld kbps" + "en": { + "stringUnit": { + "state": "translated", + "value": "%lld kbps" } } } }, - "quality.low" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Niedrig (4 Mbit/s)" + "quality.low": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Niedrig (4 Mbit/s)" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Low (4 Mbps)" + "en": { + "stringUnit": { + "state": "translated", + "value": "Low (4 Mbps)" } } } }, - "quality.lowRes" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "480p" + "quality.lowRes": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "480p" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "480p" + "en": { + "stringUnit": { + "state": "translated", + "value": "480p" } } } }, - "quality.medium" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mittel (8 Mbit/s)" + "quality.medium": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Mittel (8 Mbit/s)" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Medium (8 Mbps)" + "en": { + "stringUnit": { + "state": "translated", + "value": "Medium (8 Mbps)" } } } }, - "quality.megabits %@" : { - "comment" : "Platzhalter: Megabit je Sekunde", - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "%@ Mbps" + "quality.megabits %@": { + "comment": "Platzhalter: Megabit je Sekunde", + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "%@ Mbps" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "%@ Mbps" + "en": { + "stringUnit": { + "state": "translated", + "value": "%@ Mbps" } } } }, - "quality.minimal" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sparsam (2 Mbit/s)" + "quality.minimal": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Sparsam (2 Mbit/s)" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Data saver (2 Mbps)" + "en": { + "stringUnit": { + "state": "translated", + "value": "Data saver (2 Mbps)" } } } }, - "quality.original" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Original" + "quality.original": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Original" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Original" + "en": { + "stringUnit": { + "state": "translated", + "value": "Original" } } } }, - "quality.originalShort" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Max" + "quality.originalShort": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Max" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Max" + "en": { + "stringUnit": { + "state": "translated", + "value": "Max" } } } }, - "quality.prefetch.ahead %lld" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lld s voraus" + "quality.prefetch.ahead %lld": { + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "%lld s voraus" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lld s ahead" + "en": { + "stringUnit": { + "state": "translated", + "value": "%lld s ahead" } } } }, - "quality.prefetch.off" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "aus" + "quality.prefetch.off": { + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "aus" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "off" + "en": { + "stringUnit": { + "state": "translated", + "value": "off" } } } }, - "quality.prefetch.starting" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "läuft an" + "quality.prefetch.starting": { + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "läuft an" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "starting" + "en": { + "stringUnit": { + "state": "translated", + "value": "starting" } } } }, - "quality.seconds %@" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "%@ s" + "quality.seconds %@": { + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "%@ s" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "%@ s" + "en": { + "stringUnit": { + "state": "translated", + "value": "%@ s" } } } }, - "quality.stalls %lld %@" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lld · %@ gewartet" + "quality.stalls %lld %@": { + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "%lld · %@ gewartet" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lld · %@ waiting" + "en": { + "stringUnit": { + "state": "translated", + "value": "%lld · %@ waiting" } } } }, - "quickPlay.failed" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Abspielen nicht möglich" + "quickPlay.failed": { + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Abspielen nicht möglich" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Can’t play this" + "en": { + "stringUnit": { + "state": "translated", + "value": "Can’t play this" } } } }, - "quickPlay.noServer" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Keine Verbindung zum Server." + "quickPlay.noServer": { + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Keine Verbindung zum Server." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Not connected to the server." + "en": { + "stringUnit": { + "state": "translated", + "value": "Not connected to the server." } } } }, - "quickPlay.nothingToPlay" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Hier ist keine Folge zu finden, die sich abspielen lässt." + "quickPlay.nothingToPlay": { + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Hier ist keine Folge zu finden, die sich abspielen lässt." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "There’s no episode here that could be played." + "en": { + "stringUnit": { + "state": "translated", + "value": "There’s no episode here that could be played." } } } }, - "runtime.hoursMinutes %lld %lld" : { - "comment" : "Laufzeit mit Stunden", - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lld Std %lld Min" + "runtime.hoursMinutes %lld %lld": { + "comment": "Laufzeit mit Stunden", + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "%lld Std %lld Min" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lldh %lldm" + "en": { + "stringUnit": { + "state": "translated", + "value": "%lldh %lldm" } } } }, - "runtime.minutes %lld" : { - "comment" : "Laufzeit unter einer Stunde", - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lld Min" + "runtime.minutes %lld": { + "comment": "Laufzeit unter einer Stunde", + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "%lld Min" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lldm" + "en": { + "stringUnit": { + "state": "translated", + "value": "%lldm" } } } }, - "SD" : { - "comment" : "A label for a 720p movie.", - "isCommentAutoGenerated" : true - }, - "search.forgetRecent" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Verlauf löschen" + "search.forgetRecent": { + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Verlauf löschen" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Clear history" + "en": { + "stringUnit": { + "state": "translated", + "value": "Clear history" } } } }, - "search.hint" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Suche nach Filmen, Serien, Folgen und Personen." + "search.hint": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Suche nach Filmen, Serien, Folgen und Personen." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Search for movies, shows, episodes and people." + "en": { + "stringUnit": { + "state": "translated", + "value": "Search for movies, shows, episodes and people." } } } }, - "search.noResults" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nichts gefunden." + "search.noResults": { + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Nichts gefunden." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nothing found." + "en": { + "stringUnit": { + "state": "translated", + "value": "Nothing found." } } } }, - "search.recent" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Zuletzt gesucht" + "search.recent": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Zuletzt gesucht" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Recent searches" + "en": { + "stringUnit": { + "state": "translated", + "value": "Recent searches" } } } }, - "search.title" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Suche" + "search.title": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Suche" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Search" + "en": { + "stringUnit": { + "state": "translated", + "value": "Search" } } } }, - "season.number %lld" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Staffel %lld" + "season.number %lld": { + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Staffel %lld" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Season %lld" + "en": { + "stringUnit": { + "state": "translated", + "value": "Season %lld" } } } }, - "season.short %lld" : { - "comment" : "Platzhalter: Staffelnummer", - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Staffel %lld" + "season.short %lld": { + "comment": "Platzhalter: Staffelnummer", + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Staffel %lld" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Season %lld" + "en": { + "stringUnit": { + "state": "translated", + "value": "Season %lld" } } } }, - "serverPicker.owned" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Eigener Server" + "serverPicker.owned": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Eigener Server" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Your server" + "en": { + "stringUnit": { + "state": "translated", + "value": "Your server" } } } }, - "serverPicker.shared" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Geteilt" + "serverPicker.shared": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Geteilt" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Shared" + "en": { + "stringUnit": { + "state": "translated", + "value": "Shared" } } } }, - "serverPicker.title" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Server wählen" + "serverPicker.title": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Server wählen" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Choose a server" + "en": { + "stringUnit": { + "state": "translated", + "value": "Choose a server" } } } }, - "settings.account" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Konto" + "settings.account": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Konto" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Account" + "en": { + "stringUnit": { + "state": "translated", + "value": "Account" } } } }, - "settings.address" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Adresse" + "settings.address": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Adresse" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Address" + "en": { + "stringUnit": { + "state": "translated", + "value": "Address" } } } }, - "settings.app" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "App" + "settings.app": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "App" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "App" + "en": { + "stringUnit": { + "state": "translated", + "value": "App" } } } }, - "settings.autoPlayNext" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nächste Folge automatisch" + "settings.autoPlayNext": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Nächste Folge automatisch" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Play next episode automatically" + "en": { + "stringUnit": { + "state": "translated", + "value": "Play next episode automatically" } } } }, - "settings.autoSkipCredits" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Abspann automatisch überspringen" + "settings.autoSkipCredits": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Abspann automatisch überspringen" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Skip credits automatically" + "en": { + "stringUnit": { + "state": "translated", + "value": "Skip credits automatically" } } } }, - "settings.autoSkipIntro" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Intro automatisch überspringen" + "settings.autoSkipIntro": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Intro automatisch überspringen" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Skip intros automatically" + "en": { + "stringUnit": { + "state": "translated", + "value": "Skip intros automatically" } } } }, - "settings.build" : { - "comment" : "Zeitpunkt, zu dem der laufende Build entstanden ist", - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Build" + "settings.build": { + "comment": "Zeitpunkt, zu dem der laufende Build entstanden ist", + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Build" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Build" + "en": { + "stringUnit": { + "state": "translated", + "value": "Build" } } } }, - "settings.concurrent" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Höchstens gleichzeitig" + "settings.concurrent": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Höchstens gleichzeitig" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "At most at once" + "en": { + "stringUnit": { + "state": "translated", + "value": "At most at once" } } } }, - "settings.concurrentExplanation" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Eine ganze Staffel auf einmal zu laden teilt die Leitung so auf, dass keine Folge fertig wird. Unterwegs ist eine fertige Folge mehr wert als zehn halbe." + "settings.concurrentExplanation": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Eine ganze Staffel auf einmal zu laden teilt die Leitung so auf, dass keine Folge fertig wird. Unterwegs ist eine fertige Folge mehr wert als zehn halbe." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loading a whole season at once splits the line so that no episode finishes. On the move, one finished episode beats ten half ones." + "en": { + "stringUnit": { + "state": "translated", + "value": "Loading a whole season at once splits the line so that no episode finishes. On the move, one finished episode beats ten half ones." } } } }, - "settings.concurrentHeader" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Gleichzeitige Downloads" + "settings.concurrentHeader": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Gleichzeitige Downloads" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Concurrent downloads" + "en": { + "stringUnit": { + "state": "translated", + "value": "Concurrent downloads" } } } }, - "settings.connection" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Verbindung" + "settings.connection": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Verbindung" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Connection" + "en": { + "stringUnit": { + "state": "translated", + "value": "Connection" } } } }, - "settings.connectionLocal" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Lokal" + "settings.connectionLocal": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Lokal" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Local" + "en": { + "stringUnit": { + "state": "translated", + "value": "Local" } } } }, - "settings.connectionRelay" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Über Plex-Vermittlung (gedrosselt)" + "settings.connectionRelay": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Über Plex-Vermittlung (gedrosselt)" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Via Plex relay (throttled)" + "en": { + "stringUnit": { + "state": "translated", + "value": "Via Plex relay (throttled)" } } } }, - "settings.connectionRemote" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Direkt von außen" + "settings.connectionRemote": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Direkt von außen" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Direct remote" + "en": { + "stringUnit": { + "state": "translated", + "value": "Direct remote" } } } }, - "settings.controls" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Steuerung" + "settings.controls": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Steuerung" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Controls" + "en": { + "stringUnit": { + "state": "translated", + "value": "Controls" } } } }, - "settings.controlsExplanation" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Zurück und vor lassen sich getrennt einstellen: zurück springt man, weil man etwas nicht verstanden hat — vor, weil man etwas überspringen will." + "settings.controlsExplanation": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Zurück und vor lassen sich getrennt einstellen: zurück springt man, weil man etwas nicht verstanden hat — vor, weil man etwas überspringen will." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Back and forward are set separately: you jump back because you missed something, and forward because you want to skip something." + "en": { + "stringUnit": { + "state": "translated", + "value": "Back and forward are set separately: you jump back because you missed something, and forward because you want to skip something." } } } }, - "settings.defaultRate" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Startgeschwindigkeit" + "settings.defaultRate": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Startgeschwindigkeit" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Starting speed" + "en": { + "stringUnit": { + "state": "translated", + "value": "Starting speed" } } } }, - "settings.deleteAfterDay" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nach einem Tag" + "settings.deleteAfterDay": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Nach einem Tag" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "After a day" + "en": { + "stringUnit": { + "state": "translated", + "value": "After a day" } } } }, - "settings.deleteAfterWeek" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nach einer Woche" + "settings.deleteAfterWeek": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Nach einer Woche" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "After a week" + "en": { + "stringUnit": { + "state": "translated", + "value": "After a week" } } } }, - "settings.deleteImmediately" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sofort" + "settings.deleteImmediately": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Sofort" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Immediately" + "en": { + "stringUnit": { + "state": "translated", + "value": "Immediately" } } } }, - "settings.deleteNever" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nie" + "settings.deleteNever": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Nie" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Never" + "en": { + "stringUnit": { + "state": "translated", + "value": "Never" } } } }, - "settings.deleteWatched" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Gesehenes löschen" + "settings.deleteWatched": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Gesehenes löschen" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Delete watched" + "en": { + "stringUnit": { + "state": "translated", + "value": "Delete watched" } } } }, - "settings.downloadsExplanation" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Fehlende Folgen werden nachgeladen, sobald du eine gesehen hast. Gesehenes wird nach der eingestellten Frist entfernt." + "settings.downloadsExplanation": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Fehlende Folgen werden nachgeladen, sobald du eine gesehen hast. Gesehenes wird nach der eingestellten Frist entfernt." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Missing episodes are downloaded once you finish one. Watched titles are removed after the chosen delay." + "en": { + "stringUnit": { + "state": "translated", + "value": "Missing episodes are downloaded once you finish one. Watched titles are removed after the chosen delay." } } } }, - "settings.downloadsHeaderExplanation" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Wie viel Platz Offline-Titel belegen dürfen, wie viele gleichzeitig laden und wann sie wieder verschwinden." + "settings.downloadsHeaderExplanation": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Wie viel Platz Offline-Titel belegen dürfen, wie viele gleichzeitig laden und wann sie wieder verschwinden." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "How much space offline titles may take, how many load at once, and when they disappear again." + "en": { + "stringUnit": { + "state": "translated", + "value": "How much space offline titles may take, how many load at once, and when they disappear again." } } } }, - "settings.episodes" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Folgen" + "settings.episodes": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Folgen" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Episodes" + "en": { + "stringUnit": { + "state": "translated", + "value": "Episodes" } } } }, - "settings.forward" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vorspulen" + "settings.forward": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Vorspulen" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Fast-forward" + "en": { + "stringUnit": { + "state": "translated", + "value": "Fast-forward" } } } }, - "settings.keepCount %@" : { - "comment" : "A label that lets the user choose how many episodes of a series to keep after they've finished watching them all. The argument is the number of episodes to keep.", - "isCommentAutoGenerated" : true + "settings.keepCount %@": { + "comment": "A label that lets the user choose how many episodes of a series to keep after they've finished watching them all. The argument is the number of episodes to keep.", + "isCommentAutoGenerated": true }, - "settings.keepCount %lld" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lld ungesehene" + "settings.keepCount %lld": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "%lld ungesehene" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lld unwatched" + "en": { + "stringUnit": { + "state": "translated", + "value": "%lld unwatched" } } } }, - "settings.keepOff" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Aus" + "settings.keepOff": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Aus" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Off" + "en": { + "stringUnit": { + "state": "translated", + "value": "Off" } } } }, - "settings.keepUnwatched" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Folgen vorhalten" + "settings.keepUnwatched": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Folgen vorhalten" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Keep episodes ready" + "en": { + "stringUnit": { + "state": "translated", + "value": "Keep episodes ready" } } } }, - "settings.libraries" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mediatheken" + "settings.libraries": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Mediatheken" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Libraries" + "en": { + "stringUnit": { + "state": "translated", + "value": "Libraries" } } } }, - "settings.lockLandscape" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Im Querformat sperren" + "settings.lockLandscape": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Im Querformat sperren" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Lock to landscape" + "en": { + "stringUnit": { + "state": "translated", + "value": "Lock to landscape" } } } }, - "settings.lockLandscapeExplanation" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Der Player dreht beim Öffnen ins Querformat und bleibt dort. Abgeschaltet folgt er dem Gerät — für den, der im Liegen schaut." + "settings.lockLandscapeExplanation": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Der Player dreht beim Öffnen ins Querformat und bleibt dort. Abgeschaltet folgt er dem Gerät — für den, der im Liegen schaut." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "The player rotates to landscape when it opens and stays there. Switched off it follows the device — for watching while lying down." + "en": { + "stringUnit": { + "state": "translated", + "value": "The player rotates to landscape when it opens and stays there. Switched off it follows the device — for watching while lying down." } } } }, - "settings.playback" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Wiedergabe" + "settings.playback": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Wiedergabe" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Playback" + "en": { + "stringUnit": { + "state": "translated", + "value": "Playback" } } } }, - "settings.playbackExplanation" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Wie gut das Bild sein soll, wie weit der Doppeltipp springt und was am Ende einer Folge passiert." + "settings.playbackExplanation": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Wie gut das Bild sein soll, wie weit der Doppeltipp springt und was am Ende einer Folge passiert." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "How good the picture should be, how far a double tap jumps, and what happens at the end of an episode." + "en": { + "stringUnit": { + "state": "translated", + "value": "How good the picture should be, how far a double tap jumps, and what happens at the end of an episode." } } } }, - "settings.quality" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Qualität" + "settings.quality": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Qualität" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Quality" + "en": { + "stringUnit": { + "state": "translated", + "value": "Quality" } } } }, - "settings.qualityCellular" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Über Mobilfunk" + "settings.qualityCellular": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Über Mobilfunk" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "On cellular" + "en": { + "stringUnit": { + "state": "translated", + "value": "On cellular" } } } }, - "settings.qualityExplanation" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Die Obergrenze, nicht das Ziel. Im eigenen Netz überträgt der Server das Original; unterwegs beginnt Flixy kleiner und tastet sich hinauf, solange die Verbindung es trägt — höchstens bis zu dieser Stufe. Auflösung und Bitrate gehen dabei zusammen." + "settings.qualityExplanation": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Die Obergrenze, nicht das Ziel. Im eigenen Netz überträgt der Server das Original; unterwegs beginnt Flixy kleiner und tastet sich hinauf, solange die Verbindung es trägt — höchstens bis zu dieser Stufe. Auflösung und Bitrate gehen dabei zusammen." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "An upper limit, not a target. On your own network the server sends the original; away from home Flixy starts lower and works its way up as long as the connection holds — never above this step. Resolution and bitrate move together." + "en": { + "stringUnit": { + "state": "translated", + "value": "An upper limit, not a target. On your own network the server sends the original; away from home Flixy starts lower and works its way up as long as the connection holds — never above this step. Resolution and bitrate move together." } } } }, - "settings.qualityUnlimitedHint" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Zuletzt hat die Wiedergabe gehakt. Ohne Grenze überträgt der Server das Original — gibt die Leitung das nicht her, kann nichts ausweichen. Eine Stufe darunter läuft dann ruhiger." + "settings.qualityUnlimitedHint": { + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Zuletzt hat die Wiedergabe gehakt. Ohne Grenze überträgt der Server das Original — gibt die Leitung das nicht her, kann nichts ausweichen. Eine Stufe darunter läuft dann ruhiger." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Playback stalled recently. Without a limit the server sends the original — if the connection cannot carry it, there is nothing to fall back to. One step down plays more smoothly." + "en": { + "stringUnit": { + "state": "translated", + "value": "Playback stalled recently. Without a limit the server sends the original — if the connection cannot carry it, there is nothing to fall back to. One step down plays more smoothly." } } } }, - "settings.qualityWifi" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Im WLAN" + "settings.qualityWifi": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Im WLAN" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "On Wi-Fi" + "en": { + "stringUnit": { + "state": "translated", + "value": "On Wi-Fi" } } } }, - "settings.rateNormal" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Normal" + "settings.rateNormal": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Normal" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Normal" + "en": { + "stringUnit": { + "state": "translated", + "value": "Normal" } } } }, - "settings.rememberQuality" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Gewählte Qualität merken" + "settings.rememberQuality": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Gewählte Qualität merken" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Remember the chosen quality" + "en": { + "stringUnit": { + "state": "translated", + "value": "Remember the chosen quality" } } } }, - "settings.rememberQualityExplanation" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Die im Player gewählte Stufe gilt dann auch für den nächsten Titel. Gibt es sie dort nicht, wird die nächstgelegene genommen." + "settings.rememberQualityExplanation": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Die im Player gewählte Stufe gilt dann auch für den nächsten Titel. Gibt es sie dort nicht, wird die nächstgelegene genommen." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "The step you pick in the player then applies to the next title as well. If it isn't available there, the closest one is used." + "en": { + "stringUnit": { + "state": "translated", + "value": "The step you pick in the player then applies to the next title as well. If it isn't available there, the closest one is used." } } } }, - "settings.rewind" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Zurückspulen" + "settings.rewind": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Zurückspulen" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Rewind" + "en": { + "stringUnit": { + "state": "translated", + "value": "Rewind" } } } }, - "settings.scanExplanation" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Der Suchlauf durchsucht die Ordner auf dem Server nach neuen Dateien. Das kann einige Minuten dauern." + "settings.scanExplanation": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Der Suchlauf durchsucht die Ordner auf dem Server nach neuen Dateien. Das kann einige Minuten dauern." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "A scan looks through the folders on the server for new files. It can take a few minutes." + "en": { + "stringUnit": { + "state": "translated", + "value": "A scan looks through the folders on the server for new files. It can take a few minutes." } } } }, - "settings.seconds %@" : { - "comment" : "Ein Picker, das die Anzahl der Sekunden zeigt, die man zurückgehen kann.", - "isCommentAutoGenerated" : true + "settings.seconds %@": { + "comment": "Ein Picker, das die Anzahl der Sekunden zeigt, die man zurückgehen kann.", + "isCommentAutoGenerated": true }, - "settings.seconds %lld" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lld Sekunden" + "settings.seconds %lld": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "%lld Sekunden" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lld seconds" + "en": { + "stringUnit": { + "state": "translated", + "value": "%lld seconds" } } } }, - "settings.section.look" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Darstellung" + "settings.section.look": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Darstellung" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Appearance" + "en": { + "stringUnit": { + "state": "translated", + "value": "Appearance" } } } }, - "settings.section.playing" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Wiedergabe & Offline" + "settings.section.playing": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Wiedergabe & Offline" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Playback & offline" + "en": { + "stringUnit": { + "state": "translated", + "value": "Playback & offline" } } } }, - "settings.server" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Server" + "settings.server": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Server" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Server" + "en": { + "stringUnit": { + "state": "translated", + "value": "Server" } } } }, - "settings.serverKind" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Art" + "settings.serverKind": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Art" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Type" + "en": { + "stringUnit": { + "state": "translated", + "value": "Type" } } } }, - "settings.serverName" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Name" + "settings.serverName": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Name" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Name" + "en": { + "stringUnit": { + "state": "translated", + "value": "Name" } } } }, - "settings.showBitrate" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bitrate im Qualitätsmenü zeigen" + "settings.showBitrate": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Bitrate im Qualitätsmenü zeigen" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Show the bitrate in the quality menu" + "en": { + "stringUnit": { + "state": "translated", + "value": "Show the bitrate in the quality menu" } } } }, - "settings.signOutConfirm" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Wirklich abmelden?" + "settings.signOutConfirm": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Wirklich abmelden?" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sign out?" + "en": { + "stringUnit": { + "state": "translated", + "value": "Sign out?" } } } }, - "settings.signOutExplanation" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Das Token wird vom Gerät entfernt. Heruntergeladene Titel bleiben erhalten." + "settings.signOutExplanation": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Das Token wird vom Gerät entfernt. Heruntergeladene Titel bleiben erhalten." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "The token will be removed from this device. Downloaded titles are kept." + "en": { + "stringUnit": { + "state": "translated", + "value": "The token will be removed from this device. Downloaded titles are kept." } } } }, - "settings.storageLimit" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Speicherlimit" + "settings.storageLimit": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Speicherlimit" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Storage limit" + "en": { + "stringUnit": { + "state": "translated", + "value": "Storage limit" } } } }, - "settings.storageUnlimited" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Unbegrenzt" + "settings.storageUnlimited": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Unbegrenzt" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Unlimited" + "en": { + "stringUnit": { + "state": "translated", + "value": "Unlimited" } } } }, - "settings.version" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Version" + "settings.version": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Version" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Version" + "en": { + "stringUnit": { + "state": "translated", + "value": "Version" } } } }, - "sidebar.close" : { - "comment" : "Barrierefreiheit: abgedunkelte Fläche", - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Seitenleiste schließen" + "sidebar.close": { + "comment": "Barrierefreiheit: abgedunkelte Fläche", + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Seitenleiste schließen" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Close sidebar" + "en": { + "stringUnit": { + "state": "translated", + "value": "Close sidebar" } } } }, - "sidebar.downloads" : { - "comment" : "Eintrag in der Seitenleiste", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Downloads" + "sidebar.downloads": { + "comment": "Eintrag in der Seitenleiste", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Downloads" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Downloads" + "en": { + "stringUnit": { + "state": "translated", + "value": "Downloads" } } } }, - "sidebar.edit" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bearbeiten" + "sidebar.edit": { + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Bearbeiten" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Edit" + "en": { + "stringUnit": { + "state": "translated", + "value": "Edit" } } } }, - "sidebar.editHint" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ziehen zum Umsortieren, Haken zum Ein- und Ausblenden." + "sidebar.editHint": { + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Ziehen zum Umsortieren, Haken zum Ein- und Ausblenden." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Drag to reorder, tap the check to show or hide." + "en": { + "stringUnit": { + "state": "translated", + "value": "Drag to reorder, tap the check to show or hide." } } } }, - "sidebar.hide" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ausblenden" + "sidebar.hide": { + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Ausblenden" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Hide" + "en": { + "stringUnit": { + "state": "translated", + "value": "Hide" } } } }, - "sidebar.home" : { - "comment" : "Eintrag der Startseite in der Seitenleiste", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Start" + "sidebar.home": { + "comment": "Eintrag der Startseite in der Seitenleiste", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Start" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Home" + "en": { + "stringUnit": { + "state": "translated", + "value": "Home" } } } }, - "sidebar.libraries" : { - "comment" : "Überschrift über der Liste der Mediatheken", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mediatheken" + "sidebar.libraries": { + "comment": "Überschrift über der Liste der Mediatheken", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Mediatheken" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Libraries" + "en": { + "stringUnit": { + "state": "translated", + "value": "Libraries" } } } }, - "sidebar.library" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bibliothek" + "sidebar.library": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Bibliothek" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Library" + "en": { + "stringUnit": { + "state": "translated", + "value": "Library" } } } }, - "sidebar.liveTV" : { - "comment" : "Eintrag in der Seitenleiste", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Live-TV" + "sidebar.liveTV": { + "comment": "Eintrag in der Seitenleiste", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Live-TV" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Live TV" + "en": { + "stringUnit": { + "state": "translated", + "value": "Live TV" } } } }, - "sidebar.more" : { - "comment" : "Überschrift über Live-TV, Downloads und Einstellungen", - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mehr" + "sidebar.more": { + "comment": "Überschrift über Live-TV, Downloads und Einstellungen", + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Mehr" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "More" + "en": { + "stringUnit": { + "state": "translated", + "value": "More" } } } }, - "sidebar.moveToSide" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Navigation zur Seite" + "sidebar.moveToSide": { + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Navigation zur Seite" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Move navigation to side" + "en": { + "stringUnit": { + "state": "translated", + "value": "Move navigation to side" } } } }, - "sidebar.moveToTop" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Navigation nach oben" + "sidebar.moveToTop": { + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Navigation nach oben" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Move navigation to top" + "en": { + "stringUnit": { + "state": "translated", + "value": "Move navigation to top" } } } }, - "sidebar.open" : { - "comment" : "Barrierefreiheit: Hamburger-Knopf", - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Seitenleiste öffnen" + "sidebar.open": { + "comment": "Barrierefreiheit: Hamburger-Knopf", + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Seitenleiste öffnen" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Open sidebar" + "en": { + "stringUnit": { + "state": "translated", + "value": "Open sidebar" } } } }, - "sidebar.search" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Suchen" + "sidebar.search": { + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Suchen" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Search" + "en": { + "stringUnit": { + "state": "translated", + "value": "Search" } } } }, - "sidebar.settings" : { - "comment" : "Eintrag in der Seitenleiste", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Einstellungen" + "sidebar.settings": { + "comment": "Eintrag in der Seitenleiste", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Einstellungen" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Settings" + "en": { + "stringUnit": { + "state": "translated", + "value": "Settings" } } } }, - "sidebar.show" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Einblenden" + "sidebar.show": { + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Einblenden" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Show" + "en": { + "stringUnit": { + "state": "translated", + "value": "Show" } } } }, - "signIn.cancelled" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Anmeldung abgebrochen." + "signIn.cancelled": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Anmeldung abgebrochen." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sign-in cancelled." + "en": { + "stringUnit": { + "state": "translated", + "value": "Sign-in cancelled." } } } }, - "signIn.jellyfin.addressHint" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Schema und Port kannst du weglassen — die üblichen werden durchprobiert." + "signIn.jellyfin.addressHint": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Schema und Port kannst du weglassen — die üblichen werden durchprobiert." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "You can leave out the scheme and port — the usual ones are tried in turn." + "en": { + "stringUnit": { + "state": "translated", + "value": "You can leave out the scheme and port — the usual ones are tried in turn." } } } }, - "signIn.jellyfin.addressMissing" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Trag bitte die Adresse deines Servers ein." + "signIn.jellyfin.addressMissing": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Trag bitte die Adresse deines Servers ein." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Please enter your server's address." + "en": { + "stringUnit": { + "state": "translated", + "value": "Please enter your server's address." } } } }, - "signIn.jellyfin.addressPlaceholder" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "192.168.1.50 oder medien.example.com" + "signIn.jellyfin.addressPlaceholder": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "192.168.1.50 oder medien.example.com" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "192.168.1.50 or media.example.com" + "en": { + "stringUnit": { + "state": "translated", + "value": "192.168.1.50 or media.example.com" } } } }, - "signIn.jellyfin.addressPrompt" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Wo steht dein Server?" + "signIn.jellyfin.addressPrompt": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Wo steht dein Server?" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Where is your server?" + "en": { + "stringUnit": { + "state": "translated", + "value": "Where is your server?" } } } }, - "signIn.jellyfin.codeExpired" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Der Code ist abgelaufen. Fordere einen neuen an oder melde dich mit Passwort an." + "signIn.jellyfin.codeExpired": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Der Code ist abgelaufen. Fordere einen neuen an oder melde dich mit Passwort an." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "The code has expired. Request a new one or sign in with a password." + "en": { + "stringUnit": { + "state": "translated", + "value": "The code has expired. Request a new one or sign in with a password." } } } }, - "signIn.jellyfin.connect" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Verbinden" + "signIn.jellyfin.connect": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Verbinden" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Connect" + "en": { + "stringUnit": { + "state": "translated", + "value": "Connect" } } } }, - "signIn.jellyfin.foundServer" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Gefundener Server" + "signIn.jellyfin.foundServer": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Gefundener Server" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Server found" + "en": { + "stringUnit": { + "state": "translated", + "value": "Server found" } } } }, - "signIn.jellyfin.notFound" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Unter dieser Adresse antwortet kein Jellyfin-Server." + "signIn.jellyfin.notFound": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Unter dieser Adresse antwortet kein Jellyfin-Server." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "No Jellyfin server answers at that address." + "en": { + "stringUnit": { + "state": "translated", + "value": "No Jellyfin server answers at that address." } } } }, - "signIn.jellyfin.otherAddress" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Andere Adresse eintragen" + "signIn.jellyfin.otherAddress": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Andere Adresse eintragen" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Enter a different address" + "en": { + "stringUnit": { + "state": "translated", + "value": "Enter a different address" } } } }, - "signIn.jellyfin.password" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Passwort" + "signIn.jellyfin.password": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Passwort" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Password" + "en": { + "stringUnit": { + "state": "translated", + "value": "Password" } } } }, - "signIn.jellyfin.passwordNote" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Das Passwort wird nur für diese eine Anfrage gehalten. Gespeichert wird allein der Zugangsschlüssel, und der im Schlüsselbund." + "signIn.jellyfin.passwordNote": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Das Passwort wird nur für diese eine Anfrage gehalten. Gespeichert wird allein der Zugangsschlüssel, und der im Schlüsselbund." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "The password is held for this one request only. Only the access token is stored, and it goes into the keychain." + "en": { + "stringUnit": { + "state": "translated", + "value": "The password is held for this one request only. Only the access token is stored, and it goes into the keychain." } } } }, - "signIn.jellyfin.privacy" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "So erreicht dein Passwort diese App nie." + "signIn.jellyfin.privacy": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "So erreicht dein Passwort diese App nie." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "This way your password never reaches this app." + "en": { + "stringUnit": { + "state": "translated", + "value": "This way your password never reaches this app." } } } }, - "signIn.jellyfin.quickConnectHint" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Öffne Jellyfin auf einem Gerät, an dem du schon angemeldet bist, und gib diesen Code unter „Benutzer → Quick Connect“ ein." + "signIn.jellyfin.quickConnectHint": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Öffne Jellyfin auf einem Gerät, an dem du schon angemeldet bist, und gib diesen Code unter „Benutzer → Quick Connect“ ein." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Open Jellyfin on a device you're already signed in on and enter this code under “User → Quick Connect”." + "en": { + "stringUnit": { + "state": "translated", + "value": "Open Jellyfin on a device you're already signed in on and enter this code under “User → Quick Connect”." } } } }, - "signIn.jellyfin.searching" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Server wird gesucht …" + "signIn.jellyfin.searching": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Server wird gesucht …" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Looking for the server …" + "en": { + "stringUnit": { + "state": "translated", + "value": "Looking for the server …" } } } }, - "signIn.jellyfin.signingIn" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Anmeldung läuft …" + "signIn.jellyfin.signingIn": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Anmeldung läuft …" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Signing in …" + "en": { + "stringUnit": { + "state": "translated", + "value": "Signing in …" } } } }, - "signIn.jellyfin.submit" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Anmelden" + "signIn.jellyfin.submit": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Anmelden" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sign in" + "en": { + "stringUnit": { + "state": "translated", + "value": "Sign in" } } } }, - "signIn.jellyfin.title" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Jellyfin-Server" + "signIn.jellyfin.title": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Jellyfin-Server" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Jellyfin server" + "en": { + "stringUnit": { + "state": "translated", + "value": "Jellyfin server" } } } }, - "signIn.jellyfin.useCode" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Stattdessen einen Code bestätigen" + "signIn.jellyfin.useCode": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Stattdessen einen Code bestätigen" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Approve a code instead" + "en": { + "stringUnit": { + "state": "translated", + "value": "Approve a code instead" } } } }, - "signIn.jellyfin.usePassword" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Stattdessen Benutzername und Passwort" + "signIn.jellyfin.usePassword": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Stattdessen Benutzername und Passwort" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Use a username and password instead" + "en": { + "stringUnit": { + "state": "translated", + "value": "Use a username and password instead" } } } }, - "signIn.jellyfin.userName" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Benutzername" + "signIn.jellyfin.userName": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Benutzername" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Username" + "en": { + "stringUnit": { + "state": "translated", + "value": "Username" } } } }, - "signIn.jellyfin.waitingForApproval" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Warte auf die Bestätigung …" + "signIn.jellyfin.waitingForApproval": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Warte auf die Bestätigung …" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Waiting for approval …" + "en": { + "stringUnit": { + "state": "translated", + "value": "Waiting for approval …" } } } }, - "signIn.manualHint" : { - "comment" : "Hinweis, wenn die Browser-Verknüpfung nicht greift", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Hat das Fenster nicht verknüpft? Öffne plex.tv/link und gib diesen Code ein:" + "signIn.manualHint": { + "comment": "Hinweis, wenn die Browser-Verknüpfung nicht greift", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Hat das Fenster nicht verknüpft? Öffne plex.tv/link und gib diesen Code ein:" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Window didn't link? Open plex.tv/link and enter this code:" + "en": { + "stringUnit": { + "state": "translated", + "value": "Window didn't link? Open plex.tv/link and enter this code:" } } } }, - "signIn.privacy" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Die Anmeldung läuft über deinen Server. Dein Passwort erreicht diese App nie." + "signIn.privacy": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Die Anmeldung läuft über deinen Server. Dein Passwort erreicht diese App nie." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sign-in happens on your server. Your password never reaches this app." + "en": { + "stringUnit": { + "state": "translated", + "value": "Sign-in happens on your server. Your password never reaches this app." } } } }, - "signIn.tagline" : { - "comment" : "Untertitel auf dem Anmeldebildschirm", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Deine Mediathek, schöner." + "signIn.tagline": { + "comment": "Untertitel auf dem Anmeldebildschirm", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Deine Mediathek, schöner." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Your library, nicer." + "en": { + "stringUnit": { + "state": "translated", + "value": "Your library, nicer." } } } }, - "signIn.timedOut" : { - "comment" : "Die Anmeldung wurde nicht innerhalb der Gültigkeit des Codes abgeschlossen", - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Die Anmeldung wurde nicht abgeschlossen. Versuch es noch einmal." + "signIn.timedOut": { + "comment": "Die Anmeldung wurde nicht innerhalb der Gültigkeit des Codes abgeschlossen", + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Die Anmeldung wurde nicht abgeschlossen. Versuch es noch einmal." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sign-in wasn't completed. Please try again." + "en": { + "stringUnit": { + "state": "translated", + "value": "Sign-in wasn't completed. Please try again." } } } }, - "signIn.windowFailed" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Das Anmeldefenster ließ sich nicht öffnen." + "signIn.windowFailed": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Das Anmeldefenster ließ sich nicht öffnen." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "The sign-in window couldn't be opened." + "en": { + "stringUnit": { + "state": "translated", + "value": "The sign-in window couldn't be opened." } } } }, - "signIn.withJellyfin" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mit Jellyfin anmelden" + "signIn.withJellyfin": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Mit Jellyfin anmelden" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sign in with Jellyfin" + "en": { + "stringUnit": { + "state": "translated", + "value": "Sign in with Jellyfin" } } } }, - "signIn.withPlex" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mit Plex anmelden" + "signIn.withPlex": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Mit Plex anmelden" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sign in with Plex" + "en": { + "stringUnit": { + "state": "translated", + "value": "Sign in with Plex" } } } }, - "sort.lastPlayed" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Zuletzt gesehen" + "sort.lastPlayed": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Zuletzt gesehen" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Last watched" + "en": { + "stringUnit": { + "state": "translated", + "value": "Last watched" } } } }, - "sort.rating" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bewertung" + "sort.rating": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Bewertung" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Rating" + "en": { + "stringUnit": { + "state": "translated", + "value": "Rating" } } } }, - "sort.recentlyAdded" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Zuletzt hinzugefügt" + "sort.recentlyAdded": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Zuletzt hinzugefügt" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Recently added" + "en": { + "stringUnit": { + "state": "translated", + "value": "Recently added" } } } }, - "sort.releaseDate" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Erscheinungsjahr" + "sort.releaseDate": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Erscheinungsjahr" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Release date" + "en": { + "stringUnit": { + "state": "translated", + "value": "Release date" } } } }, - "sort.titleAscending" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Titel (A–Z)" + "sort.titleAscending": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Titel (A–Z)" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Title (A–Z)" + "en": { + "stringUnit": { + "state": "translated", + "value": "Title (A–Z)" } } } }, - "sort.titleDescending" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Titel (Z–A)" + "sort.titleDescending": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Titel (Z–A)" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Title (Z–A)" + "en": { + "stringUnit": { + "state": "translated", + "value": "Title (Z–A)" } } } }, - "subtitleStyle.headerExplanation" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Größe und Farbe der Untertitel im Player." + "subtitleStyle.headerExplanation": { + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Größe und Farbe der Untertitel im Player." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "How subtitles look in the player." + "en": { + "stringUnit": { + "state": "translated", + "value": "How subtitles look in the player." } } } }, - "subtitleStyle.palette" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Darstellung" + "subtitleStyle.palette": { + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Darstellung" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Appearance" + "en": { + "stringUnit": { + "state": "translated", + "value": "Appearance" } } } }, - "subtitleStyle.palette.blackOnWhite" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Schwarz auf Weiß" + "subtitleStyle.palette.blackOnWhite": { + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Schwarz auf Weiß" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Black on white" + "en": { + "stringUnit": { + "state": "translated", + "value": "Black on white" } } } }, - "subtitleStyle.palette.whiteOnBlack" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Weiß auf Schwarz" + "subtitleStyle.palette.whiteOnBlack": { + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Weiß auf Schwarz" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "White on black" + "en": { + "stringUnit": { + "state": "translated", + "value": "White on black" } } } }, - "subtitleStyle.palette.whiteOnClear" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Weiß" + "subtitleStyle.palette.whiteOnClear": { + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Weiß" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "White" + "en": { + "stringUnit": { + "state": "translated", + "value": "White" } } } }, - "subtitleStyle.palette.yellowOnBlack" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Gelb auf Schwarz" + "subtitleStyle.palette.yellowOnBlack": { + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Gelb auf Schwarz" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Yellow on black" + "en": { + "stringUnit": { + "state": "translated", + "value": "Yellow on black" } } } }, - "subtitleStyle.palette.yellowOnClear" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Gelb" + "subtitleStyle.palette.yellowOnClear": { + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Gelb" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Yellow" + "en": { + "stringUnit": { + "state": "translated", + "value": "Yellow" } } } }, - "subtitleStyle.paletteExplanation" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Gilt für den Standardplayer und für Untertitel, die als Text vorliegen. Bilduntertitel von Blu-rays brennt der Server ins Bild — dort gibt es keinen Text zu gestalten, und diese Einstellungen bleiben ohne Wirkung." + "subtitleStyle.paletteExplanation": { + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Gilt für den Standardplayer und für Untertitel, die als Text vorliegen. Bilduntertitel von Blu-rays brennt der Server ins Bild — dort gibt es keinen Text zu gestalten, und diese Einstellungen bleiben ohne Wirkung." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Applies to the standard player and to subtitles that come as text. Image subtitles from Blu-rays are burned into the picture by the server — there is no text to style, so these settings have no effect there." + "en": { + "stringUnit": { + "state": "translated", + "value": "Applies to the standard player and to subtitles that come as text. Image subtitles from Blu-rays are burned into the picture by the server — there is no text to style, so these settings have no effect there." } } } }, - "subtitleStyle.paletteHeader" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Schrift und Hintergrund" + "subtitleStyle.paletteHeader": { + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Schrift und Hintergrund" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Text and background" + "en": { + "stringUnit": { + "state": "translated", + "value": "Text and background" } } } }, - "subtitleStyle.previewHeader" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vorschau" + "subtitleStyle.previewHeader": { + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Vorschau" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Preview" + "en": { + "stringUnit": { + "state": "translated", + "value": "Preview" } } } }, - "subtitleStyle.previewText" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "So sehen deine Untertitel aus." + "subtitleStyle.previewText": { + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "So sehen deine Untertitel aus." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "This is how your subtitles look." + "en": { + "stringUnit": { + "state": "translated", + "value": "This is how your subtitles look." } } } }, - "subtitleStyle.size" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Größe" + "subtitleStyle.size": { + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Größe" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Size" + "en": { + "stringUnit": { + "state": "translated", + "value": "Size" } } } }, - "subtitleStyle.size.huge" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sehr groß" + "subtitleStyle.size.huge": { + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Sehr groß" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Very large" + "en": { + "stringUnit": { + "state": "translated", + "value": "Very large" } } } }, - "subtitleStyle.size.large" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Groß" + "subtitleStyle.size.large": { + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Groß" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Large" + "en": { + "stringUnit": { + "state": "translated", + "value": "Large" } } } }, - "subtitleStyle.size.normal" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Normal" + "subtitleStyle.size.normal": { + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Normal" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Normal" + "en": { + "stringUnit": { + "state": "translated", + "value": "Normal" } } } }, - "subtitleStyle.size.small" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Klein" + "subtitleStyle.size.small": { + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Klein" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Small" + "en": { + "stringUnit": { + "state": "translated", + "value": "Small" } } } }, - "subtitleStyle.sizeExplanation" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bezogen auf die Vorgabe des Players, nicht auf eine feste Schriftgröße — die Untertitel bleiben damit auf jedem Bildschirm gleich gut lesbar." + "subtitleStyle.sizeExplanation": { + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Bezogen auf die Vorgabe des Players, nicht auf eine feste Schriftgröße — die Untertitel bleiben damit auf jedem Bildschirm gleich gut lesbar." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Relative to the player’s own default rather than a fixed point size, so subtitles stay readable on any screen." + "en": { + "stringUnit": { + "state": "translated", + "value": "Relative to the player’s own default rather than a fixed point size, so subtitles stay readable on any screen." } } } }, - "subtitleStyle.sizeHeader" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Schriftgröße" + "subtitleStyle.sizeHeader": { + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Schriftgröße" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Text size" + "en": { + "stringUnit": { + "state": "translated", + "value": "Text size" } } } }, - "subtitleStyle.title" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Untertitel" + "subtitleStyle.title": { + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Untertitel" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Subtitles" + "en": { + "stringUnit": { + "state": "translated", + "value": "Subtitles" } } } }, - "technical.audio" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tonspur" + "technical.audio": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Tonspur" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Audio" + "en": { + "stringUnit": { + "state": "translated", + "value": "Audio" } } } }, - "technical.bitrate" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bitrate" + "technical.bitrate": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Bitrate" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bitrate" + "en": { + "stringUnit": { + "state": "translated", + "value": "Bitrate" } } } }, - "technical.container" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Container" + "technical.container": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Container" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Container" + "en": { + "stringUnit": { + "state": "translated", + "value": "Container" } } } }, - "technical.directPlayNative" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Direkt (Systemplayer)" + "technical.directPlayNative": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Direkt (Systemplayer)" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Direct (system player)" + "en": { + "stringUnit": { + "state": "translated", + "value": "Direct (system player)" } } } }, - "technical.directPlayVLC" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Direkt (VLC)" + "technical.directPlayVLC": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Direkt (VLC)" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Direct (VLC)" + "en": { + "stringUnit": { + "state": "translated", + "value": "Direct (VLC)" } } } }, - "technical.file" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Datei auf dem Server" + "technical.file": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Datei auf dem Server" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "File on server" + "en": { + "stringUnit": { + "state": "translated", + "value": "File on server" } } } }, - "technical.playbackMethod" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Wiedergabe" + "technical.playbackMethod": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Wiedergabe" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Playback" + "en": { + "stringUnit": { + "state": "translated", + "value": "Playback" } } } }, - "technical.reason" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Grund" + "technical.reason": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Grund" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Reason" + "en": { + "stringUnit": { + "state": "translated", + "value": "Reason" } } } }, - "technical.reason.audioCodec %@" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "iOS kann %@-Ton nicht wiedergeben." + "technical.reason.audioCodec %@": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "iOS kann %@-Ton nicht wiedergeben." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "iOS can't play %@ audio." + "en": { + "stringUnit": { + "state": "translated", + "value": "iOS can't play %@ audio." } } } }, - "technical.reason.chosen" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Im Player wurde eine Qualitätsstufe gewählt." + "technical.reason.chosen": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Im Player wurde eine Qualitätsstufe gewählt." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "A quality step was chosen in the player." + "en": { + "stringUnit": { + "state": "translated", + "value": "A quality step was chosen in the player." } } } }, - "technical.reason.container %@" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "iOS öffnet den Container %@ nicht." + "technical.reason.container %@": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "iOS öffnet den Container %@ nicht." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "iOS can't open the %@ container." + "en": { + "stringUnit": { + "state": "translated", + "value": "iOS can't open the %@ container." } } } }, - "technical.reason.unanalyzed" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Der Server hat diese Datei noch nicht analysiert." + "technical.reason.unanalyzed": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Der Server hat diese Datei noch nicht analysiert." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "The server hasn't analyzed this file yet." + "en": { + "stringUnit": { + "state": "translated", + "value": "The server hasn't analyzed this file yet." } } } }, - "technical.reason.videoCodec %@" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "iOS kann %@ nicht dekodieren." + "technical.reason.videoCodec %@": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "iOS kann %@ nicht dekodieren." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "iOS can't decode %@." + "en": { + "stringUnit": { + "state": "translated", + "value": "iOS can't decode %@." } } } }, - "technical.remux" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Umgepackt" + "technical.remux": { + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Umgepackt" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Remuxed" + "en": { + "stringUnit": { + "state": "translated", + "value": "Remuxed" } } } }, - "technical.remuxAudio" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Umgepackt, Ton umgerechnet" + "technical.remuxAudio": { + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Umgepackt, Ton umgerechnet" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Remuxed, audio converted" + "en": { + "stringUnit": { + "state": "translated", + "value": "Remuxed, audio converted" } } } }, - "technical.resolution" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Auflösung" + "technical.resolution": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Auflösung" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Resolution" + "en": { + "stringUnit": { + "state": "translated", + "value": "Resolution" } } } }, - "technical.size" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Dateigröße" + "technical.size": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Dateigröße" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "File size" + "en": { + "stringUnit": { + "state": "translated", + "value": "File size" } } } }, - "technical.subtitle" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Untertitel" + "technical.subtitle": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Untertitel" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Subtitle" + "en": { + "stringUnit": { + "state": "translated", + "value": "Subtitle" } } } }, - "technical.title" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Technische Angaben" + "technical.title": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Technische Angaben" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Technical details" + "en": { + "stringUnit": { + "state": "translated", + "value": "Technical details" } } } }, - "technical.transcode" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Umgerechnet vom Server" + "technical.transcode": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Umgerechnet vom Server" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Transcoded by server" + "en": { + "stringUnit": { + "state": "translated", + "value": "Transcoded by server" } } } }, - "technical.videoCodec" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bildcodec" + "technical.videoCodec": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Bildcodec" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Video codec" + "en": { + "stringUnit": { + "state": "translated", + "value": "Video codec" } } } }, - "tile.play" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Abspielen" + "tile.play": { + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Abspielen" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Play" + "en": { + "stringUnit": { + "state": "translated", + "value": "Play" } } } }, - "topShelf.noFile" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Noch nichts abgelegt." + "topShelf.noFile": { + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Noch nichts abgelegt." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nothing stored yet." + "en": { + "stringUnit": { + "state": "translated", + "value": "Nothing stored yet." } } } }, - "topShelf.noGroup" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Keine App-Gruppe — das Regal kann nichts lesen." + "topShelf.noGroup": { + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Keine App-Gruppe — das Regal kann nichts lesen." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "No app group — the shelf cannot read anything." + "en": { + "stringUnit": { + "state": "translated", + "value": "No app group — the shelf cannot read anything." } } } }, - "topShelf.title" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Regal über der App" + "topShelf.title": { + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Regal über der App" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Top Shelf" + "en": { + "stringUnit": { + "state": "translated", + "value": "Top Shelf" } } } }, - "topShelf.unreadable" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Datei liegt da, lässt sich aber nicht lesen." + "topShelf.unreadable": { + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Datei liegt da, lässt sich aber nicht lesen." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "File is there but cannot be read." + "en": { + "stringUnit": { + "state": "translated", + "value": "File is there but cannot be read." } } } }, - "topShelf.written %lld %lld %@" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lld Reihen, %lld Einträge, zuletzt %@" + "topShelf.written %lld %lld %@": { + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "%lld Reihen, %lld Einträge, zuletzt %@" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lld rows, %lld entries, last %@" + "en": { + "stringUnit": { + "state": "translated", + "value": "%lld rows, %lld entries, last %@" } } } } }, - "version" : "1.1" -} \ No newline at end of file + "version": "1.1" +} diff --git a/Flixy/Session/DownloadService.swift b/Flixy/Session/DownloadService.swift index 72faf6b..c3560b8 100644 --- a/Flixy/Session/DownloadService.swift +++ b/Flixy/Session/DownloadService.swift @@ -139,6 +139,11 @@ struct DownloadService { durationMilliseconds: item.duration, originalBytes: part.size ?? 0 ), + // **Nur beim Original ist die Größe eine Tatsache.** Sie steht in + // den Metadaten des Servers. Die umgerechneten Stufen sind aus + // Bitrate und Dauer hochgerechnet — und was hochgerechnet ist, + // darf abweichen, ohne dass die Datei kaputt wäre. + expectedIsExact: quality == .original && part.size != nil, offlineMedia: offlineMedia(for: item, media: media, part: part) ) } diff --git a/Flixy/Views/Downloads/DownloadsView.swift b/Flixy/Views/Downloads/DownloadsView.swift index bf7c7b6..77defc6 100644 --- a/Flixy/Views/Downloads/DownloadsView.swift +++ b/Flixy/Views/Downloads/DownloadsView.swift @@ -222,14 +222,21 @@ private struct DownloadRow: View { .accessibilityLabel(Text("downloads.pause")) case .paused, .failed: + // **Fortsetzen und erneut versuchen sind nicht dasselbe**, und + // der Knopf hat beides gleich genannt. „Fortsetzen" stand auch + // dort, wo es nichts fortzusetzen gab — eine Zusage, die der Code + // nicht halten konnte: ein gescheiterter Download fing wieder bei + // null an. Jetzt entscheidet der Wiederaufnahme-Datensatz, und er + // entsteht genau dann, wenn eine Übertragung unterbrochen wurde. Button { Task { await downloads.resume(item) } } label: { - Image(systemName: "arrow.clockwise.circle").font(.title3) + Image(systemName: continues ? "play.circle" : "arrow.clockwise.circle") + .font(.title3) } .buttonStyle(.plain) .foregroundStyle(accent) - .accessibilityLabel(Text("downloads.resume")) + .accessibilityLabel(Text(continues ? "downloads.resume" : "action.retry")) case .completed: Image(systemName: "checkmark.circle.fill") @@ -260,12 +267,51 @@ private struct DownloadRow: View { } } + /// Ob ein Antippen dort weitermacht, wo es aufgehört hat. + /// + /// Ein angehaltener Download hat seinen Wiederaufnahme-Datensatz immer; + /// ein gescheiterter nur, wenn ihn das Netz unterbrochen hat und nicht der + /// Server abgelehnt. + private var continues: Bool { + item.state == .paused || item.resumeData != nil + } + + /// Wie schnell es gerade geht — `nil`, solange nichts gemessen ist. + private var speed: String? { + guard let perSecond = downloads.rate(forRatingKey: item.ratingKey)?.bytesPerSecond, + perSecond > 0 + else { return nil } + let menge = Int64(perSecond).formatted(.byteCount(style: .file)) + return String(localized: "downloads.perSecond \(menge)") + } + + /// Wie lange es bei diesem Tempo noch dauert. + private var remaining: String? { + guard let rate = downloads.rate(forRatingKey: item.ratingKey), + let rest = rate.remaining(received: item.receivedBytes, total: item.totalBytes) + else { return nil } + // Unter einer Minute keine Zahl: „noch 0 Min" wäre falsch und „noch + // 43 Sek" genauer, als die Schätzung ist. + guard rest >= .seconds(60) else { + return String(localized: "downloads.remaining.underMinute") + } + return String(localized: "downloads.remaining \(rest.runtimeLabel)") + } + private var statusLine: String { switch item.state { case .downloading where item.totalBytes > 0: - "\(item.receivedBytes.formatted(.byteCount(style: .file))) / \(item.totalBytes.formatted(.byteCount(style: .file)))" + [ + "\(item.receivedBytes.formatted(.byteCount(style: .file))) / \(item.totalBytes.formatted(.byteCount(style: .file)))", + speed, + remaining, + ].compactMap(\.self).joined(separator: " · ") case .downloading: - item.receivedBytes.formatted(.byteCount(style: .file)) + // Ohne bekannte Gesamtgröße gibt es keine Restzeit — das Tempo + // aber schon, und es ist genau dort die einzige Auskunft darüber, + // ob überhaupt etwas durchgeht. + [item.receivedBytes.formatted(.byteCount(style: .file)), speed] + .compactMap(\.self).joined(separator: " · ") case .paused: String(localized: "downloads.paused") case .queued, .waiting: diff --git a/Flixy/Views/Navigation/RootView.swift b/Flixy/Views/Navigation/RootView.swift index 7afc157..f9b0eeb 100644 --- a/Flixy/Views/Navigation/RootView.swift +++ b/Flixy/Views/Navigation/RootView.swift @@ -181,6 +181,19 @@ struct RootView: View { Task { await handOver(to: request) } } .task { await session.restore() } + // **Kommt das Netz zurück, geht es weiter.** + // + // Ein Download, den ein Funkloch unterbrochen hat, lag bisher da, bis + // ihn jemand von Hand antippte — auch dann, wenn das WLAN seit einer + // Stunde wieder stand. + // + // Hier und nicht im Koordinator: `FlixyDownloads` hängt bewusst nicht + // an `NetworkConditions`, es bekommt den Netzzustand als Wert gereicht + // (siehe `TransferHold`). Diese Ansicht kennt beide. + .onChange(of: network.isReachable) { _, erreichbar in + guard erreichbar else { return } + Task { await downloads.resumeInterrupted() } + } .onChange(of: session.phase) { _, newPhase in // Nach einer Neuanmeldung darf nicht eine Mediathek ausgewählt // bleiben, die es auf dem neuen Server gar nicht gibt. diff --git a/Packages/FlixyKit/Sources/FlixyDownloads/DownloadRate.swift b/Packages/FlixyKit/Sources/FlixyDownloads/DownloadRate.swift new file mode 100644 index 0000000..01cbf19 --- /dev/null +++ b/Packages/FlixyKit/Sources/FlixyDownloads/DownloadRate.swift @@ -0,0 +1,102 @@ +import Foundation + +/// Wie schnell ein Download gerade läuft — und wie lange er noch braucht. +/// +/// **Ein Wertetyp mit Uhr von außen.** Die Rückmeldungen der Übertragung +/// kommen mehrmals je Sekunde; was daraus eine Zahl macht, ist reine Rechnung +/// und gehört nicht in den Koordinator. Die Zeitpunkte kommen als Argument +/// herein, damit sich eine Übertragung ohne echte Uhr nachstellen lässt — eine +/// Rate, die man nur im Betrieb beurteilen kann, ist keine prüfbare Zahl. +/// +/// **Warum geglättet.** Zwischen zwei Rückmeldungen schwankt die rohe +/// Differenz um ein Vielfaches: ein Puffer läuft voll, der Funk hat ein Loch, +/// das System hält die Hintergrundsitzung kurz an. Ungeglättet stünde dort +/// eine Zahl, die zehnmal je Sekunde springt — und eine springende Zahl ist +/// keine Auskunft, sondern Unruhe. +public struct DownloadRate: Sendable, Equatable { + /// Wie stark ein neuer Messwert gegenüber dem bisherigen zählt. + /// + /// Ein Drittel: schnell genug, dass ein echter Einbruch binnen weniger + /// Sekunden ankommt, träge genug, dass einzelne Ausreißer untergehen. + private static let weight = 1.0 / 3.0 + + /// Kürzere Abstände werden nicht gemessen. + /// + /// Über fünfzig Millisekunden gerechnet entscheidet der Zufall, wann das + /// System den Puffer leert — nicht die Leitung. + static let minimumInterval: Duration = .milliseconds(500) + + /// Ab dieser Lücke beginnt die Messung von vorn. + /// + /// So lange ohne eine einzige Rückmeldung heißt nicht „langsam", sondern + /// „stand still": die App war angehalten, das Gerät gesperrt, das Netz + /// weg. Diese Pause in die Rate einzurechnen ergäbe eine Zahl nahe null + /// und eine Restzeit von Stunden. + static let maximumInterval: Duration = .seconds(10) + + /// Ein Messpunkt: wie viel war wann da. + /// + /// Als Typ und nicht als Tupel — ein Tupel kann `Equatable` nicht erben, + /// und ohne das wäre der ganze Wertetyp nicht vergleichbar. + private struct Anchor: Equatable { + var bytes: Int64 + var at: ContinuousClock.Instant + } + + /// Der letzte Messpunkt, gegen den gerechnet wird. + private var anchor: Anchor? + private var smoothed: Double? + + public init() {} + + /// Bytes je Sekunde — `nil`, solange nichts gemessen wurde. + /// + /// **Nicht null.** Null wäre eine Auskunft („es geht nichts durch"), und + /// die stimmt in der ersten halben Sekunde nie. + public var bytesPerSecond: Double? { smoothed } + + /// Nimmt den Stand der Übertragung entgegen. + public mutating func record(bytes: Int64, at now: ContinuousClock.Instant) { + guard let anchor else { + self.anchor = Anchor(bytes: bytes, at: now) + return + } + + let elapsed = now - anchor.at + guard elapsed >= Self.minimumInterval else { return } + defer { self.anchor = Anchor(bytes: bytes, at: now) } + + // Rückwärts heißt: eine neue Aufgabe fängt von vorn an. Dann ist jede + // bisherige Messung ungültig — und eine negative Rate wäre Unsinn. + guard bytes >= anchor.bytes, elapsed <= Self.maximumInterval else { + smoothed = nil + return + } + + let seconds = elapsed.inSeconds + guard seconds > 0 else { return } + + let gemessen = Double(bytes - anchor.bytes) / seconds + smoothed = smoothed.map { $0 + (gemessen - $0) * Self.weight } ?? gemessen + } + + /// Wie lange es bei dieser Rate noch dauert. + /// + /// `nil`, solange keine Rate feststeht oder die Gesamtgröße unbekannt ist. + /// Eine Restzeit ohne Rate wäre geraten, und geraten sieht man ihr nicht + /// an. + public func remaining(received: Int64, total: Int64) -> Duration? { + guard let smoothed, smoothed > 0, total > received else { return nil } + return .seconds(Double(total - received) / smoothed) + } +} + +extension Duration { + /// Die Dauer als Sekunden. + /// + /// `components` liefert ganze Sekunden und Attosekunden getrennt; für eine + /// Division braucht es beides in einer Zahl. + var inSeconds: Double { + Double(components.seconds) + Double(components.attoseconds) / 1e18 + } +} diff --git a/Packages/FlixyKit/Sources/FlixyDownloads/Engine/DownloadCoordinator.swift b/Packages/FlixyKit/Sources/FlixyDownloads/Engine/DownloadCoordinator.swift index 6a23a31..f4578e8 100644 --- a/Packages/FlixyKit/Sources/FlixyDownloads/Engine/DownloadCoordinator.swift +++ b/Packages/FlixyKit/Sources/FlixyDownloads/Engine/DownloadCoordinator.swift @@ -16,6 +16,8 @@ public struct DownloadPlan: Sendable { public var episodeIndex: Int? public var durationMilliseconds: Int? public var expectedBytes: Int64 + /// Ob `expectedBytes` die genaue Größe ist — siehe `DownloadItem`. + public var expectedIsExact: Bool public var offlineMedia: OfflineMedia? public init( @@ -30,6 +32,7 @@ public struct DownloadPlan: Sendable { episodeIndex: Int? = nil, durationMilliseconds: Int? = nil, expectedBytes: Int64 = 0, + expectedIsExact: Bool = false, offlineMedia: OfflineMedia? = nil ) { self.ratingKey = ratingKey @@ -43,6 +46,7 @@ public struct DownloadPlan: Sendable { self.episodeIndex = episodeIndex self.durationMilliseconds = durationMilliseconds self.expectedBytes = expectedBytes + self.expectedIsExact = expectedIsExact self.offlineMedia = offlineMedia } } @@ -80,6 +84,28 @@ public final class DownloadCoordinator { /// Wie viel die Downloads insgesamt belegen. public private(set) var usedBytes: Int64 = 0 + /// Das gemessene Tempo je laufender Übertragung. + /// + /// **Im Arbeitsspeicher, nicht in der Datenbank** — aus demselben Grund, + /// aus dem `handleProgress` kein `save()` ruft: die Messpunkte kommen + /// mehrmals je Sekunde, und jedes Schreiben wäre eine Plattenoperation. + /// Über einen Neustart hinweg wäre eine alte Rate ohnehin wertlos. + private var rates: [String: DownloadRate] = [:] + + /// Für welche Titel der Server die Größe **angekündigt** hat. + /// + /// Der Unterschied entscheidet, wann eine Datei als abgerissen gilt. Bei + /// einer angekündigten Größe ist jedes fehlende Byte ein Abriss; bei einer + /// hochgerechneten (Transcoder: Bitrate × Dauer) wäre dieselbe Strenge + /// falsch, denn die Schätzung weicht von Haus aus ab. + /// + /// Woran man es erkennt: `URLSession` meldet die erwartete Größe nur, wenn + /// die Antwort eine `Content-Length` trägt — sonst kommt −1. + /// + /// Ebenfalls im Arbeitsspeicher: nach einem Neustart wird die Übertragung + /// neu eingereiht, und dann meldet sich der Server wieder. + private var announcedSizes: Set = [] + /// Nur über WLAN übertragen. public var isWiFiOnly: Bool { didSet { defaults.set(isWiFiOnly, forKey: Key.wifiOnly) } @@ -279,7 +305,8 @@ public final class DownloadCoordinator { seasonIndex: plan.seasonIndex, episodeIndex: plan.episodeIndex, durationMilliseconds: plan.durationMilliseconds, - offlineMediaData: plan.offlineMedia.flatMap { try? JSONEncoder().encode($0) } + offlineMediaData: plan.offlineMedia.flatMap { try? JSONEncoder().encode($0) }, + expectedIsExact: plan.expectedIsExact ) context.insert(item) save() @@ -343,6 +370,7 @@ public final class DownloadCoordinator { task.cancel { continuation.resume(returning: $0) } } item.resumeData = data + forgetTransfer(item.ratingKey) item.state = .paused save() await startNextIfPossible() @@ -364,6 +392,7 @@ public final class DownloadCoordinator { /// Entfernt Eintrag und Datei. public func remove(_ item: DownloadItem) { + forgetTransfer(item.ratingKey) fileStore.remove(fileName: item.localFileName) fileStore.remove(fileName: item.artworkFileName) context.delete(item) @@ -462,13 +491,40 @@ public final class DownloadCoordinator { func handleProgress(ratingKey: String, received: Int64, expected: Int64) { guard let item = items.first(where: { $0.ratingKey == ratingKey }) else { return } item.receivedBytes = received - if expected > 0 { item.totalBytes = expected } + if expected > 0 { + item.totalBytes = expected + // Hier und nur hier ist belegt, dass die Größe vom Server kommt + // und nicht hochgerechnet ist. Siehe `announcedSizes`. + announcedSizes.insert(ratingKey) + } + + var rate = rates[ratingKey] ?? DownloadRate() + rate.record(bytes: received, at: .now) + rates[ratingKey] = rate + updateActivity() // Bewusst ohne `save()`: der Fortschritt kommt mehrmals je Sekunde, und // jedes Schreiben wäre eine Plattenoperation. Für die Anzeige reicht // die Beobachtung; gespeichert wird bei Zustandswechseln. } + /// Das gemessene Tempo einer laufenden Übertragung. + /// + /// `nil`, solange noch nichts gemessen wurde — und das ist eine ehrlichere + /// Antwort als „0 MB/s". + public func rate(forRatingKey key: String) -> DownloadRate? { + rates[key] + } + + /// Vergisst, was nur zu einer laufenden Übertragung gehört. + /// + /// Beim nächsten Anlauf wird neu gemessen: eine Rate von vorhin gälte + /// sonst für eine Leitung, die es inzwischen nicht mehr gibt. + private func forgetTransfer(_ ratingKey: String) { + rates[ratingKey] = nil + announcedSizes.remove(ratingKey) + } + func handleCompletion(ratingKey: String, temporaryFile: URL) { guard let item = items.first(where: { $0.ratingKey == ratingKey }), let sourceURL = URL(string: item.sourceURLString) @@ -483,13 +539,19 @@ public final class DownloadCoordinator { // Fehler verdeckt: kamen nur ein paar Kilobyte an, wurden sie als // empfangen *und* als erwartet notiert — und die Anzeige stand auf // hundert Prozent. - if let problem = Self.implausibility(written: written, expected: item.totalBytes) { + let problem = Self.implausibility( + written: written, + expected: item.totalBytes, + announced: item.expectedIsExact || announcedSizes.contains(ratingKey) + ) + if let problem { fileStore.remove(fileName: name) fail(item, message: problem) Task { await startNextIfPossible() } return } + forgetTransfer(ratingKey) item.localFileName = name item.receivedBytes = written if item.totalBytes <= 0 { item.totalBytes = written } @@ -508,10 +570,22 @@ public final class DownloadCoordinator { /// Prüft, ob die fertige Datei plausibel ist. /// /// Gibt eine Begründung zurück, wenn nicht. Zwei Fälle fallen darunter: - /// eine winzige Datei (fast immer eine Fehlerseite) und eine, die deutlich - /// unter der angekündigten Größe liegt (abgebrochene Übertragung, die als + /// eine winzige Datei (fast immer eine Fehlerseite) und eine, die unter + /// der erwarteten Größe liegt (abgebrochene Übertragung, die als /// erfolgreich gemeldet wurde). - static func implausibility(written: Int64, expected: Int64) -> String? { + /// + /// **`announced` entscheidet, wie streng gemessen wird** — und daran hing + /// ein Fehler, der Dateien kaputt machte und als Erfolg verbuchte. Hier + /// stand früher für jede Größe dieselbe Regel: unter der Hälfte ist es ein + /// Abriss, darüber in Ordnung. Eine bei sechzig Prozent abgerissene Datei + /// kam damit durch, bekam einen Haken — und brach beim Abspielen + /// mittendrin ab. + /// + /// Die Hälfte-Regel war nie für diesen Fall gedacht, sondern für die + /// **hochgerechnete** Größe der Transcoder-Stufen (Bitrate × Dauer). Dort + /// ist Abweichung normal. Hat der Server die Größe dagegen angekündigt, + /// ist jedes fehlende Byte ein Abriss, und zwar genau einer. + static func implausibility(written: Int64, expected: Int64, announced: Bool) -> String? { // Unter hundert Kilobyte ist keine Videodatei. Dieser Wert liegt // bewusst niedrig: eine sehr kurze Folge kann klein sein, eine // HTML-Fehlerseite ist es immer. @@ -521,9 +595,10 @@ public final class DownloadCoordinator { bundle: .module ) } - // Zehn Prozent Abweichung nach unten sind noch erklärbar — der - // Transcoder kennt die Endgröße vorab nicht genau. Die Hälfte nicht. - if expected > 0, Double(written) < Double(expected) * 0.5 { + // Angekündigt: auf das Byte genau. Hochgerechnet: die Hälfte, denn der + // Transcoder kennt die Endgröße vorab nicht. + let untergrenze = announced ? Double(expected) : Double(expected) * 0.5 + if expected > 0, Double(written) < untergrenze { return String( localized: "downloads.failed.truncated \(written.formatted(.byteCount(style: .file))) \(expected.formatted(.byteCount(style: .file)))", bundle: .module @@ -549,11 +624,49 @@ public final class DownloadCoordinator { return } + // **Den Wiederaufnahme-Datensatz retten.** + // + // Hier lag der zweite Teil des Fehlers. Es gab ihn nur beim Pausieren + // von Hand (`pause(_:)`); nach einem Netzabbruch war er weg, und + // „Fortsetzen" lud eine vierzig Gigabyte große Datei wieder ab Byte + // null — während der Knopf daneben „Fortsetzen" hieß. Eine + // Beschriftung, die der Code nicht halten konnte. + // + // iOS legt den Datensatz dem Fehler bei, wenn die Übertragung + // unterbrochen wurde und fortsetzbar ist. Fehlt er, war es keine + // Unterbrechung, sondern eine Ablehnung — und `resumeInterrupted()` + // unterscheidet die beiden genau daran. + item.resumeData = (error as NSError) + .userInfo[NSURLSessionDownloadTaskResumeData] as? Data + fail(item, message: error.localizedDescription) Task { await startNextIfPossible() } } + /// Nimmt wieder auf, was nur das Netz unterbrochen hat. + /// + /// **Weil niemand sonst merkt, dass das WLAN zurück ist.** Ein + /// gescheiterter Download lag bisher, bis man ihn von Hand antippte — auch + /// dann, wenn die Verbindung seit einer Stunde wieder stand. + /// + /// Nur Einträge mit Wiederaufnahme-Datensatz: den gibt es bei einer + /// unterbrochenen Übertragung, nicht bei einer abgelehnten. Ein Titel, den + /// der Server mit „nicht gefunden" beantwortet, versucht sich damit nicht + /// im Kreis. + public func resumeInterrupted() async { + let unterbrochen = items.filter { $0.state == .failed && $0.resumeData != nil } + guard !unterbrochen.isEmpty else { return } + + for item in unterbrochen { + item.state = .queued + item.failureMessage = nil + } + save() + await startNextIfPossible() + } + private func fail(_ item: DownloadItem, message: String) { + forgetTransfer(item.ratingKey) item.state = .failed item.failureMessage = message save() diff --git a/Packages/FlixyKit/Sources/FlixyDownloads/Model/DownloadItem.swift b/Packages/FlixyKit/Sources/FlixyDownloads/Model/DownloadItem.swift index afe93f1..ab0c071 100644 --- a/Packages/FlixyKit/Sources/FlixyDownloads/Model/DownloadItem.swift +++ b/Packages/FlixyKit/Sources/FlixyDownloads/Model/DownloadItem.swift @@ -29,6 +29,17 @@ public final class DownloadItem { public var totalBytes: Int64 public var receivedBytes: Int64 + /// Ob `totalBytes` die **genaue** Größe ist oder eine Schätzung. + /// + /// **Daran hängt, wann eine Datei als abgerissen gilt.** Beim Original + /// nennt der Server die Dateigröße in den Metadaten — da ist jedes + /// fehlende Byte ein Abriss. Bei den umgerechneten Stufen rechnet die App + /// die Größe aus Bitrate und Dauer hoch, und die weicht von Haus aus ab; + /// dort wäre dieselbe Strenge falsch. + /// + /// Ohne diese Unterscheidung galt für beides die Hälfte-Regel — und eine + /// bei sechzig Prozent abgerissene Datei bekam einen Haken. + public var expectedIsExact: Bool = false /// Dateiname im Download-Ordner. Nicht der volle Pfad: der Container der /// App bekommt bei jedem Systemupdate eine neue Kennung, ein gespeicherter @@ -69,7 +80,8 @@ public final class DownloadItem { seasonIndex: Int? = nil, episodeIndex: Int? = nil, durationMilliseconds: Int? = nil, - offlineMediaData: Data? = nil + offlineMediaData: Data? = nil, + expectedIsExact: Bool = false ) { self.ratingKey = ratingKey self.title = title @@ -85,6 +97,7 @@ public final class DownloadItem { self.episodeIndex = episodeIndex self.durationMilliseconds = durationMilliseconds self.offlineMediaData = offlineMediaData + self.expectedIsExact = expectedIsExact self.addedAt = Date() } diff --git a/Packages/FlixyKit/Tests/FlixyDownloadsTests/DownloadCoordinatorTests.swift b/Packages/FlixyKit/Tests/FlixyDownloadsTests/DownloadCoordinatorTests.swift index e83b17f..2da77d5 100644 --- a/Packages/FlixyKit/Tests/FlixyDownloadsTests/DownloadCoordinatorTests.swift +++ b/Packages/FlixyKit/Tests/FlixyDownloadsTests/DownloadCoordinatorTests.swift @@ -55,6 +55,22 @@ private func temporaryRoot() -> URL { @Suite("Download-Warteschlange") struct DownloadCoordinatorTests { + @Test("Ob die Größe genau ist, kommt am Eintrag an") + func carriesExactSizeFlag() throws { + let coordinator = try makeCoordinator(root: temporaryRoot()) + + var genau = plan("a", bytes: 20_000_000) + genau.expectedIsExact = true + let eins = try coordinator.enqueue(genau) + #expect(eins.expectedIsExact) + + // Die Voreinstellung ist „geschätzt" — die umgerechneten Stufen + // dürfen abweichen, und wer das Feld vergisst, bekommt die mildere + // Regel statt verworfener Downloads. + let zwei = try coordinator.enqueue(plan("b", bytes: 20_000_000)) + #expect(!zwei.expectedIsExact) + } + @Test("Ein Titel wird angenommen und steht in der Warteschlange") func acceptsFirstDownload() throws { let coordinator = try makeCoordinator(root: temporaryRoot()) diff --git a/Packages/FlixyKit/Tests/FlixyDownloadsTests/DownloadRateTests.swift b/Packages/FlixyKit/Tests/FlixyDownloadsTests/DownloadRateTests.swift new file mode 100644 index 0000000..a7d1259 --- /dev/null +++ b/Packages/FlixyKit/Tests/FlixyDownloadsTests/DownloadRateTests.swift @@ -0,0 +1,118 @@ +import Testing +@testable import FlixyDownloads + +@Suite("Downloadrate und Restzeit") +struct DownloadRateTests { + /// Ein fester Anfangszeitpunkt, von dem aus gerechnet wird. + /// + /// Die Uhr kommt von außen herein — deshalb braucht hier nichts zu warten, + /// und eine Übertragung über eine Viertelstunde ist in Mikrosekunden + /// nachgestellt. + private let start = ContinuousClock.now + + @Test("Vor dem ersten Abstand gibt es keine Zahl") + func noRateFromASinglePoint() { + var rate = DownloadRate() + rate.record(bytes: 0, at: start) + // Null wäre eine Auskunft („es geht nichts durch"), und die stimmt + // nicht — es ist nur noch nichts gemessen. + #expect(rate.bytesPerSecond == nil) + } + + @Test("Gleichmäßiger Zufluss ergibt eine ruhige Zahl") + func steadyFlowIsSteady() { + var rate = DownloadRate() + for sekunde in 0...5 { + rate.record(bytes: Int64(sekunde) * 1_000_000, at: start.advanced(by: .seconds(sekunde))) + } + // Auf das Byte genau: die Glättung zieht einen Messwert nur dann in + // eine Richtung, wenn er vom bisherigen abweicht. + #expect(rate.bytesPerSecond == 1_000_000) + } + + @Test("Ein Ausreißer verschiebt die Zahl, aber reißt sie nicht mit") + func outlierIsDamped() { + var rate = DownloadRate() + for sekunde in 0...5 { + rate.record(bytes: Int64(sekunde) * 1_000_000, at: start.advanced(by: .seconds(sekunde))) + } + // Eine einzelne Sekunde mit dem Zehnfachen. + rate.record(bytes: 15_000_000, at: start.advanced(by: .seconds(6))) + + let gemessen = try! #require(rate.bytesPerSecond) + // Roh wären es zehn Millionen. Geglättet liegt die Zahl dazwischen — + // sie folgt, statt zu springen. + #expect(gemessen > 1_000_000) + #expect(gemessen < 10_000_000) + } + + @Test("Messpunkte dichter als eine halbe Sekunde zählen nicht") + func samplesTooCloseAreIgnored() { + var rate = DownloadRate() + rate.record(bytes: 0, at: start) + rate.record(bytes: 50_000, at: start.advanced(by: .milliseconds(100))) + rate.record(bytes: 100_000, at: start.advanced(by: .milliseconds(200))) + #expect(rate.bytesPerSecond == nil) + + // Und der Bezugspunkt ist dabei nicht weitergerückt: gemessen wird + // über die volle Sekunde seit dem ersten Punkt, nicht über die letzten + // achthundert Millisekunden. + rate.record(bytes: 1_000_000, at: start.advanced(by: .seconds(1))) + #expect(rate.bytesPerSecond == 1_000_000) + } + + @Test("Eine lange Stille verfälscht die Rate nicht") + func longGapResetsTheMeasurement() { + var rate = DownloadRate() + rate.record(bytes: 0, at: start) + rate.record(bytes: 1_000_000, at: start.advanced(by: .seconds(1))) + #expect(rate.bytesPerSecond == 1_000_000) + + // Eine halbe Minute ohne Rückmeldung heißt nicht „langsam", sondern + // „stand still": App angehalten, Gerät gesperrt, Netz weg. Daraus eine + // Rate zu rechnen ergäbe dreißig Kilobyte je Sekunde und eine Restzeit + // von Stunden. + rate.record(bytes: 1_030_000, at: start.advanced(by: .seconds(31))) + #expect(rate.bytesPerSecond == nil) + } + + @Test("Fängt die Übertragung von vorn an, gilt keine alte Zahl") + func restartFromZeroDiscardsTheRate() { + var rate = DownloadRate() + rate.record(bytes: 0, at: start) + rate.record(bytes: 1_000_000, at: start.advanced(by: .seconds(1))) + #expect(rate.bytesPerSecond == 1_000_000) + + // Genau das passiert, wenn ein gescheiterter Download ohne + // Wiederaufnahme-Datensatz neu beginnt. + rate.record(bytes: 0, at: start.advanced(by: .seconds(2))) + #expect(rate.bytesPerSecond == nil) + } + + @Test("Die Restzeit folgt aus Rate und Rest") + func remainingFollowsFromRate() { + var rate = DownloadRate() + rate.record(bytes: 0, at: start) + rate.record(bytes: 1_000_000, at: start.advanced(by: .seconds(1))) + + let rest = try! #require(rate.remaining(received: 1_000_000, total: 11_000_000)) + #expect(rest == .seconds(10)) + } + + @Test("Ohne Rate keine Restzeit") + func noRemainingWithoutRate() { + var rate = DownloadRate() + rate.record(bytes: 0, at: start) + // Eine Restzeit ohne Messung wäre geraten — und geraten sieht man ihr + // nicht an. + #expect(rate.remaining(received: 0, total: 1_000_000) == nil) + } + + @Test("Ist alles da, gibt es nichts mehr zu schätzen") + func noRemainingWhenComplete() { + var rate = DownloadRate() + rate.record(bytes: 0, at: start) + rate.record(bytes: 1_000_000, at: start.advanced(by: .seconds(1))) + #expect(rate.remaining(received: 1_000_000, total: 1_000_000) == nil) + } +} diff --git a/Packages/FlixyKit/Tests/FlixyDownloadsTests/DownloadRulesTests.swift b/Packages/FlixyKit/Tests/FlixyDownloadsTests/DownloadRulesTests.swift index fd36040..1e2a71f 100644 --- a/Packages/FlixyKit/Tests/FlixyDownloadsTests/DownloadRulesTests.swift +++ b/Packages/FlixyKit/Tests/FlixyDownloadsTests/DownloadRulesTests.swift @@ -163,36 +163,78 @@ struct DownloadPlausibilityTests { // „fertig". Angekommen war eine HTML-Fehlerseite, und weil die // empfangene Größe auch als erwartete notiert wurde, stand die // Anzeige auf hundert Prozent. - #expect(DownloadCoordinator.implausibility(written: 2_048, expected: 873_000_000) != nil) + #expect(DownloadCoordinator.implausibility( + written: 2_048, expected: 873_000_000, announced: true + ) != nil) } @Test("Auch ohne angekündigte Größe fällt eine Fehlerseite auf") func tinyFileWithoutExpectation() { // Beim umgerechneten Download kennt der Server die Endgröße vorab // nicht — die Untergrenze muss deshalb allein tragen. - #expect(DownloadCoordinator.implausibility(written: 900, expected: 0) != nil) + #expect(DownloadCoordinator.implausibility( + written: 900, expected: 0, announced: false + ) != nil) } @Test("Eine abgebrochene Übertragung fällt auf") func truncatedTransferIsRejected() { - #expect(DownloadCoordinator.implausibility(written: 200_000_000, expected: 873_000_000) != nil) + #expect(DownloadCoordinator.implausibility( + written: 200_000_000, expected: 873_000_000, announced: false + ) != nil) + } + + @Test("Bei sechzig Prozent einer angekündigten Größe ist die Datei kaputt") + func truncatedAtSixtyPercentIsRejected() { + // **Der Fehler, der diese Unterscheidung nötig gemacht hat.** Hier + // galt dieselbe Hälfte-Regel wie für geschätzte Größen, und eine bei + // sechzig Prozent abgerissene Datei kam durch: Haken dran, und beim + // Abspielen brach sie mittendrin ab. Gemeldet wurde das als + // „plötzlich war er fertig". + #expect(DownloadCoordinator.implausibility( + written: 1_800_000_000, expected: 3_000_000_000, announced: true + ) != nil) + } + + @Test("Dieselben sechzig Prozent einer Schätzung gehen durch") + func truncatedAtSixtyPercentOfEstimatePasses() { + // Gleiche Zahlen, andere Herkunft: hochgerechnet aus Bitrate und + // Dauer. Dort ist eine Abweichung nach unten normal, und streng zu + // prüfen hieße, gültige Downloads zu verwerfen. + #expect(DownloadCoordinator.implausibility( + written: 1_800_000_000, expected: 3_000_000_000, announced: false + ) == nil) } @Test("Eine vollständige Datei geht durch") func completeFilePasses() { - #expect(DownloadCoordinator.implausibility(written: 873_000_000, expected: 873_000_000) == nil) + #expect(DownloadCoordinator.implausibility( + written: 873_000_000, expected: 873_000_000, announced: true + ) == nil) } - @Test("Kleinere Abweichungen sind erlaubt") + @Test("Kleinere Abweichungen sind erlaubt, solange die Größe geschätzt ist") func smallDeviationPasses() { // Der Transcoder arbeitet mit variabler Bitrate und trifft die - // geschätzte Größe nie genau. Zu streng zu prüfen hieße, gültige - // Downloads zu verwerfen. - #expect(DownloadCoordinator.implausibility(written: 800_000_000, expected: 873_000_000) == nil) + // geschätzte Größe nie genau. + #expect(DownloadCoordinator.implausibility( + written: 800_000_000, expected: 873_000_000, announced: false + ) == nil) + } + + @Test("Dieselbe Abweichung bei angekündigter Größe ist ein Abriss") + func smallDeviationOfAnnouncedSizeIsRejected() { + // Hat der Server die Größe genannt, gibt es keine erklärbare + // Abweichung: was fehlt, fehlt. + #expect(DownloadCoordinator.implausibility( + written: 800_000_000, expected: 873_000_000, announced: true + ) != nil) } @Test("Ohne Erwartung geht eine normale Datei durch") func noExpectationPasses() { - #expect(DownloadCoordinator.implausibility(written: 500_000_000, expected: 0) == nil) + #expect(DownloadCoordinator.implausibility( + written: 500_000_000, expected: 0, announced: false + ) == nil) } } diff --git a/Tools/JellyfinStub/server.py b/Tools/JellyfinStub/server.py index 9f40233..0c578b5 100644 --- a/Tools/JellyfinStub/server.py +++ b/Tools/JellyfinStub/server.py @@ -1,6 +1,6 @@ """Ein Jellyfin-Attrappenserver — gerade so viel, dass die App anmelden und stöbern kann. Nur zum Ansehen der Oberfläche; kein echter Server wird berührt.""" -import json, os, struct, zlib, hashlib +import json, os, struct, time, zlib, hashlib from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from urllib.parse import urlparse, parse_qs @@ -10,6 +10,28 @@ SAMPLE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "sample.mp4") # Was an Wiedergabemeldungen hereinkam — damit sich prüfen lässt, ob die App # den Fortschritt tatsächlich meldet. REPORTS = [] +# Die kuenstliche grosse Datei — und was mit ihr passieren soll. +# +# **Ohne sie ist ein abgebrochener Download nicht nachstellbar**, und genau +# daran ist ein Fehler jahrelang vorbeigekommen: eine bei sechzig Prozent +# abgerissene Datei galt als fertig. Mit der Beispieldatei (28 KB) laesst sich +# das nicht zeigen — sie ist in einem Rutsch da. +# +# groesse wie gross die Datei sich gibt (Content-Length) +# abbruch_bei nach wie vielen Bytes die Verbindung geschlossen wird, 0 = nie +# tempo Bytes je Sekunde, 0 = so schnell die Leitung hergibt +# anfragen jede Anfrage mit ihrem Bereich — daran ist ablesbar, ob +# fortgesetzt oder von vorn begonnen wurde +# ohne_laenge keine Content-Length ankuendigen. Dann gilt fuer den Client +# das Ende der Verbindung als Ende der Datei — eine abgerissene +# Uebertragung sieht damit wie eine vollstaendige aus. Genau +# dieser Fall macht Dateien kaputt, ohne dass irgendwo ein +# Fehler auftaucht. +TRANSFER = {"groesse": 20_000_000, "abbruch_bei": 0, "tempo": 0, + "ohne_laenge": 0, "anfragen": []} +# Dieselbe Drossel fuer die Beispieldatei. Damit laesst sich der Player lange +# genug offen halten, um eine Geste zu messen. +VIDEO_TEMPO = {"bytes_je_sekunde": 0} # Die Benutzerdatenblaetter, je Titel. # # Ohne sie laesst sich „Aus Weiterschauen entfernen" gar nicht pruefen: der @@ -49,6 +71,8 @@ TITLES = [ # sieht ein wirkungsloser Filter wie ein wirksamer aus. ("x1", "Zwischen den Zeilen", "Movie", "Nur in Gemischtes zu finden."), ("x2", "Randnotiz", "Movie", "Ebenfalls nur in Gemischtes."), + # Die kuenstliche grosse Datei. Nicht zum Abspielen — darin stehen Nullen. + ("big1", "Die grosse Datei", "Movie", "Zum Pruefen von Abbruch und Fortsetzen."), ] # Welche Titel in welcher Mediathek liegen. Was hier nicht steht, liegt # ueberall — so war es vorher, und fuer die uebrigen Pruefungen reicht das. @@ -72,7 +96,10 @@ def item(key, name, kind, overview, *, resume=None, series=None): "MediaSources": [{ "Id": "src-" + key, "Container": "mp4", - "Size": 28135, + # Die angekuendigte Groesse. Sie landet als `expectedBytes` im + # Download und entscheidet mit darueber, ob eine Datei als + # abgerissen gilt. + "Size": TRANSFER["groesse"] if key == "big1" else 28135, "Bitrate": 1_200_000, "RunTimeTicks": 10 * 10_000_000, "SupportsDirectPlay": True, @@ -251,7 +278,11 @@ class Handler(BaseHTTPRequestHandler): # Sowohl `/stream` (abspielen) als auch `/stream.mp4` (herunterladen). # Umgerechnet wird hier natürlich nichts — es kommt dieselbe Datei. if p.startswith("/Videos/") and ("/stream" in p): + if p.split("/")[2] == "big1": + return self.serve_transfer() return self.serve_file(SAMPLE, "video/mp4") + if p == "/__transfer": + return self.reply(TRANSFER) if p.startswith(f"/Users/{USER_ID}/Items/") and p.count("/") == 4: key = p.rsplit("/", 1)[-1] match = next((t for t in TITLES if t[0] == key), None) @@ -290,6 +321,74 @@ class Handler(BaseHTTPRequestHandler): return self.reply(page([{"Id": "g1", "Name": "Drama", "Type": "Genre"}])) return self.reply(page([])) + def serve_transfer(self): + """Eine beliebig grosse Datei, die auf Wunsch mittendrin abreisst. + + Sie besteht aus Nullen — geprueft wird die Uebertragung, nicht der + Inhalt. Bereichsanfragen beantwortet sie wie ein echter Server, und + genau daran ist ablesbar, ob die App fortsetzt oder von vorn beginnt: + steht im Mitschnitt `bytes=12000000-`, hat sie fortgesetzt. + """ + gesamt = TRANSFER["groesse"] + bereich = self.headers.get("Range") + start, status = 0, 200 + if bereich and bereich.startswith("bytes="): + start = int(bereich[6:].split("-")[0] or 0) + status = 206 + + self.send_response(status) + self.send_header("Content-Type", "video/mp4") + self.send_header("Accept-Ranges", "bytes") + # **Ohne Pruefmarke kein Fortsetzen.** `URLSession` legt dem + # Wiederaufnahme-Versuch ein `If-Range` bei; fehlt dem Server eine + # Marke, mit der er das beantworten koennte, faengt die Uebertragung + # wieder bei null an — ganz gleich, wie gut die App ihren Datensatz + # aufbewahrt hat. Gemessen: ohne diese zwei Zeilen kam die zweite + # Anfrage ohne `Range` und holte alle 20 MB noch einmal. + self.send_header("ETag", f'"big1-{TRANSFER["groesse"]}"') + self.send_header("Last-Modified", "Mon, 01 Sep 2025 12:00:00 GMT") + if TRANSFER["ohne_laenge"]: + # Ohne Laenge und ohne Keep-Alive: das Ende der Verbindung ist das + # Ende der Datei. So verhaelt sich ein Transcoder, der die Endgroesse + # vorab nicht kennt. + self.protocol_version = "HTTP/1.0" + self.send_header("Connection", "close") + self.close_connection = True + else: + self.send_header("Content-Length", str(gesamt - start)) + if status == 206: + self.send_header("Content-Range", f"bytes {start}-{gesamt - 1}/{gesamt}") + self.end_headers() + + grenze = TRANSFER["abbruch_bei"] + tempo = TRANSFER["tempo"] + block = bytes(64 * 1024) + stelle, geliefert = start, 0 + while stelle < gesamt: + if grenze and stelle >= grenze: + break + rest = gesamt - stelle + if grenze: + rest = min(rest, grenze - stelle) + menge = min(len(block), rest) + try: + self.wfile.write(block[:menge]) + self.wfile.flush() + except (BrokenPipeError, ConnectionResetError): + break + stelle += menge + geliefert += menge + if tempo: + time.sleep(menge / tempo) + + TRANSFER["anfragen"].append( + {"bereich": bereich, "ab": start, "geliefert": geliefert, + "abgerissen": bool(grenze) and stelle < gesamt} + ) + # Angekuendigt war mehr, als gekommen ist — die Verbindung muss zu. + if stelle < gesamt: + self.close_connection = True + def serve_file(self, path, content_type): """Mit Bereichsanfragen — AVPlayer holt eine Datei nie am Stück.""" with open(path, "rb") as handle: @@ -311,7 +410,19 @@ class Handler(BaseHTTPRequestHandler): if status == 206: self.send_header("Content-Range", f"bytes {start}-{end}/{len(data)}") self.end_headers() - self.wfile.write(body) + + tempo = VIDEO_TEMPO["bytes_je_sekunde"] + if not tempo: + return self.wfile.write(body) + # Gedrosselt: nur so bleibt der Player lange genug offen, um eine + # Geste daran zu messen. Die Beispieldatei ist sonst sofort da. + for stelle in range(0, len(body), 4096): + try: + self.wfile.write(body[stelle:stelle + 4096]) + self.wfile.flush() + except (BrokenPipeError, ConnectionResetError): + return + time.sleep(4096 / tempo) def do_DELETE(self): REPORTS.append(("encoding-stopped", self.path)) @@ -330,6 +441,19 @@ class Handler(BaseHTTPRequestHandler): if url.path.startswith("/Sessions/Playing"): REPORTS.append((url.path, body.decode() if body else "")) return self.reply({}) + # Die Uebertragung steuern: Groesse, Abbruchstelle, Tempo — und die + # Drossel fuer die Beispieldatei. Alles zur Laufzeit, damit sich + # „abreissen lassen, dann fortsetzen" in einem Lauf pruefen laesst. + if url.path == "/__transfer": + neu = json.loads(body or b"{}") + for feld in ("groesse", "abbruch_bei", "tempo", "ohne_laenge"): + if feld in neu: + TRANSFER[feld] = int(neu[feld]) + if neu.get("vergessen"): + TRANSFER["anfragen"].clear() + if "video_tempo" in neu: + VIDEO_TEMPO["bytes_je_sekunde"] = int(neu["video_tempo"]) + return self.reply(TRANSFER) schluessel = userdata_key(url.path) if schluessel is not None: try: