diff --git a/Flixy/Resources/Localizable.xcstrings b/Flixy/Resources/Localizable.xcstrings index df62f31..08aeebd 100644 --- a/Flixy/Resources/Localizable.xcstrings +++ b/Flixy/Resources/Localizable.xcstrings @@ -1,3689 +1,3835 @@ { - "sourceLanguage" : "en", - "strings" : { - "%@ 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 + "sourceLanguage": "en", + "strings": { + "%@ 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 + "1080p": { + "comment": "A label for a 1080p video.", + "isCommentAutoGenerated": true }, - "720p" : { - "comment" : "A label for a 720p video.", - "isCommentAutoGenerated" : true + "4K": { + "comment": "A label for a 4K video.", + "isCommentAutoGenerated": true }, - "1080p" : { - "comment" : "A label for a 1080p video.", - "isCommentAutoGenerated" : true + "720p": { + "comment": "A label for a 720p video.", + "isCommentAutoGenerated": true }, - "action.apply" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Übernehmen" + "SD": { + "comment": "A label for a 720p movie.", + "isCommentAutoGenerated": true + }, + "action.apply": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Übernehmen" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "Apply" + } + } + } + }, + "action.cancel": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Abbrechen" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "Cancel" + } + } + } + }, + "action.clearRating": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Bewertung entfernen" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "Clear rating" + } + } + } + }, + "action.close": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Schließen" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "Close" + } + } + } + }, + "action.delete": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Löschen" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "Delete" + } + } + } + }, + "action.markUnwatched": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Als ungesehen markieren" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "Mark as unwatched" + } + } + } + }, + "action.markWatched": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Als gesehen markieren" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Apply" + "en": { + "stringUnit": { + "state": "translated", + "value": "Mark as watched" } } } }, - "action.cancel" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Abbrechen" + "action.rate": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Bewerten" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Cancel" + "en": { + "stringUnit": { + "state": "translated", + "value": "Rate" } } } }, - "action.clearRating" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bewertung entfernen" + "action.retry": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Erneut versuchen" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Clear rating" + "en": { + "stringUnit": { + "state": "translated", + "value": "Try again" } } } }, - "action.close" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Schließen" + "action.signOut": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Abmelden" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Close" + "en": { + "stringUnit": { + "state": "translated", + "value": "Sign out" } } } }, - "action.delete" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Löschen" + "appearance.accent": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Akzentfarbe" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Delete" + "en": { + "stringUnit": { + "state": "translated", + "value": "Accent color" } } } }, - "action.markUnwatched" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Als ungesehen markieren" + "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" : "Mark as unwatched" + "en": { + "stringUnit": { + "state": "translated", + "value": "The color appears on buttons, progress bars and the selected row in the sidebar." } } } }, - "action.markWatched" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Als gesehen markieren" + "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" : "Mark as watched" + "en": { + "stringUnit": { + "state": "translated", + "value": "Lightened to %@ so it stays readable on the dark background." } } } }, - "action.rate" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bewerten" + "appearance.customColor": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Eigene Farbe" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Rate" + "en": { + "stringUnit": { + "state": "translated", + "value": "Custom color" } } } }, - "action.retry" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Erneut versuchen" + "appearance.hexPlaceholder": { + "comment": "Beispiel für eine Hex-Farbe", + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "#1DB954" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Try again" + "en": { + "stringUnit": { + "state": "translated", + "value": "#1DB954" } } } }, - "action.signOut" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Abmelden" + "appearance.invalidHex": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Das ist keine gültige Farbe. Erwartet wird etwas wie #1DB954." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sign out" + "en": { + "stringUnit": { + "state": "translated", + "value": "That's not a valid color. Try something like #1DB954." } } } }, - "appearance.accent" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Akzentfarbe" + "appearance.preview": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Vorschau" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Accent color" + "en": { + "stringUnit": { + "state": "translated", + "value": "Preview" } } } }, - "appearance.accentExplanation" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Die Farbe erscheint auf Knöpfen, Fortschrittsbalken und der ausgewählten Zeile in der Seitenleiste." + "appearance.title": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Erscheinungsbild" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "The color appears on buttons, progress bars and the selected row in the sidebar." + "en": { + "stringUnit": { + "state": "translated", + "value": "Appearance" } } } }, - "appearance.adjusted %@" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Die Farbe wurde auf %@ aufgehellt, damit sie auf dem dunklen Hintergrund lesbar bleibt." + "connecting.searching": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Server werden gesucht …" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Lightened to %@ so it stays readable on the dark background." + "en": { + "stringUnit": { + "state": "translated", + "value": "Looking for servers…" } } } }, - "appearance.customColor" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Eigene Farbe" + "connecting.to %@": { + "comment": "Platzhalter: Servername", + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Verbinde mit %@ …" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Custom color" + "en": { + "stringUnit": { + "state": "translated", + "value": "Connecting to %@…" } } } }, - "appearance.hexPlaceholder" : { - "comment" : "Beispiel für eine Hex-Farbe", - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "#1DB954" + "detail.cast": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Besetzung" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "#1DB954" + "en": { + "stringUnit": { + "state": "translated", + "value": "Cast" } } } }, - "appearance.invalidHex" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Das ist keine gültige Farbe. Erwartet wird etwas wie #1DB954." + "detail.episodeCount %lld": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "%lld Folgen" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "That's not a valid color. Try something like #1DB954." + "en": { + "stringUnit": { + "state": "translated", + "value": "%lld episodes" } } } }, - "appearance.preview" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vorschau" + "detail.episodes": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Folgen" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Preview" + "en": { + "stringUnit": { + "state": "translated", + "value": "Episodes" } } } }, - "appearance.title" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Erscheinungsbild" + "detail.playFromStart": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Von vorn abspielen" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Appearance" + "en": { + "stringUnit": { + "state": "translated", + "value": "Play from start" } } } }, - "connecting.searching" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Server werden gesucht …" + "detail.seasons": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Staffeln" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Looking for servers…" + "en": { + "stringUnit": { + "state": "translated", + "value": "Seasons" } } } }, - "connecting.to %@" : { - "comment" : "Platzhalter: Servername", - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Verbinde mit %@ …" + "detail.unwatchedCount %lld": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "%lld ungesehen" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Connecting to %@…" + "en": { + "stringUnit": { + "state": "translated", + "value": "%lld unwatched" } } } }, - "detail.cast" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Besetzung" + "detail.versions": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Fassung" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Cast" + "en": { + "stringUnit": { + "state": "translated", + "value": "Version" } } } }, - "detail.episodeCount %lld" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lld Folgen" + "diagnostics.connection": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Verbindung" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lld episodes" + "en": { + "stringUnit": { + "state": "translated", + "value": "Connection" } } } }, - "detail.episodes" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Folgen" + "diagnostics.connectionUsed": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Verbindung" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Episodes" + "en": { + "stringUnit": { + "state": "translated", + "value": "Connection" } } } }, - "detail.playFromStart" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Von vorn abspielen" + "diagnostics.copy": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Bericht kopieren" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Play from start" + "en": { + "stringUnit": { + "state": "translated", + "value": "Copy report" } } } }, - "detail.seasons" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Staffeln" + "diagnostics.copyHint": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Enthält keinen Token. Kannst du unbesorgt weitergeben." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Seasons" + "en": { + "stringUnit": { + "state": "translated", + "value": "Contains no token. Safe to share." } } } }, - "detail.unwatchedCount %lld" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lld ungesehen" + "diagnostics.lastAttempt": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Letzter Abspielversuch" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lld unwatched" + "en": { + "stringUnit": { + "state": "translated", + "value": "Last playback attempt" } } } }, - "detail.versions" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Fassung" + "diagnostics.liveTV": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Live-TV-Antwort ansehen" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Version" + "en": { + "stringUnit": { + "state": "translated", + "value": "Inspect Live TV response" } } } }, - "diagnostics.connection" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Verbindung" + "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" : "Connection" + "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.connectionUsed" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Verbindung" + "diagnostics.method": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Weg" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Connection" + "en": { + "stringUnit": { + "state": "translated", + "value": "Method" } } } }, - "diagnostics.copy" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bericht kopieren" + "diagnostics.noServer": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Keine Serververbindung." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Copy report" + "en": { + "stringUnit": { + "state": "translated", + "value": "No server connection." } } } }, - "diagnostics.copyHint" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Enthält keinen Token. Kannst du unbesorgt weitergeben." + "diagnostics.nothingYet": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Noch nichts abgespielt." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Contains no token. Safe to share." + "en": { + "stringUnit": { + "state": "translated", + "value": "Nothing played yet." } } } }, - "diagnostics.lastAttempt" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Letzter Abspielversuch" + "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" : "Last playback attempt" + "en": { + "stringUnit": { + "state": "translated", + "value": "Via the Plex relay. Heavily throttled — large files barely start over it." } } } }, - "diagnostics.liveTV" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Live-TV-Antwort ansehen" + "diagnostics.source": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Quelle (ohne Token)" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Inspect Live TV response" + "en": { + "stringUnit": { + "state": "translated", + "value": "Source (token removed)" } } } }, - "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.steps": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Ablauf" } }, - "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": "Timeline" } } } }, - "diagnostics.method" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Weg" + "diagnostics.title": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Diagnose" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Method" + "en": { + "stringUnit": { + "state": "translated", + "value": "Diagnostics" } } } }, - "diagnostics.noServer" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Keine Serververbindung." + "diagnostics.title.label": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Titel" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "No server connection." + "en": { + "stringUnit": { + "state": "translated", + "value": "Title" } } } }, - "diagnostics.nothingYet" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Noch nichts abgespielt." + "downloads.active": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Läuft gerade" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nothing played yet." + "en": { + "stringUnit": { + "state": "translated", + "value": "In progress" } } } }, - "diagnostics.relayWarning" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Über die Plex-Vermittlung. Stark gedrosselt — große Dateien laufen darüber kaum an." + "downloads.allEpisodes": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Alle Folgen laden" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Via the Plex relay. Heavily throttled — large files barely start over it." + "en": { + "stringUnit": { + "state": "translated", + "value": "Download all episodes" } } } }, - "diagnostics.source" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Quelle (ohne Token)" + "downloads.available": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Offline verfügbar" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Source (token removed)" + "en": { + "stringUnit": { + "state": "translated", + "value": "Available offline" } } } }, - "diagnostics.steps" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ablauf" + "downloads.batch.queued %lld": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "%lld Folgen in die Warteschlange gelegt." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Timeline" + "en": { + "stringUnit": { + "state": "translated", + "value": "Queued %lld episodes." } } } }, - "diagnostics.title" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Diagnose" + "downloads.batch.title": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Downloads" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Diagnostics" + "en": { + "stringUnit": { + "state": "translated", + "value": "Downloads" } } } }, - "diagnostics.title.label" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Titel" + "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" : "Title" + "en": { + "stringUnit": { + "state": "translated", + "value": "%lld episodes have no playable file on the server." } } } }, - "downloads.active" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Läuft gerade" + "downloads.cancel": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Download abbrechen" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "In progress" + "en": { + "stringUnit": { + "state": "translated", + "value": "Cancel download" } } } }, - "downloads.allEpisodes" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Alle Folgen laden" + "downloads.download": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Herunterladen" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Download all episodes" + "en": { + "stringUnit": { + "state": "translated", + "value": "Download" } } } }, - "downloads.available" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Offline verfügbar" + "downloads.empty.description": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Lade Filme und Folgen herunter, um sie ohne Verbindung anzusehen." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Available offline" + "en": { + "stringUnit": { + "state": "translated", + "value": "Download movies and episodes to watch them without a connection." } } } }, - "downloads.batch.queued %lld" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lld Folgen in die Warteschlange gelegt." + "downloads.empty.title": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Nichts offline" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Queued %lld episodes." + "en": { + "stringUnit": { + "state": "translated", + "value": "Nothing offline" } } } }, - "downloads.batch.title" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Downloads" + "downloads.failed": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Fehlgeschlagen" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Downloads" + "en": { + "stringUnit": { + "state": "translated", + "value": "Failed" } } } }, - "downloads.batch.unavailable %lld" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lld Folgen haben keine abspielbare Datei auf dem Server." + "downloads.movies": { + "comment": "Überschrift für heruntergeladene Filme", + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Filme" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lld episodes have no playable file on the server." + "en": { + "stringUnit": { + "state": "translated", + "value": "Movies" } } } }, - "downloads.cancel" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Download abbrechen" + "downloads.pause": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Anhalten" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Cancel download" + "en": { + "stringUnit": { + "state": "translated", + "value": "Pause" } } } }, - "downloads.download" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Herunterladen" + "downloads.paused": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Angehalten" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Download" + "en": { + "stringUnit": { + "state": "translated", + "value": "Paused" } } } }, - "downloads.empty.description" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Lade Filme und Folgen herunter, um sie ohne Verbindung anzusehen." + "downloads.quality.approx %@ %@": { + "comment": "Platzhalter: Stufenname, geschätzte Größe", + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "%@ · etwa %@" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Download movies and episodes to watch them without a connection." + "en": { + "stringUnit": { + "state": "translated", + "value": "%@ · about %@" } } } }, - "downloads.empty.title" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nichts offline" + "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" : "Nothing offline" + "en": { + "stringUnit": { + "state": "translated", + "value": "Lower levels are transcoded by the server. That takes longer but saves a lot of space." } } } }, - "downloads.failed" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Fehlgeschlagen" + "downloads.quality.title": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "In welcher Qualität?" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Failed" + "en": { + "stringUnit": { + "state": "translated", + "value": "Which quality?" } } } }, - "downloads.movies" : { - "comment" : "Überschrift für heruntergeladene Filme", - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Filme" + "downloads.queued": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Wartet" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Movies" + "en": { + "stringUnit": { + "state": "translated", + "value": "Queued" } } } }, - "downloads.pause" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Anhalten" + "downloads.refused.exists": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Dieser Titel steht bereits in der Warteschlange." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Pause" + "en": { + "stringUnit": { + "state": "translated", + "value": "This title is already queued." } } } }, - "downloads.paused" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Angehalten" + "downloads.refused.limit %@ %@": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Das Speicherlimit ist erreicht: %@ von %@ belegt." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Paused" + "en": { + "stringUnit": { + "state": "translated", + "value": "Storage limit reached: %@ of %@ used." } } } }, - "downloads.quality.approx %@ %@" : { - "comment" : "Platzhalter: Stufenname, geschätzte Größe", - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "%@ · etwa %@" + "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" : "%@ · about %@" + "en": { + "stringUnit": { + "state": "translated", + "value": "There's no file for this title on the server." } } } }, - "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.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" : "Lower levels are transcoded by the server. That takes longer but saves a lot of space." + "en": { + "stringUnit": { + "state": "translated", + "value": "The file on the server has no retrievable part. It may not have been analyzed yet." } } } }, - "downloads.quality.title" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "In welcher Qualität?" + "downloads.refused.noServer": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Keine Verbindung zum Server." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Which quality?" + "en": { + "stringUnit": { + "state": "translated", + "value": "No connection to the server." } } } }, - "downloads.queued" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Wartet" + "downloads.refused.noURL": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Die Download-Adresse ließ sich nicht bilden." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Queued" + "en": { + "stringUnit": { + "state": "translated", + "value": "The download address couldn't be built." } } } }, - "downloads.refused.exists" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Dieser Titel steht bereits in der Warteschlange." + "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" : "This title is already queued." + "en": { + "stringUnit": { + "state": "translated", + "value": "Not enough space on this device: %@ needed, %@ free." } } } }, - "downloads.refused.limit %@ %@" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Das Speicherlimit ist erreicht: %@ von %@ belegt." + "downloads.refused.title": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Download nicht möglich" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Storage limit reached: %@ of %@ used." + "en": { + "stringUnit": { + "state": "translated", + "value": "Can't download" } } } }, - "downloads.refused.noMedia" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Zu diesem Titel liegt keine Datei auf dem Server." + "downloads.removeConfirm": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Download entfernen?" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "There's no file for this title on the server." + "en": { + "stringUnit": { + "state": "translated", + "value": "Remove download?" } } } }, - "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.resume": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Fortsetzen" } }, - "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": "Resume" } } } }, - "downloads.refused.noServer" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Keine Verbindung zum Server." + "downloads.season": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Staffel laden" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "No connection to the server." + "en": { + "stringUnit": { + "state": "translated", + "value": "Download season" } } } }, - "downloads.refused.noURL" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Die Download-Adresse ließ sich nicht bilden." + "downloads.used": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Belegt" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "The download address couldn't be built." + "en": { + "stringUnit": { + "state": "translated", + "value": "Used" } } } }, - "downloads.refused.space %@ %@" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Zu wenig Platz auf dem Gerät: %@ nötig, %@ frei." + "downloads.wifiOnly": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Nur über WLAN laden" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Not enough space on this device: %@ needed, %@ free." + "en": { + "stringUnit": { + "state": "translated", + "value": "Download over Wi-Fi only" } } } }, - "downloads.refused.title" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Download nicht möglich" + "episode.short %lld %lld": { + "comment": "Staffel- und Folgennummer", + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "S%lld E%lld" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Can't download" + "en": { + "stringUnit": { + "state": "translated", + "value": "S%lld E%lld" } } } }, - "downloads.removeConfirm" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Download entfernen?" + "error.noServers": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "An deinem Plex-Konto hängt kein Server." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Remove download?" + "en": { + "stringUnit": { + "state": "translated", + "value": "There's no server on your Plex account." } } } }, - "downloads.resume" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Fortsetzen" + "error.notFound": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Dieser Titel ist auf dem Server nicht mehr vorhanden." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Resume" + "en": { + "stringUnit": { + "state": "translated", + "value": "This title is no longer on the server." } } } }, - "downloads.season" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Staffel laden" + "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" : "Download season" + "en": { + "stringUnit": { + "state": "translated", + "value": "The server responded with error %lld." } } } }, - "downloads.used" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Belegt" + "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" : "Used" + "en": { + "stringUnit": { + "state": "translated", + "value": "The server can't be reached. Check that it's running and that you're on the same network." } } } }, - "downloads.wifiOnly" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nur über WLAN laden" + "error.title": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Das hat nicht geklappt" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Download over Wi-Fi only" + "en": { + "stringUnit": { + "state": "translated", + "value": "Something went wrong" } } } }, - "episode.short %lld %lld" : { - "comment" : "Staffel- und Folgennummer", - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "S%lld E%lld" + "error.unauthorized": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Die Anmeldung ist abgelaufen. Melde dich erneut an." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "S%lld E%lld" + "en": { + "stringUnit": { + "state": "translated", + "value": "Your sign-in has expired. Please sign in again." } } } }, - "error.noServers" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "An deinem Plex-Konto hängt kein Server." + "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" : "There's no server on your Plex account." + "en": { + "stringUnit": { + "state": "translated", + "value": "The server's response was unexpected. It may be running a newer version of Plex." } } } }, - "error.notFound" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Dieser Titel ist auf dem Server nicht mehr vorhanden." + "filter.any": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Alle" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "This title is no longer on the server." + "en": { + "stringUnit": { + "state": "translated", + "value": "Any" } } } }, - "error.server %lld" : { - "comment" : "Platzhalter: HTTP-Statuscode", - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Der Server hat mit Fehler %lld geantwortet." + "filter.filters": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Filter" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "The server responded with error %lld." + "en": { + "stringUnit": { + "state": "translated", + "value": "Filters" } } } }, - "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." + "filter.genre": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Genre" } }, - "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": "Genre" } } } }, - "error.title" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Das hat nicht geklappt" + "filter.reset": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Zurücksetzen" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Something went wrong" + "en": { + "stringUnit": { + "state": "translated", + "value": "Reset" } } } }, - "error.unauthorized" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Die Anmeldung ist abgelaufen. Melde dich erneut an." + "filter.resolution": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Auflösung" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Your sign-in has expired. Please sign in again." + "en": { + "stringUnit": { + "state": "translated", + "value": "Resolution" } } } }, - "error.unexpectedResponse" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Die Antwort des Servers war unerwartet. Möglicherweise ist eine neuere Plex-Version installiert." + "filter.sort": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Sortierung" } }, - "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": "Sort by" } } } }, - "filter.any" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Alle" + "filter.title": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Filter" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Any" + "en": { + "stringUnit": { + "state": "translated", + "value": "Filters" } } } }, - "filter.filters" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Filter" + "filter.unwatchedOnly": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Nur Ungesehene" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Filters" + "en": { + "stringUnit": { + "state": "translated", + "value": "Unwatched only" } } } }, - "filter.genre" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Genre" + "hero.details": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Details" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Genre" + "en": { + "stringUnit": { + "state": "translated", + "value": "Details" } } } }, - "filter.reset" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Zurücksetzen" + "hero.play": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Abspielen" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Reset" + "en": { + "stringUnit": { + "state": "translated", + "value": "Play" } } } }, - "filter.resolution" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Auflösung" + "hero.resume": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Weiterschauen" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Resolution" + "en": { + "stringUnit": { + "state": "translated", + "value": "Resume" } } } }, - "filter.sort" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sortierung" + "home.empty.description": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Sobald dein Server Mediatheken enthält, erscheinen sie hier." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sort by" + "en": { + "stringUnit": { + "state": "translated", + "value": "Once your server has libraries, they'll show up here." } } } }, - "filter.title" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Filter" + "home.empty.title": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Noch nichts da" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Filters" + "en": { + "stringUnit": { + "state": "translated", + "value": "Nothing here yet" } } } }, - "filter.unwatchedOnly" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nur Ungesehene" + "homeLayout.empty": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Der Server liefert derzeit keine Reihen." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Unwatched only" + "en": { + "stringUnit": { + "state": "translated", + "value": "The server isn't providing any rows right now." } } } }, - "hero.details" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Details" + "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" : "Details" + "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." } } } }, - "hero.play" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Abspielen" + "homeLayout.title": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Startseite" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Play" + "en": { + "stringUnit": { + "state": "translated", + "value": "Home screen" } } } }, - "hero.resume" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Weiterschauen" + "item.watched": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Gesehen" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Resume" + "en": { + "stringUnit": { + "state": "translated", + "value": "Watched" } } } }, - "home.empty.description" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sobald dein Server Mediatheken enthält, erscheinen sie hier." + "library.clearFilters": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Filter zurücksetzen" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Once your server has libraries, they'll show up here." + "en": { + "stringUnit": { + "state": "translated", + "value": "Clear filters" } } } }, - "home.empty.title" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Noch nichts da" + "library.empty.description": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Mit den aktuellen Filtern bleibt nichts übrig." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nothing here yet" + "en": { + "stringUnit": { + "state": "translated", + "value": "Nothing matches the current filters." } } } }, - "homeLayout.empty" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Der Server liefert derzeit keine Reihen." + "library.empty.title": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Keine Titel" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "The server isn't providing any rows right now." + "en": { + "stringUnit": { + "state": "translated", + "value": "No titles" } } } }, - "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." + "library.filters": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Filter" } }, - "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": "Filters" } } } }, - "homeLayout.title" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Startseite" + "library.scan": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Mediathek durchsuchen" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Home screen" + "en": { + "stringUnit": { + "state": "translated", + "value": "Scan library" } } } }, - "item.watched" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Gesehen" + "library.toggleLayout": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Ansicht wechseln" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Watched" + "en": { + "stringUnit": { + "state": "translated", + "value": "Switch layout" } } } }, - "library.clearFilters" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Filter zurücksetzen" + "liveTV.channelCount %lld": { + "comment": "Platzhalter: Anzahl Sender", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "%lld Sender" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Clear filters" + "en": { + "stringUnit": { + "state": "translated", + "value": "%lld channels" } } } }, - "library.empty.description" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mit den aktuellen Filtern bleibt nichts übrig." + "liveTV.gap": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Keine Angaben" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nothing matches the current filters." + "en": { + "stringUnit": { + "state": "translated", + "value": "No data" } } } }, - "library.empty.title" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Keine Titel" + "liveTV.mode": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Ansicht" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "No titles" + "en": { + "stringUnit": { + "state": "translated", + "value": "View" } } } }, - "library.filters" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Filter" + "liveTV.mode.guide": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Programm" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Filters" + "en": { + "stringUnit": { + "state": "translated", + "value": "Guide" } } } }, - "library.scan" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mediathek durchsuchen" + "liveTV.mode.now": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Jetzt" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Scan library" + "en": { + "stringUnit": { + "state": "translated", + "value": "Now" } } } }, - "library.toggleLayout" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ansicht wechseln" + "liveTV.next %@ %@": { + "comment": "Platzhalter: Uhrzeit, Sendungstitel", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Danach %@ · %@" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Switch layout" + "en": { + "stringUnit": { + "state": "translated", + "value": "Next %@ · %@" } } } }, - "liveTV.next %@ %@" : { - "comment" : "Platzhalter: Uhrzeit, Sendungstitel", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Danach %@ · %@" + "liveTV.noChannels.description": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Der Empfänger ist eingerichtet, liefert aber keine Sender." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Next %@ · %@" + "en": { + "stringUnit": { + "state": "translated", + "value": "The tuner is set up but isn't providing any channels." } } } }, - "liveTV.noChannels.description" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Der Empfänger ist eingerichtet, liefert aber keine Sender." + "liveTV.noChannels.title": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Keine Sender" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "The tuner is set up but isn't providing any channels." + "en": { + "stringUnit": { + "state": "translated", + "value": "No channels" } } } }, - "liveTV.noChannels.title" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Keine Sender" + "liveTV.noProgram": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Kein Programm verfügbar" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "No channels" + "en": { + "stringUnit": { + "state": "translated", + "value": "No guide data" } } } }, - "liveTV.noProgram" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Kein Programm verfügbar" + "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" : "No guide data" + "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.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.title": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Kein Empfänger eingerichtet" } }, - "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": "No tuner set up" } } } }, - "liveTV.notConfigured.title" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Kein Empfänger eingerichtet" + "liveTV.now": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Jetzt" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "No tuner set up" + "en": { + "stringUnit": { + "state": "translated", + "value": "Now" } } } }, - "placeholder.description" : { - "comment" : "Platzhalter-Bildschirme", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Dieser Bereich entsteht in einer späteren Phase." + "liveTV.runningNow": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Läuft gerade" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "This area is coming in a later phase." + "en": { + "stringUnit": { + "state": "translated", + "value": "On now" } } } }, - "player.audioTracks" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tonspur" + "liveTV.timeRange %@ %@": { + "comment": "Platzhalter: Anfangs- und Endzeit", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "%@ – %@" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Audio" + "en": { + "stringUnit": { + "state": "translated", + "value": "%@ – %@" } } } }, - "player.back10" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "10 Sekunden zurück" + "liveTV.tune": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Einschalten" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Back 10 seconds" + "en": { + "stringUnit": { + "state": "translated", + "value": "Watch" } } } }, - "player.close" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Player schließen" + "placeholder.description": { + "comment": "Platzhalter-Bildschirme", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Dieser Bereich entsteht in einer späteren Phase." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Close player" + "en": { + "stringUnit": { + "state": "translated", + "value": "This area is coming in a later phase." } } } }, - "player.forward10" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "10 Sekunden vor" + "player.audioTracks": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Tonspur" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Forward 10 seconds" + "en": { + "stringUnit": { + "state": "translated", + "value": "Audio" } } } }, - "player.loadFailed" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Die Datei ließ sich nicht öffnen." + "player.back10": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "10 Sekunden zurück" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "The file couldn't be opened." + "en": { + "stringUnit": { + "state": "translated", + "value": "Back 10 seconds" } } } }, - "player.nextEpisode" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nächste Folge" + "player.close": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Player schließen" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Next episode" + "en": { + "stringUnit": { + "state": "translated", + "value": "Close player" } } } }, - "player.noPlayableFile" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Zu diesem Titel gibt es keine abspielbare Datei auf dem Server." + "player.forward10": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "10 Sekunden vor" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "There's no playable file for this title on the server." + "en": { + "stringUnit": { + "state": "translated", + "value": "Forward 10 seconds" } } } }, - "player.pause" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Pause" + "player.loadFailed": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Die Datei ließ sich nicht öffnen." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Pause" + "en": { + "stringUnit": { + "state": "translated", + "value": "The file couldn't be opened." } } } }, - "player.play" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Abspielen" + "player.nextEpisode": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Nächste Folge" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Play" + "en": { + "stringUnit": { + "state": "translated", + "value": "Next episode" } } } }, - "player.playbackFailed" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Die Wiedergabe wurde unterbrochen." + "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" : "Playback was interrupted." + "en": { + "stringUnit": { + "state": "translated", + "value": "There's no playable file for this title on the server." } } } }, - "player.preparing" : { - "comment" : "Hinweis, während der Player die Datei öffnet", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Wird vorbereitet …" + "player.pause": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Pause" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Preparing…" + "en": { + "stringUnit": { + "state": "translated", + "value": "Pause" } } } }, - "player.quality" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Qualität" + "player.play": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Abspielen" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Quality" + "en": { + "stringUnit": { + "state": "translated", + "value": "Play" } } } }, - "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.playbackFailed": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Die Wiedergabe wurde unterbrochen." } }, - "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": "Playback was interrupted." } } } }, - "player.skipCredits" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Abspann überspringen" + "player.preparing": { + "comment": "Hinweis, während der Player die Datei öffnet", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Wird vorbereitet …" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Skip credits" + "en": { + "stringUnit": { + "state": "translated", + "value": "Preparing…" } } } }, - "player.skipIntro" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Intro überspringen" + "player.quality": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Qualität" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Skip intro" + "en": { + "stringUnit": { + "state": "translated", + "value": "Quality" } } } }, - "player.subtitlesOff" : { - "comment" : "Eintrag, der die Untertitel abschaltet", - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Aus" + "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" : "Off" + "en": { + "stringUnit": { + "state": "translated", + "value": "You're connected through the Plex relay, which is heavily throttled. Large files barely start over it." } } } }, - "player.subtitleTracks" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Untertitel" + "player.skipCredits": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Abspann überspringen" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Subtitles" + "en": { + "stringUnit": { + "state": "translated", + "value": "Skip credits" } } } }, - "player.tracks" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ton und Untertitel" + "player.skipIntro": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Intro überspringen" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Audio and subtitles" + "en": { + "stringUnit": { + "state": "translated", + "value": "Skip intro" } } } }, - "player.transcodeRefused" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Der Server hat die Wiedergabe abgelehnt." + "player.subtitleTracks": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Untertitel" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "The server refused playback." + "en": { + "stringUnit": { + "state": "translated", + "value": "Subtitles" } } } }, - "quality.automatic" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Automatisch" + "player.subtitlesOff": { + "comment": "Eintrag, der die Untertitel abschaltet", + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Aus" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Automatic" + "en": { + "stringUnit": { + "state": "translated", + "value": "Off" } } } }, - "quality.fullHD" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Full HD · 1080p" + "player.tracks": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Ton und Untertitel" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Full HD · 1080p" + "en": { + "stringUnit": { + "state": "translated", + "value": "Audio and subtitles" } } } }, - "quality.hd" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "HD · 720p" + "player.transcodeRefused": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Der Server hat die Wiedergabe abgelehnt." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "HD · 720p" + "en": { + "stringUnit": { + "state": "translated", + "value": "The server refused playback." } } } }, - "quality.high" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Hoch (12 Mbit/s)" + "quality.automatic": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Automatisch" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "High (12 Mbps)" + "en": { + "stringUnit": { + "state": "translated", + "value": "Automatic" } } } }, - "quality.low" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Niedrig (4 Mbit/s)" + "quality.fullHD": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Full HD · 1080p" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Low (4 Mbps)" + "en": { + "stringUnit": { + "state": "translated", + "value": "Full HD · 1080p" } } } }, - "quality.lowRes" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sparsam · 480p" + "quality.hd": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "HD · 720p" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Data saver · 480p" + "en": { + "stringUnit": { + "state": "translated", + "value": "HD · 720p" } } } }, - "quality.medium" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mittel (8 Mbit/s)" + "quality.high": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Hoch (12 Mbit/s)" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Medium (8 Mbps)" + "en": { + "stringUnit": { + "state": "translated", + "value": "High (12 Mbps)" } } } }, - "quality.minimal" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sparsam (2 Mbit/s)" + "quality.low": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Niedrig (4 Mbit/s)" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Data saver (2 Mbps)" + "en": { + "stringUnit": { + "state": "translated", + "value": "Low (4 Mbps)" } } } }, - "quality.original" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Original" + "quality.lowRes": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Sparsam · 480p" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Original" + "en": { + "stringUnit": { + "state": "translated", + "value": "Data saver · 480p" } } } }, - "runtime.hoursMinutes %lld %lld" : { - "comment" : "Laufzeit mit Stunden", - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lld Std %lld Min" + "quality.medium": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Mittel (8 Mbit/s)" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lldh %lldm" + "en": { + "stringUnit": { + "state": "translated", + "value": "Medium (8 Mbps)" } } } }, - "runtime.minutes %lld" : { - "comment" : "Laufzeit unter einer Stunde", - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lld Min" + "quality.minimal": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Sparsam (2 Mbit/s)" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lldm" + "en": { + "stringUnit": { + "state": "translated", + "value": "Data saver (2 Mbps)" } } } }, - "SD" : { - "comment" : "A label for a 720p movie.", - "isCommentAutoGenerated" : true + "quality.original": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Original" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "Original" + } + } + } + }, + "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" + } + } + } + }, + "runtime.minutes %lld": { + "comment": "Laufzeit unter einer Stunde", + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "%lld Min" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "%lldm" + } + } + } }, - "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.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" } } } }, - "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.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.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.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.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" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mediatheken" + "settings.libraries": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Mediatheken" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Libraries" + "en": { + "stringUnit": { + "state": "translated", + "value": "Libraries" } } } }, - "settings.playback" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Wiedergabe" + "settings.playback": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Wiedergabe" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Playback" + "en": { + "stringUnit": { + "state": "translated", + "value": "Playback" } } } }, - "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" : "Wird eine Datei größer als die eingestellte Grenze, rechnet der Server sie unterwegs herunter und passt die Bitrate laufend an die Verbindung an." + "settings.qualityExplanation": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Wird eine Datei größer als die eingestellte Grenze, rechnet der Server sie unterwegs herunter und passt die Bitrate laufend an die Verbindung an." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "If a file exceeds the limit, the server transcodes it on the fly and adapts the bitrate to your connection." + "en": { + "stringUnit": { + "state": "translated", + "value": "If a file exceeds the limit, the server transcodes it on the fly and adapts the bitrate to your connection." } } } }, - "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.server" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Server" + "settings.server": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Server" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Server" + "en": { + "stringUnit": { + "state": "translated", + "value": "Server" } } } }, - "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.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", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Seitenleiste schließen" + "sidebar.close": { + "comment": "Barrierefreiheit: abgedunkelte Fläche", + "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.home" : { - "comment" : "Eintrag der Startseite in der Seitenleiste", - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Start" + "sidebar.home": { + "comment": "Eintrag der Startseite in der Seitenleiste", + "extractionState": "stale", + "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", - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mediatheken" + "sidebar.libraries": { + "comment": "Überschrift über der Liste der Mediatheken", + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Mediatheken" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Libraries" + "en": { + "stringUnit": { + "state": "translated", + "value": "Libraries" } } } }, - "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.open" : { - "comment" : "Barrierefreiheit: Hamburger-Knopf", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Seitenleiste öffnen" + "sidebar.open": { + "comment": "Barrierefreiheit: Hamburger-Knopf", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Seitenleiste öffnen" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Open sidebar" + "en": { + "stringUnit": { + "state": "translated", + "value": "Open sidebar" } } } }, - "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" } } } }, - "signIn.action" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mit Plex anmelden" + "signIn.action": { + "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" } } } }, - "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.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 Anmeldemaske kommt von Plex. Dein Passwort erreicht diese App nie." + "signIn.privacy": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Die Anmeldemaske kommt von Plex. Dein Passwort erreicht diese App nie." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "The sign-in page comes from Plex. Your password never reaches this app." + "en": { + "stringUnit": { + "state": "translated", + "value": "The sign-in page comes from Plex. Your password never reaches this app." } } } }, - "signIn.tagline" : { - "comment" : "Untertitel auf dem Anmeldebildschirm", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Deine Plex-Mediathek, schöner." + "signIn.tagline": { + "comment": "Untertitel auf dem Anmeldebildschirm", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Deine Plex-Mediathek, schöner." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Your Plex library, nicer." + "en": { + "stringUnit": { + "state": "translated", + "value": "Your Plex 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." } } } }, - "sort.lastViewed" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Zuletzt gesehen" + "sort.lastViewed": { + "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)" } } } }, - "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.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.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" } } } } }, - "version" : "1.1" + "version": "1.1" } \ No newline at end of file diff --git a/Flixy/Views/LiveTV/LiveTVView.swift b/Flixy/Views/LiveTV/LiveTVView.swift index ab4d836..df1be63 100644 --- a/Flixy/Views/LiveTV/LiveTVView.swift +++ b/Flixy/Views/LiveTV/LiveTVView.swift @@ -16,6 +16,12 @@ struct LiveTVView: View { @State private var channelSource: String? @State private var programs: [String: [PlexProgram]] = [:] @State private var isTuning: String? + /// Raster oder Senderliste. + /// + /// Auf dem Handy sind beide Ansichten berechtigt: das Raster zum Stöbern, + /// die Liste zum schnellen Umschalten. Die Wahl bleibt erhalten, weil + /// niemand sie zweimal treffen will. + @AppStorage("liveTV.showsGuide") private var showsGuide = true var body: some View { Group { @@ -65,17 +71,38 @@ struct LiveTVView: View { } case .available: - guide + if showsGuide { + ProgramGuide( + channels: channels, + programs: programs, + tuning: isTuning, + onTune: { channel in Task { await tune(channel) } } + ) + } else { + channelList + } } } .background(Color.flixyCanvas) .navigationTitle(Text("sidebar.liveTV")) .navigationBarTitleDisplayMode(.inline) + .toolbar { + if case .available = availability, !channels.isEmpty { + ToolbarItem(placement: .topBarTrailing) { + Picker("liveTV.mode", selection: $showsGuide) { + Image(systemName: "calendar.day.timeline.left").tag(true) + Image(systemName: "list.bullet").tag(false) + } + .pickerStyle(.segmented) + .labelsHidden() + } + } + } .task { await load() } .refreshable { await load() } } - private var guide: some View { + private var channelList: some View { ScrollView { LazyVStack(spacing: 0) { ForEach(channels) { channel in @@ -99,26 +126,18 @@ struct LiveTVView: View { guard case .available(let dvrs) = availability, let dvr = dvrs.first else { return } - let lookup = await server.channels(inDVR: dvr) - channels = lookup.channels - channelSource = lookup.description - - // Der Programmführer kommt getrennt: er kann ausfallen, ohne dass - // deshalb das Umschalten unmöglich wäre. Eine Senderliste ohne - // Programm ist immer noch nützlich. - guard let provider = await server.electronicProgramGuideIdentifier() else { return } - let now = Date() - let entries = (try? await server.guide( - providerIdentifier: provider, - from: now.addingTimeInterval(-3_600), - to: now.addingTimeInterval(6 * 3_600) - )) ?? [] - - programs = Dictionary(grouping: entries.compactMap { program in - program.channelID.map { (key: $0, program: program) } - }, by: \.key).mapValues { pairs in - pairs.map(\.program).sorted { $0.startsAt < $1.startsAt } - } + // Sender und Programm kommen zusammen: die Senderliste des Anbieters + // antwortet auf manchen Servern gar nicht, und dann tragen die + // Rastereinträge die Sender selbst. Getrennt geholt passten die + // Kennungen zudem nicht zueinander, und das Programm landete in + // keiner Zeile. + // Sechs Stunden: ein ganzer Fernsehabend. Mehr wäre bei einer Liste + // mit fünfhundert Sendern eine Antwort im zweistelligen Megabereich, + // und über Mobilfunk wartet man darauf. + let guide = await server.liveTVGuide(dvr: dvr, hours: 6) + channels = guide.channels + programs = guide.programs + channelSource = guide.source } private func tune(_ channel: PlexChannel) async { diff --git a/Flixy/Views/LiveTV/ProgramGuide.swift b/Flixy/Views/LiveTV/ProgramGuide.swift new file mode 100644 index 0000000..a5e80a3 --- /dev/null +++ b/Flixy/Views/LiveTV/ProgramGuide.swift @@ -0,0 +1,332 @@ +import SwiftUI +import FlixyDesign +import PlexKit + +/// Das Programmraster: Sender als Zeilen, Zeit als Achse. +/// +/// Warum ein Raster und nicht eine Liste mit „läuft gerade": beim Fernsehen +/// sucht man nicht einen Sender, sondern eine Sendung — und die findet man +/// nur, wenn man sieht, was auf allen Sendern gleichzeitig läuft. Genau das +/// ist die Achse. +/// +/// Der Aufbau: die Senderspalte steht **außerhalb** des waagerechten +/// Rollbereichs und bleibt deshalb stehen, während das Programm daran +/// vorbeizieht. Die Zeitachse steht außerhalb des senkrechten und folgt dem +/// waagerechten Versatz von Hand — anders bekommt man zwei Achsen nicht +/// gleichzeitig festgenagelt. +struct ProgramGuide: View { + let channels: [PlexChannel] + let programs: [String: [PlexProgram]] + let tuning: String? + let onTune: (PlexChannel) -> Void + + @Environment(FlixySession.self) private var session + @Environment(\.flixyAccent) private var accent + + @State private var scrollX: CGFloat = 0 + @State private var selection: GuideSelection? + @State private var now = Date() + + private let channelWidth: CGFloat = 116 + private let rowHeight: CGFloat = 64 + /// Eine halbe Stunde ist gut 130 Punkte breit — schmal genug, dass ein + /// Abend auf den Schirm passt, breit genug für einen lesbaren Titel. + private let pointsPerMinute: CGFloat = 4.4 + private let axisHeight: CGFloat = 26 + + var body: some View { + VStack(spacing: 0) { + axis + Divider().overlay(Color.flixySeparator) + + ScrollView(.vertical) { + HStack(alignment: .top, spacing: 0) { + channelColumn + lanes + } + } + .scrollIndicators(.hidden) + } + .background(Color.flixyCanvas) + .sheet(item: $selection) { selection in + ProgramDetailSheet( + channel: selection.channel, + program: selection.program, + isRunning: selection.program.isRunning(at: now), + onTune: { onTune(selection.channel) } + ) + } + // Die Fortschrittsbalken und die Jetzt-Linie stimmen sonst nach ein + // paar Minuten nicht mehr. Einmal pro Minute reicht dafür. + .task { + while !Task.isCancelled { + try? await Task.sleep(for: .seconds(60)) + now = Date() + } + } + } + + // MARK: - Zeitfenster + + /// Zeit in Punkte. Die Rechnungen dahinter stehen in `GuideLayout` und + /// sind dort geprüft — hier wären sie unsichtbar falsch. + private var layout: GuideLayout { + GuideLayout( + now: now, + latestEnd: programs.values.flatMap { $0 }.map(\.endsAt).max(), + pointsPerMinute: pointsPerMinute + ) + } + + // MARK: - Teile + + private var axis: some View { + HStack(spacing: 0) { + Text("liveTV.channelCount \(channels.count)") + .font(.flixyCaption) + .foregroundStyle(Color.flixyTextMuted) + .frame(width: channelWidth, alignment: .leading) + .padding(.leading, FlixySpacing.snug) + + HStack(spacing: 0) { + ForEach(layout.ticks, id: \.self) { tick in + Text(tick.formatted(date: .omitted, time: .shortened)) + .font(.system(.caption2, design: .rounded)) + .foregroundStyle(Color.flixyTextMuted) + .frame(width: layout.tickWidth, alignment: .leading) + } + } + .frame(width: layout.totalWidth, alignment: .leading) + .offset(x: -scrollX) + .frame(maxWidth: .infinity, alignment: .leading) + .clipped() + } + .frame(height: axisHeight) + .background(Color.flixyCanvas) + } + + private var channelColumn: some View { + VStack(spacing: 0) { + ForEach(channels) { channel in + Button { onTune(channel) } label: { + HStack(spacing: FlixySpacing.snug) { + FlixyArtwork( + url: session.server?.artworkURL(path: channel.thumb, width: 120, height: 120), + aspectRatio: 1, + fallbackSymbol: "tv" + ) + .frame(width: 30, height: 30) + .clipShape(.rect(cornerRadius: 4)) + + VStack(alignment: .leading, spacing: 1) { + Text(channel.title) + .font(.flixyCaption) + .foregroundStyle(Color.flixyTextPrimary) + .lineLimit(2) + if let number = channel.channelIdentifier { + Text(number) + .font(.system(.caption2, design: .monospaced)) + .foregroundStyle(Color.flixyTextMuted) + } + } + + Spacer(minLength: 0) + + if tuning == channel.id { + ProgressView().controlSize(.mini) + } + } + .padding(.horizontal, FlixySpacing.snug) + .frame(width: channelWidth, height: rowHeight, alignment: .leading) + .overlay(alignment: .bottom) { rowSeparator } + .contentShape(.rect) + } + .buttonStyle(.plain) + } + } + .frame(width: channelWidth) + .background(Color.flixyElevated.opacity(0.4)) + } + + private var lanes: some View { + ScrollView(.horizontal) { + ZStack(alignment: .topLeading) { + VStack(spacing: 0) { + ForEach(channels) { channel in + lane(for: channel) + } + } + + // Die Jetzt-Linie läuft mit dem Inhalt mit, nicht mit dem + // Blickfeld — sie markiert einen Zeitpunkt, keine Stelle auf + // dem Schirm. + Rectangle() + .fill(accent) + .frame(width: 2) + .offset(x: layout.x(for: now)) + .allowsHitTesting(false) + } + .frame(width: layout.totalWidth, alignment: .topLeading) + } + .scrollIndicators(.hidden) + .frame(height: CGFloat(channels.count) * rowHeight) + .onScrollGeometryChange(for: CGFloat.self) { geometry in + geometry.contentOffset.x + } action: { _, offset in + scrollX = offset + } + } + + private func lane(for channel: PlexChannel) -> some View { + let entries = programs[channel.id] ?? [] + + return ZStack(alignment: .topLeading) { + if entries.isEmpty { + Text("liveTV.noProgram") + .font(.flixyCaption) + .foregroundStyle(Color.flixyTextMuted) + .padding(.horizontal, FlixySpacing.regular) + .frame(height: rowHeight, alignment: .leading) + } + + ForEach(entries) { program in + block(program, on: channel) + .offset(x: layout.x(for: program.startsAt)) + } + } + .frame(width: layout.totalWidth, height: rowHeight, alignment: .topLeading) + .overlay(alignment: .bottom) { rowSeparator } + } + + /// Die Trennlinie gehört in die Zeile, nicht zwischen die Zeilen. + /// + /// `Divider()` ist auf einem 3x-Schirm keinen ganzen Punkt hoch. Zwischen + /// den Zeilen gestapelt summiert sich dieser Bruchteil, und Senderspalte + /// und Programm stehen nach zwanzig Sendern sichtbar versetzt — die + /// Zeile, die man antippt, gehört dann zum Nachbarsender. + private var rowSeparator: some View { + Rectangle() + .fill(Color.flixySeparator) + .frame(height: 1) + } + + private func block(_ program: PlexProgram, on channel: PlexChannel) -> some View { + let running = program.isRunning(at: now) + + return Button { selection = GuideSelection(channel: channel, program: program) } label: { + VStack(alignment: .leading, spacing: 2) { + Text(program.title) + .font(.flixyCaption) + .foregroundStyle(Color.flixyTextPrimary) + .lineLimit(2) + .multilineTextAlignment(.leading) + + Text(program.startsAt.formatted(date: .omitted, time: .shortened)) + .font(.system(.caption2, design: .rounded)) + .foregroundStyle(Color.flixyTextMuted) + } + .padding(.horizontal, FlixySpacing.snug) + .padding(.vertical, 6) + .frame(width: layout.width(from: program.startsAt, to: program.endsAt) - 2, height: rowHeight - 2, alignment: .topLeading) + .background( + running ? accent.opacity(0.28) : Color.flixyElevated, + in: .rect(cornerRadius: FlixyRadius.control) + ) + .overlay(alignment: .bottomLeading) { + // Der Balken zeigt, wie weit die laufende Sendung ist — beim + // Umschalten die nützlichste Angabe überhaupt. + if running { + WatchProgressBar(fraction: program.progress(at: now)) + .padding(.horizontal, 6) + .padding(.bottom, 5) + } + } + .contentShape(.rect) + } + .buttonStyle(.plain) + } +} + +/// Die angetippte Sendung samt ihrem Sender. +private struct GuideSelection: Identifiable { + let channel: PlexChannel + let program: PlexProgram + + var id: String { "\(channel.id)-\(program.id)" } +} + +/// Was zu einer Sendung im Raster gehört, in voller Länge. +private struct ProgramDetailSheet: View { + let channel: PlexChannel + let program: PlexProgram + let isRunning: Bool + let onTune: () -> Void + + @Environment(FlixySession.self) private var session + @Environment(\.flixyAccent) private var accent + @Environment(\.dismiss) private var dismiss + + var body: some View { + NavigationStack { + ScrollView { + VStack(alignment: .leading, spacing: FlixySpacing.regular) { + HStack(spacing: FlixySpacing.regular) { + FlixyArtwork( + url: session.server?.artworkURL(path: channel.thumb, width: 160, height: 160), + aspectRatio: 1, + fallbackSymbol: "tv" + ) + .frame(width: 44, height: 44) + .clipShape(.rect(cornerRadius: 6)) + + VStack(alignment: .leading, spacing: 2) { + Text(channel.title) + .font(.flixyCardTitle) + .foregroundStyle(Color.flixyTextPrimary) + Text("liveTV.timeRange \(program.startsAt.formatted(date: .omitted, time: .shortened)) \(program.endsAt.formatted(date: .omitted, time: .shortened))") + .font(.flixyCaption) + .foregroundStyle(Color.flixyTextSecondary) + } + + Spacer() + } + + Text(program.title) + .font(.flixyRowHeader) + .foregroundStyle(Color.flixyTextPrimary) + + if let subtitle = program.subtitle { + Text(subtitle) + .font(.flixyBody) + .foregroundStyle(Color.flixyTextSecondary) + } + + if isRunning { + Button { onTune(); dismiss() } label: { + Label("liveTV.tune", systemImage: "play.fill") + .frame(maxWidth: .infinity) + .padding(.vertical, FlixySpacing.snug) + } + .buttonStyle(.borderedProminent) + .tint(accent) + } + + if let summary = program.summary, !summary.isEmpty { + Text(summary) + .font(.flixyBody) + .foregroundStyle(Color.flixyTextSecondary) + } + } + .padding(FlixySpacing.screenMargin) + .frame(maxWidth: .infinity, alignment: .leading) + } + .background(Color.flixyCanvas) + .toolbar { + ToolbarItem(placement: .confirmationAction) { + Button("action.close") { dismiss() } + } + } + } + .presentationDetents([.medium, .large]) + } +} diff --git a/Packages/FlixyKit/Sources/FlixyDesign/GuideLayout.swift b/Packages/FlixyKit/Sources/FlixyDesign/GuideLayout.swift new file mode 100644 index 0000000..5924bc9 --- /dev/null +++ b/Packages/FlixyKit/Sources/FlixyDesign/GuideLayout.swift @@ -0,0 +1,78 @@ +import Foundation +import CoreGraphics + +/// Die Geometrie eines Programmrasters: Zeit in Punkte. +/// +/// Eigener Typ statt Rechnungen in der Ansicht, weil hier alles schiefgehen +/// kann, was man nicht sieht: eine Sendung, die vor dem Fenster begann, säße +/// bei negativem Versatz außerhalb; eine Kurzmeldung wäre zwei Punkte breit +/// und unlesbar; ein leeres Raster hätte die Breite null. Geprüft werden +/// diese Fälle nur, wenn sie außerhalb einer `View` stehen. +public struct GuideLayout: Sendable, Equatable { + /// Der Anfang des Fensters — die angebrochene halbe Stunde vor „jetzt". + /// + /// Nicht „jetzt" selbst: dann trüge die Achse krumme Zeiten und die + /// laufende Sendung wäre immer angeschnitten. + public let start: Date + public let end: Date + public let pointsPerMinute: CGFloat + /// Schmaler wird kein Block, egal wie kurz die Sendung ist. + public let minimumBlockWidth: CGFloat + + private static let halfHour: TimeInterval = 30 * 60 + + public init( + now: Date, + latestEnd: Date? = nil, + pointsPerMinute: CGFloat = 4.4, + minimumBlockWidth: CGFloat = 36, + minimumHours: Double = 3, + maximumHours: Double = 14 + ) { + let anchor = (now.timeIntervalSince1970 / Self.halfHour).rounded(.down) * Self.halfHour + let start = Date(timeIntervalSince1970: anchor) + self.start = start + // Unten begrenzt, damit ein Raster mit einer einzigen bekannten + // Sendung nicht zur Briefmarke wird; oben, damit es bei einem + // Anbieter mit zwei Wochen Vorschau nicht endlos wird. + let bounded = min( + max(latestEnd ?? start, start.addingTimeInterval(minimumHours * 3_600)), + start.addingTimeInterval(maximumHours * 3_600) + ) + // Auch das Ende auf die halbe Stunde, nach oben. Sonst endet die + // Achse mit einem Stummel, dessen Beschriftung keine Spalte mehr hat. + self.end = Date( + timeIntervalSince1970: (bounded.timeIntervalSince1970 / Self.halfHour).rounded(.up) * Self.halfHour + ) + self.pointsPerMinute = pointsPerMinute + self.minimumBlockWidth = minimumBlockWidth + } + + public var totalWidth: CGFloat { + CGFloat(end.timeIntervalSince(start) / 60) * pointsPerMinute + } + + /// Der waagerechte Versatz eines Zeitpunkts. + /// + /// Nie negativ: was vor dem Fenster liegt, klebt an dessen linkem Rand. + public func x(for date: Date) -> CGFloat { + max(0, CGFloat(date.timeIntervalSince(start) / 60) * pointsPerMinute) + } + + /// Die Breite einer Sendung, auf das Fenster beschnitten. + public func width(from begin: Date, to finish: Date) -> CGFloat { + let visibleStart = max(begin, start) + let visibleEnd = min(finish, end) + let minutes = max(0, visibleEnd.timeIntervalSince(visibleStart) / 60) + return max(CGFloat(minutes) * pointsPerMinute, minimumBlockWidth) + } + + /// Die Beschriftungen der Zeitachse, halbstündlich. + public var ticks: [Date] { + stride(from: 0, to: end.timeIntervalSince(start), by: Self.halfHour) + .map { start.addingTimeInterval($0) } + } + + /// Wie breit eine Achsenbeschriftung ist. + public var tickWidth: CGFloat { CGFloat(Self.halfHour / 60) * pointsPerMinute } +} diff --git a/Packages/FlixyKit/Sources/PlexKit/Endpoints/PlexServer+LiveTV.swift b/Packages/FlixyKit/Sources/PlexKit/Endpoints/PlexServer+LiveTV.swift index f903fbe..7f37b86 100644 --- a/Packages/FlixyKit/Sources/PlexKit/Endpoints/PlexServer+LiveTV.swift +++ b/Packages/FlixyKit/Sources/PlexKit/Endpoints/PlexServer+LiveTV.swift @@ -56,8 +56,14 @@ extension PlexServer { return ChannelLookup(channels: [], source: .noLineup) } + let advertised = await providerFeatures(for: epgIdentifier) + var lastFailure: String? - for path in Self.channelPaths(dvr: dvr, epgIdentifier: epgIdentifier) { + for path in Self.channelPaths( + dvr: dvr, + epgIdentifier: epgIdentifier, + advertised: advertised + ) { do { let container = try await client.send( PlexRequest(path: path, timeout: .seconds(30)), @@ -102,14 +108,41 @@ extension PlexServer { /// Entscheidend ist `pathIdentifier`: die Kennung der Senderliste kommt /// als vollständige Adresse mit Schema und Anhang. Roh eingesetzt ergibt /// jeder dieser Pfade 404. - static func channelPaths(dvr: PlexDVR, epgIdentifier: String) -> [String] { + static func channelPaths( + dvr: PlexDVR, + epgIdentifier: String, + advertised: [PlexProviderFeature] = [] + ) -> [String] { var paths: [String] = [] + + // Zuerst, was der Server selbst angibt. Die Namen der Fähigkeiten sind + // nicht dokumentiert, deshalb alles, was nach Sendern klingt. + for feature in advertised where ["channels", "lineups", "livetv", "grid"].contains(feature.type) { + if let key = feature.key { paths.append(key) } + } + + // Danach die geratenen Formen. Sie haben bei einem Server mit + // Swisscom-Liste alle 404 ergeben — sie stehen hier nur noch, weil + // andere Serverfassungen sie kennen könnten. if let lineup = dvr.lineups.first { paths.append("/\(epgIdentifier)/lineups/\(lineup.pathIdentifier)/channels") } paths.append("/\(epgIdentifier)/channels") paths.append("/livetv/dvrs/\(dvr.key)/channels") - return paths + + // Doppelte entfernen, Reihenfolge behalten. + var seen = Set() + return paths.filter { seen.insert($0).inserted } + } + + /// Was der Programmanbieter an Fähigkeiten angibt. + public func providerFeatures(for identifier: String) async -> [PlexProviderFeature] { + guard let container = try? await client.send( + PlexRequest(path: "/media/providers"), + as: PlexMediaContainer.self + ) else { return [] } + + return container.content.first { $0.identifier == identifier }?.features ?? [] } /// Die Kennung des Programmanbieters. @@ -126,7 +159,7 @@ extension PlexServer { return container.content.first { $0.identifier.contains("epg") }?.identifier } - /// Das Programmraster für einen Zeitraum. + /// Das Programmraster für einen Zeitraum, mit Sendern. /// /// Bewusst über einen Zeitraum statt „alles": ein Tag über hundert Sender /// sind mehrere tausend Sendungen, und mehr als die nächsten Stunden @@ -135,7 +168,7 @@ extension PlexServer { providerIdentifier: String, from start: Date, to end: Date - ) async throws(PlexError) -> [PlexProgram] { + ) async throws(PlexError) -> [PlexGuideEntry] { let container = try await client.send( PlexRequest( path: "/\(providerIdentifier)/grid", @@ -151,14 +184,73 @@ extension PlexServer { ) return container.content.compactMap { entry in - // Die Senderzuordnung steckt im Medien-Block. Fehlt sie, lässt - // sich die Sendung keiner Spalte zuordnen — dann ist sie für ein - // Raster wertlos. - let channelID = entry.media?.first?.parts.first?.key - return entry.program(channelID: channelID) + // Sender und Sendung stammen aus demselben Eintrag — deshalb + // passen die Kennungen zueinander. Getrennt geholt taten sie das + // nicht, und das Programm landete in keiner Spalte. + let channel = entry.channel + guard let program = entry.program(channelID: channel?.id) else { return nil } + return PlexGuideEntry(channel: channel, program: program) } } + /// Senderliste und Programm in einem Zug. + /// + /// Der Weg über die Senderliste des Anbieters ist der erste Versuch; wo + /// er greift, sind Logos und Nummern vollständiger. Wo er nicht greift — + /// und bei einem Server mit Swisscom-Liste tut er das nicht, alle + /// bekannten Adressen antworten mit 404 — tragen die Rastereinträge die + /// Sender selbst. Die Weboberfläche von Plex zeichnet ihren Guide genau + /// daraus. + public func liveTVGuide( + dvr: PlexDVR, + hours: Int = 6 + ) async -> LiveTVGuide { + let lookup = await channels(inDVR: dvr) + + var identifier = dvr.epgIdentifier + if identifier == nil { identifier = await electronicProgramGuideIdentifier() } + guard let epgIdentifier = identifier else { + return LiveTVGuide(channels: lookup.channels, programs: [:], source: lookup.description) + } + + let now = Date() + let entries = (try? await guide( + providerIdentifier: epgIdentifier, + from: now.addingTimeInterval(-3_600), + to: now.addingTimeInterval(TimeInterval(hours) * 3_600) + )) ?? [] + + let programs = Dictionary(grouping: entries.compactMap { entry in + entry.program.channelID.map { (key: $0, program: entry.program) } + }, by: \.key).mapValues { pairs in + pairs.map(\.program).sorted { $0.startsAt < $1.startsAt } + } + + if !lookup.channels.isEmpty { + return LiveTVGuide(channels: lookup.channels, programs: programs, source: lookup.description) + } + + // Aus dem Raster: jeden Sender einmal, in der Reihenfolge, in der das + // Raster ihn nennt. Plex sortiert dort bereits nach Sendernummer. + var seen = Set() + let fromGrid = entries.compactMap(\.channel).filter { seen.insert($0.id).inserted } + + let enabled = dvr.enabledChannelKeys + let filtered = enabled.isEmpty ? fromGrid : fromGrid.filter { $0.matches(anyOf: enabled) } + // Passt keiner zur Zuordnung, ist die ganze Liste besser als keine: + // die Zuordnung zu erzwingen hieße, bei abweichenden Schlüsseln alles + // wegzuwerfen, was der Server gerade geliefert hat. + let channels = filtered.isEmpty ? fromGrid : filtered + + return LiveTVGuide( + channels: channels, + programs: programs, + source: channels.isEmpty + ? "Raster: keine Sender (\(lookup.description))" + : "Raster: \(channels.count) von \(fromGrid.count) Sendern" + ) + } + /// Schaltet einen Sender ein. /// /// Live-TV kommt immer als umgerechneter Strom: der Tuner liefert einen @@ -202,6 +294,11 @@ extension PlexServer { var report = ["=== /livetv/dvrs ==="] report.append(await structure(ofPath: "/livetv/dvrs")) + // Was der Server an Endpunkten angibt. Nach drei geratenen Adressen + // mit 404 ist das die Stelle, an der die Antwort stehen muss. + report.append("\n=== /media/providers ===") + report.append(await structure(ofPath: "/media/providers")) + // Und die Senderliste beim Anbieter — dort stehen die Namen. Ohne sie // im selben Abzug bliebe nach einem Fehlschlag wieder eine Runde // Raten übrig. @@ -210,10 +307,29 @@ extension PlexServer { as: PlexMediaContainer.self ).content.first, let epgIdentifier = dvr.epgIdentifier { - for path in Self.channelPaths(dvr: dvr, epgIdentifier: epgIdentifier) { + let advertised = await providerFeatures(for: epgIdentifier) + for path in Self.channelPaths( + dvr: dvr, + epgIdentifier: epgIdentifier, + advertised: advertised + ) { report.append("\n=== \(path) ===") report.append(await structure(ofPath: path)) } + // Und das Raster: wenn keine Senderliste antwortet, muss die + // Antwort hier stehen — die Weboberfläche zeichnet ihren Guide + // daraus. + let now = Int(Date().timeIntervalSince1970) + let gridPath = "/\(epgIdentifier)/grid" + report.append("\n=== \(gridPath) ===") + report.append(await structure( + ofPath: gridPath, + query: [ + URLQueryItem(name: "type", value: "1"), + URLQueryItem(name: "beginsAt>", value: String(now - 3_600)), + URLQueryItem(name: "endsAt<", value: String(now + 3_600)), + ] + )) } else { report.append("\n=== Senderliste ===\nKeine Kennung des Programmanbieters am Empfänger") } @@ -221,10 +337,10 @@ extension PlexServer { return report.joined(separator: "\n") } - private func structure(ofPath path: String) async -> String { + private func structure(ofPath path: String, query: [URLQueryItem] = []) async -> String { let data: Data do { - data = try await client.send(PlexRequest(path: path, timeout: .seconds(30))) + data = try await client.send(PlexRequest(path: path, query: query, timeout: .seconds(30))) } catch { return "Abfrage gescheitert: \(error)" } @@ -245,7 +361,8 @@ extension PlexServer { case let dictionary as [String: Any]: return dictionary.keys.sorted().flatMap { key -> [String] in let child = dictionary[key]! - if ["title", "channelIdentifier", "lineupTitle"].contains(key), + if ["title", "channelIdentifier", "lineupTitle", "type", "key", "identifier", + "channelTitle", "channelCallSign", "channelVcn"].contains(key), let text = child as? String { return ["\(padding)\(key): \(text)"] } @@ -310,6 +427,65 @@ public struct PlexMediaProvider: Sendable, Equatable, Decodable, Identifiable { public let identifier: String public let title: String? public let types: String? + /// Was dieser Anbieter kann, und unter welcher Adresse. + /// + /// Der Grund, warum hier überhaupt etwas steht: Plex sagt selbst, welche + /// Endpunkte es anbietet. Drei Versuche, die Adresse der Senderliste zu + /// erraten, endeten in 404 — sie hier abzulesen ist kein Ratespiel mehr. + public let features: [PlexProviderFeature] public var id: String { identifier } + + private enum CodingKeys: String, CodingKey { + case identifier, title, types + case features = "Feature" + } + + public init(from decoder: any Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + self.identifier = try values.decode(String.self, forKey: .identifier) + self.title = try values.decodeIfPresent(String.self, forKey: .title) + self.types = try values.decodeIfPresent(String.self, forKey: .types) + self.features = try values.decodeIfPresent([PlexProviderFeature].self, forKey: .features) ?? [] + } + + public init(identifier: String, title: String? = nil, types: String? = nil, features: [PlexProviderFeature] = []) { + self.identifier = identifier + self.title = title + self.types = types + self.features = features + } + + /// Die Adresse für eine Fähigkeit, falls der Anbieter sie anbietet. + public func key(forFeature type: String) -> String? { + features.first { $0.type == type }?.key + } +} + +/// Eine Fähigkeit eines Anbieters, mit der Adresse dahinter. +public struct PlexProviderFeature: Sendable, Equatable, Decodable, Identifiable { + public let type: String + public let key: String? + + public var id: String { key ?? type } + + public init(type: String, key: String? = nil) { + self.type = type + self.key = key + } +} + +/// Ein Rastereintrag: eine Sendung und der Sender, auf dem sie läuft. +public struct PlexGuideEntry: Sendable { + public let channel: PlexChannel? + public let program: PlexProgram +} + +/// Senderliste und Programm, so wie die Live-TV-Ansicht sie braucht. +public struct LiveTVGuide: Sendable { + public let channels: [PlexChannel] + /// Sendungen je Sender, nach Anfangszeit sortiert. + public let programs: [String: [PlexProgram]] + /// Woher die Sender kamen — steht in der leeren Ansicht und in der Diagnose. + public let source: String } diff --git a/Packages/FlixyKit/Sources/PlexKit/Models/PlexLiveTV.swift b/Packages/FlixyKit/Sources/PlexKit/Models/PlexLiveTV.swift index a11f7ea..bb77311 100644 --- a/Packages/FlixyKit/Sources/PlexKit/Models/PlexLiveTV.swift +++ b/Packages/FlixyKit/Sources/PlexKit/Models/PlexLiveTV.swift @@ -167,6 +167,65 @@ public struct PlexTunerDevice: Sendable, Equatable, Decodable, Identifiable { } } +/// Der Medien-Block einer Sendung im Raster. +/// +/// Er trägt die Angaben zum Sender mit: das Raster ist bei Plex nicht nur +/// eine Liste von Sendungen, sondern die einzige Antwort, in der Sendername, +/// Nummer, Logo und Einschaltweg zusammen stehen. Drei geratene +/// Senderlisten-Adressen ergaben 404 — diese hier liefert die +/// Weboberfläche selbst aus. +struct PlexGridMedia: Decodable { + let channelIdentifier: String? + let channelTitle: String? + let channelCallSign: String? + let channelThumb: String? + let channelVcn: String? + let parts: [PlexGridPart] + + private enum CodingKeys: String, CodingKey { + case channelIdentifier, channelTitle, channelCallSign, channelThumb, channelVcn + case part = "Part" + } + + init(from decoder: any Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + self.channelIdentifier = try? values.decodeIfPresent(String.self, forKey: .channelIdentifier) + self.channelTitle = try values.decodeIfPresent(String.self, forKey: .channelTitle) + self.channelCallSign = try values.decodeIfPresent(String.self, forKey: .channelCallSign) + self.channelThumb = try values.decodeIfPresent(String.self, forKey: .channelThumb) + // Die Sendernummer kommt mal als Zahl, mal als Zeichenkette. + if let text = try? values.decodeIfPresent(String.self, forKey: .channelVcn) { + self.channelVcn = text + } else if let number = try? values.decodeIfPresent(Int.self, forKey: .channelVcn) { + self.channelVcn = String(number) + } else { + self.channelVcn = nil + } + self.parts = try values.decodeIfPresent([PlexGridPart].self, forKey: .part) ?? [] + } + + /// Der Sender, sofern dieser Block einen benennt. + /// + /// Ohne Namen kein Sender: eine Zeile, die nur „—" hieße, ist für die + /// Senderliste wertlos und stünde bei jedem Eintrag doppelt. + var channel: PlexChannel? { + guard let name = channelTitle ?? channelCallSign else { return nil } + let identity = channelIdentifier ?? parts.first?.key ?? name + return PlexChannel( + id: identity, + title: name, + channelIdentifier: channelVcn ?? channelIdentifier, + thumb: channelThumb, + key: parts.first?.key + ) + } +} + +/// Der Strom hinter einer Rastersendung. +struct PlexGridPart: Decodable { + let key: String? +} + /// Ein Sender. public struct PlexChannel: Sendable, Equatable, Decodable, Identifiable { /// Die Nummer, unter der der Sender läuft — etwa `"3.1"`. @@ -295,13 +354,19 @@ struct PlexGridEntry: Decodable { let endsAt: Int? /// Die Sendermedien; darin steht, zu welchem Sender die Sendung gehört. - let media: [PlexMedia]? + let media: [PlexGridMedia]? private enum CodingKeys: String, CodingKey { case ratingKey, title, summary, grandparentTitle, thumb, beginsAt, endsAt case media = "Media" } + /// Der Sender, auf dem diese Sendung läuft. + /// + /// Hier steht der Sender vollständig: Name, Nummer, Logo, Einschaltweg. + /// Das ist der Grund, warum das Raster die Senderliste ersetzen kann. + var channel: PlexChannel? { media?.compactMap(\.channel).first } + func program(channelID: String?) -> PlexProgram? { guard let beginsAt, let endsAt, let title else { return nil } return PlexProgram( diff --git a/Packages/FlixyKit/Tests/FlixyDesignTests/GuideLayoutTests.swift b/Packages/FlixyKit/Tests/FlixyDesignTests/GuideLayoutTests.swift new file mode 100644 index 0000000..50e60bb --- /dev/null +++ b/Packages/FlixyKit/Tests/FlixyDesignTests/GuideLayoutTests.swift @@ -0,0 +1,81 @@ +import Foundation +import Testing +@testable import FlixyDesign + +/// Die Geometrie des Programmrasters. +@Suite("Raster-Geometrie") +struct GuideLayoutTests { + + /// 13:47 Uhr — bewusst krumm, denn genau das ist der Normalfall. + private let now = Date(timeIntervalSince1970: 1_757_684_820) + + private var layout: GuideLayout { + GuideLayout(now: now, latestEnd: now.addingTimeInterval(6 * 3_600), pointsPerMinute: 4) + } + + @Test("Das Fenster beginnt auf der halben Stunde, nicht jetzt") + func snapsToHalfHour() { + let start = layout.start + let minutes = Calendar(identifier: .gregorian).component(.minute, from: start) + + #expect(minutes % 30 == 0) + #expect(start <= now) + #expect(now.timeIntervalSince(start) < 30 * 60) + } + + @Test("Eine Sendung, die vor dem Fenster begann, klebt am linken Rand") + func clampsEarlierStart() { + // Sonst säße die laufende Sendung bei negativem Versatz außerhalb — + // ausgerechnet die, die man am ehesten sehen will. + #expect(layout.x(for: now.addingTimeInterval(-9 * 3_600)) == 0) + } + + @Test("Eine angebrochene Sendung behält nur ihren sichtbaren Teil") + func clipsToWindow() { + let start = layout.start.addingTimeInterval(-1_800) + let end = layout.start.addingTimeInterval(1_800) + + // Eine Stunde lang, aber nur die zweite Hälfte liegt im Fenster. + #expect(layout.width(from: start, to: end) == 120.0) + } + + @Test("Eine Kurzmeldung bleibt lesbar breit") + func keepsShortProgramsReadable() { + let start = layout.start + let end = start.addingTimeInterval(120) + + #expect(layout.width(from: start, to: end) == layout.minimumBlockWidth) + } + + @Test("Ohne Programm hat das Raster trotzdem Breite") + func emptyGuideHasWidth() { + let empty = GuideLayout(now: now, latestEnd: nil, pointsPerMinute: 4) + + #expect(empty.totalWidth > 0) + #expect(empty.end.timeIntervalSince(empty.start) == 3 * 3_600) + } + + @Test("Das Fenster endet auf der halben Stunde") + func snapsEndToHalfHour() { + let minutes = Calendar(identifier: .gregorian).component(.minute, from: layout.end) + + #expect(minutes % 30 == 0) + #expect(layout.end >= now.addingTimeInterval(6 * 3_600)) + } + + @Test("Zwei Wochen Vorschau werden auf vierzehn Stunden gekürzt") + func capsLongGuides() { + let long = GuideLayout(now: now, latestEnd: now.addingTimeInterval(14 * 24 * 3_600)) + + #expect(long.end.timeIntervalSince(long.start) == 14 * 3_600) + } + + @Test("Die Achse ist halbstündlich und deckt das Fenster ab") + func ticksCoverWindow() { + // 13:30 bis 19:47, aufgerundet auf 20:00 — dreizehn halbe Stunden. + #expect(layout.ticks.count == 13) + #expect(layout.ticks.first == layout.start) + #expect(layout.ticks.allSatisfy { $0 < layout.end }) + #expect(layout.tickWidth == 120.0) + } +} diff --git a/Packages/FlixyKit/Tests/PlexKitTests/Fixtures/livetv-grid.json b/Packages/FlixyKit/Tests/PlexKitTests/Fixtures/livetv-grid.json new file mode 100644 index 0000000..dff6f47 --- /dev/null +++ b/Packages/FlixyKit/Tests/PlexKitTests/Fixtures/livetv-grid.json @@ -0,0 +1,52 @@ +{ + "MediaContainer": { + "size": 3, + "Metadata": [ + { + "ratingKey": "epg://1", + "title": "Tagesschau", + "summary": "Nachrichten", + "beginsAt": 1757682000, + "endsAt": 1757682900, + "Media": [ + { + "channelIdentifier": "6228f7f2364e02fa5ec8e51c-11", + "channelTitle": "SRF 1 HD", + "channelCallSign": "SRF1", + "channelThumb": "/library/metadata/11/thumb", + "channelVcn": "1", + "Part": [{ "key": "/livetv/channels/11" }] + } + ] + }, + { + "ratingKey": "epg://2", + "title": "Meteo", + "beginsAt": 1757682900, + "endsAt": 1757683200, + "Media": [ + { + "channelIdentifier": "6228f7f2364e02fa5ec8e51c-11", + "channelTitle": "SRF 1 HD", + "channelVcn": 1, + "Part": [{ "key": "/livetv/channels/11" }] + } + ] + }, + { + "ratingKey": "epg://3", + "title": "Sport aktuell", + "beginsAt": 1757682000, + "endsAt": 1757685600, + "Media": [ + { + "channelIdentifier": "6228f7f2364e02fa5ec8e51c-12", + "channelCallSign": "SRF2", + "channelVcn": "2", + "Part": [{ "key": "/livetv/channels/12" }] + } + ] + } + ] + } +} diff --git a/Packages/FlixyKit/Tests/PlexKitTests/LiveTVGuideTests.swift b/Packages/FlixyKit/Tests/PlexKitTests/LiveTVGuideTests.swift new file mode 100644 index 0000000..43e9f3a --- /dev/null +++ b/Packages/FlixyKit/Tests/PlexKitTests/LiveTVGuideTests.swift @@ -0,0 +1,104 @@ +import Foundation +import Testing +@testable import PlexKit + +/// Woher die Senderliste kommt. +/// +/// Drei Adressen, unter denen die Senderliste stehen sollte, antworteten auf +/// einem echten Server mit 404 — geraten war jede. Das Programmraster ist +/// keine Vermutung: die Weboberfläche von Plex zeichnet ihren Guide daraus, +/// und jeder Eintrag trägt den Sender mit sich. Diese Tests halten fest, dass +/// die Sender dort auch dann herauskommen, wenn sonst nichts antwortet. +@Suite("Senderliste aus dem Programmraster") +struct LiveTVGuideTests { + + private func server(routes: [String: String]) -> PlexServer { + PlexServer( + resource: PlexResource( + name: "Test", + clientIdentifier: "srv", + accessToken: "token", + connections: [PlexConnection(uri: "http://server:32400", local: true)] + ), + connection: PlexConnection(uri: "http://server:32400", local: true), + identity: .test, + transport: FakeTransport.routes(routes) + )! + } + + private func fixture(_ name: String) throws -> String { + let url = try #require(Bundle.module.url(forResource: "Fixtures/\(name)", withExtension: "json")) + return try String(contentsOf: url, encoding: .utf8) + } + + private var dvr: PlexDVR { + PlexDVR(key: "2", epgIdentifier: "tv.plex.providers.epg.cloud:2") + } + + @Test("Jeder Sender kommt genau einmal, obwohl er in mehreren Sendungen steht") + func derivesChannelsFromGrid() async throws { + let server = server(routes: [ + "/tv.plex.providers.epg.cloud:2/grid": try fixture("livetv-grid"), + ]) + + let guide = await server.liveTVGuide(dvr: dvr) + + #expect(guide.channels.count == 2) + #expect(guide.channels.map(\.title) == ["SRF 1 HD", "SRF2"]) + } + + @Test("Fehlt der Sendername, trägt das Kürzel den Sender") + func fallsBackToCallSign() async throws { + let server = server(routes: [ + "/tv.plex.providers.epg.cloud:2/grid": try fixture("livetv-grid"), + ]) + + let guide = await server.liveTVGuide(dvr: dvr) + let second = try #require(guide.channels.last) + + #expect(second.title == "SRF2") + // Die Sendernummer kommt mal als Zahl, mal als Zeichenkette — beides + // muss lesbar sein, sonst fehlt sie bei der Hälfte der Sender. + #expect(second.channelIdentifier == "2") + } + + @Test("Sendungen landen beim richtigen Sender, nach Anfangszeit sortiert") + func groupsProgramsByChannel() async throws { + let server = server(routes: [ + "/tv.plex.providers.epg.cloud:2/grid": try fixture("livetv-grid"), + ]) + + let guide = await server.liveTVGuide(dvr: dvr) + let first = try #require(guide.channels.first) + let programs = try #require(guide.programs[first.id]) + + #expect(programs.map(\.title) == ["Tagesschau", "Meteo"]) + #expect(guide.programs.count == 2) + } + + @Test("Ein Sender ohne Namen taucht nicht als Strich auf") + func skipsNamelessChannels() async throws { + let json = """ + {"MediaContainer":{"size":1,"Metadata":[ + {"ratingKey":"x","title":"Irgendwas","beginsAt":1757682000,"endsAt":1757685600, + "Media":[{"channelIdentifier":"nur-eine-kennung","Part":[{"key":"/livetv/channels/9"}]}]} + ]}} + """ + let server = server(routes: ["/tv.plex.providers.epg.cloud:2/grid": json]) + + let guide = await server.liveTVGuide(dvr: dvr) + + #expect(guide.channels.isEmpty) + } + + @Test("Die Einschaltadresse reist mit dem Sender mit") + func keepsTuneKey() async throws { + let server = server(routes: [ + "/tv.plex.providers.epg.cloud:2/grid": try fixture("livetv-grid"), + ]) + + let guide = await server.liveTVGuide(dvr: dvr) + + #expect(guide.channels.first?.key == "/livetv/channels/11") + } +}