diff --git a/Flixy/Branding.swift b/Flixy/Branding.swift
index c34dd39..9ce3cf4 100644
--- a/Flixy/Branding.swift
+++ b/Flixy/Branding.swift
@@ -28,6 +28,20 @@ enum Branding {
Bundle.main.bundleIdentifier ?? "com.scarriffleservices.flixy"
}
+ /// Das eigene URL-Schema, über das Plex nach der Anmeldung zurückschickt.
+ ///
+ /// Die Bundle-Kennung statt eines kurzen Wortes wie „flixy": sie ist
+ /// garantiert eindeutig. Bei einem kurzen Schema könnte eine andere App
+ /// denselben Namen beanspruchen und die Anmeldung abfangen.
+ static var urlScheme: String {
+ Bundle.main.bundleIdentifier ?? "com.scarriffleservices.flixy"
+ }
+
+ /// Wohin Plex nach der Anmeldung zurückschickt.
+ static var authCallbackURL: URL {
+ URL(string: "\(urlScheme)://auth")!
+ }
+
/// Die Version aus dem Bundle, für die Kopfzeile `X-Plex-Version`.
static var version: String {
Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "0"
diff --git a/Flixy/Info.plist b/Flixy/Info.plist
index 5adb7c9..fa73e5b 100644
--- a/Flixy/Info.plist
+++ b/Flixy/Info.plist
@@ -18,6 +18,17 @@
APPL
CFBundleShortVersionString
1.0
+ CFBundleURLTypes
+
+
+ CFBundleURLName
+ com.scarriffleservices.flixy.auth
+ CFBundleURLSchemes
+
+ com.scarriffleservices.flixy
+
+
+
CFBundleVersion
1
NSAppTransportSecurity
diff --git a/Flixy/Resources/Localizable.xcstrings b/Flixy/Resources/Localizable.xcstrings
index af2a5f2..e801039 100644
--- a/Flixy/Resources/Localizable.xcstrings
+++ b/Flixy/Resources/Localizable.xcstrings
@@ -1,3166 +1,3165 @@
{
- "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
+ "4K": {
+ "comment": "A label for a 4K video.",
+ "isCommentAutoGenerated": true
},
- "720p" : {
- "comment" : "A label for a 720p video.",
- "isCommentAutoGenerated" : true
+ "720p": {
+ "comment": "A label for a 720p video.",
+ "isCommentAutoGenerated": true
},
- "1080p" : {
- "comment" : "A label for a 1080p video.",
- "isCommentAutoGenerated" : true
+ "1080p": {
+ "comment": "A label for a 1080p video.",
+ "isCommentAutoGenerated": true
},
- "action.apply" : {
- "extractionState" : "stale",
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Übernehmen"
+ "action.apply": {
+ "extractionState": "stale",
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Übernehmen"
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Apply"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Apply"
}
}
}
},
- "action.cancel" : {
- "extractionState" : "stale",
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Abbrechen"
+ "action.cancel": {
+ "extractionState": "stale",
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Abbrechen"
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Cancel"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Cancel"
}
}
}
},
- "action.clearRating" : {
- "extractionState" : "stale",
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Bewertung entfernen"
+ "action.clearRating": {
+ "extractionState": "stale",
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Bewertung entfernen"
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Clear rating"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Clear rating"
}
}
}
},
- "action.close" : {
- "extractionState" : "stale",
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Schließen"
+ "action.close": {
+ "extractionState": "stale",
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Schließen"
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Close"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Close"
}
}
}
},
- "action.delete" : {
- "extractionState" : "stale",
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Löschen"
+ "action.delete": {
+ "extractionState": "stale",
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Löschen"
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Delete"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Delete"
}
}
}
},
- "action.markUnwatched" : {
- "extractionState" : "stale",
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Als ungesehen markieren"
+ "action.markUnwatched": {
+ "extractionState": "stale",
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Als ungesehen markieren"
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Mark as unwatched"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Mark as unwatched"
}
}
}
},
- "action.markWatched" : {
- "extractionState" : "stale",
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Als gesehen markieren"
+ "action.markWatched": {
+ "extractionState": "stale",
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Als gesehen markieren"
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Mark as watched"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Mark as watched"
}
}
}
},
- "action.rate" : {
- "extractionState" : "stale",
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Bewerten"
+ "action.rate": {
+ "extractionState": "stale",
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Bewerten"
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Rate"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Rate"
}
}
}
},
- "action.retry" : {
- "extractionState" : "stale",
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Erneut versuchen"
+ "action.retry": {
+ "extractionState": "stale",
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Erneut versuchen"
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Try again"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Try again"
}
}
}
},
- "action.signOut" : {
- "extractionState" : "stale",
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Abmelden"
+ "action.signOut": {
+ "extractionState": "stale",
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Abmelden"
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Sign out"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Sign out"
}
}
}
},
- "appearance.accent" : {
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Akzentfarbe"
+ "appearance.accent": {
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Akzentfarbe"
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Accent color"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Accent color"
}
}
}
},
- "appearance.accentExplanation" : {
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Die Farbe erscheint auf Knöpfen, Fortschrittsbalken und der ausgewählten Zeile in der Seitenleiste."
+ "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" : "The color appears on buttons, progress bars and the selected row in the sidebar."
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "The color appears on buttons, progress bars and the selected row in the sidebar."
}
}
}
},
- "appearance.adjusted %@" : {
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Die Farbe wurde auf %@ aufgehellt, damit sie auf dem dunklen Hintergrund lesbar bleibt."
+ "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" : "Lightened to %@ so it stays readable on the dark background."
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Lightened to %@ so it stays readable on the dark background."
}
}
}
},
- "appearance.customColor" : {
- "extractionState" : "stale",
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Eigene Farbe"
+ "appearance.customColor": {
+ "extractionState": "stale",
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Eigene Farbe"
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Custom color"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Custom color"
}
}
}
},
- "appearance.hexPlaceholder" : {
- "comment" : "Beispiel für eine Hex-Farbe",
- "extractionState" : "stale",
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "#1DB954"
+ "appearance.hexPlaceholder": {
+ "comment": "Beispiel für eine Hex-Farbe",
+ "extractionState": "stale",
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "#1DB954"
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "#1DB954"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "#1DB954"
}
}
}
},
- "appearance.invalidHex" : {
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Das ist keine gültige Farbe. Erwartet wird etwas wie #1DB954."
+ "appearance.invalidHex": {
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Das ist keine gültige Farbe. Erwartet wird etwas wie #1DB954."
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "That's not a valid color. Try something like #1DB954."
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "That's not a valid color. Try something like #1DB954."
}
}
}
},
- "appearance.preview" : {
- "extractionState" : "stale",
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Vorschau"
+ "appearance.preview": {
+ "extractionState": "stale",
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Vorschau"
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Preview"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Preview"
}
}
}
},
- "appearance.title" : {
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Erscheinungsbild"
+ "appearance.title": {
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Erscheinungsbild"
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Appearance"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Appearance"
}
}
}
},
- "connecting.searching" : {
- "extractionState" : "stale",
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Server werden gesucht …"
+ "connecting.searching": {
+ "extractionState": "stale",
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Server werden gesucht …"
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Looking for servers…"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Looking for servers…"
}
}
}
},
- "connecting.to %@" : {
- "comment" : "Platzhalter: Servername",
- "extractionState" : "stale",
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Verbinde mit %@ …"
+ "connecting.to %@": {
+ "comment": "Platzhalter: Servername",
+ "extractionState": "stale",
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Verbinde mit %@ …"
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Connecting to %@…"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Connecting to %@…"
}
}
}
},
- "detail.cast" : {
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Besetzung"
+ "detail.cast": {
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Besetzung"
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Cast"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Cast"
}
}
}
},
- "detail.episodeCount %lld" : {
- "extractionState" : "stale",
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "%lld Folgen"
+ "detail.episodeCount %lld": {
+ "extractionState": "stale",
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "%lld Folgen"
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "%lld episodes"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "%lld episodes"
}
}
}
},
- "detail.episodes" : {
- "extractionState" : "stale",
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Folgen"
+ "detail.episodes": {
+ "extractionState": "stale",
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Folgen"
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Episodes"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Episodes"
}
}
}
},
- "detail.playFromStart" : {
- "extractionState" : "stale",
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Von vorn abspielen"
+ "detail.playFromStart": {
+ "extractionState": "stale",
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Von vorn abspielen"
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Play from start"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Play from start"
}
}
}
},
- "detail.seasons" : {
- "extractionState" : "stale",
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Staffeln"
+ "detail.seasons": {
+ "extractionState": "stale",
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Staffeln"
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Seasons"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Seasons"
}
}
}
},
- "detail.unwatchedCount %lld" : {
- "extractionState" : "stale",
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "%lld ungesehen"
+ "detail.unwatchedCount %lld": {
+ "extractionState": "stale",
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "%lld ungesehen"
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "%lld unwatched"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "%lld unwatched"
}
}
}
},
- "detail.versions" : {
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Fassung"
+ "detail.versions": {
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Fassung"
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Version"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Version"
}
}
}
},
- "downloads.active" : {
- "extractionState" : "stale",
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Läuft gerade"
+ "downloads.active": {
+ "extractionState": "stale",
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Läuft gerade"
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "In progress"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "In progress"
}
}
}
},
- "downloads.allEpisodes" : {
- "extractionState" : "stale",
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Alle Folgen laden"
+ "downloads.allEpisodes": {
+ "extractionState": "stale",
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Alle Folgen laden"
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Download all episodes"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Download all episodes"
}
}
}
},
- "downloads.available" : {
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Offline verfügbar"
+ "downloads.available": {
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Offline verfügbar"
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Available offline"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Available offline"
}
}
}
},
- "downloads.batch.queued %lld" : {
- "extractionState" : "stale",
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "%lld Folgen in die Warteschlange gelegt."
+ "downloads.batch.queued %lld": {
+ "extractionState": "stale",
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "%lld Folgen in die Warteschlange gelegt."
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Queued %lld episodes."
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Queued %lld episodes."
}
}
}
},
- "downloads.batch.title" : {
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Downloads"
+ "downloads.batch.title": {
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Downloads"
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Downloads"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Downloads"
}
}
}
},
- "downloads.batch.unavailable %lld" : {
- "extractionState" : "stale",
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "%lld Folgen haben keine abspielbare Datei auf dem Server."
+ "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" : "%lld episodes have no playable file on the server."
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "%lld episodes have no playable file on the server."
}
}
}
},
- "downloads.cancel" : {
- "extractionState" : "stale",
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Download abbrechen"
+ "downloads.cancel": {
+ "extractionState": "stale",
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Download abbrechen"
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Cancel download"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Cancel download"
}
}
}
},
- "downloads.download" : {
- "extractionState" : "stale",
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Herunterladen"
+ "downloads.download": {
+ "extractionState": "stale",
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Herunterladen"
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Download"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Download"
}
}
}
},
- "downloads.empty.description" : {
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Lade Filme und Folgen herunter, um sie ohne Verbindung anzusehen."
+ "downloads.empty.description": {
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Lade Filme und Folgen herunter, um sie ohne Verbindung anzusehen."
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Download movies and episodes to watch them without a connection."
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Download movies and episodes to watch them without a connection."
}
}
}
},
- "downloads.empty.title" : {
- "extractionState" : "stale",
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Nichts offline"
+ "downloads.empty.title": {
+ "extractionState": "stale",
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Nichts offline"
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Nothing offline"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Nothing offline"
}
}
}
},
- "downloads.failed" : {
- "extractionState" : "stale",
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Fehlgeschlagen"
+ "downloads.failed": {
+ "extractionState": "stale",
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Fehlgeschlagen"
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Failed"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Failed"
}
}
}
},
- "downloads.movies" : {
- "comment" : "Überschrift für heruntergeladene Filme",
- "extractionState" : "stale",
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Filme"
+ "downloads.movies": {
+ "comment": "Überschrift für heruntergeladene Filme",
+ "extractionState": "stale",
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Filme"
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Movies"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Movies"
}
}
}
},
- "downloads.pause" : {
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Anhalten"
+ "downloads.pause": {
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Anhalten"
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Pause"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Pause"
}
}
}
},
- "downloads.paused" : {
- "extractionState" : "stale",
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Angehalten"
+ "downloads.paused": {
+ "extractionState": "stale",
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Angehalten"
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Paused"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Paused"
}
}
}
},
- "downloads.queued" : {
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Wartet"
+ "downloads.queued": {
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Wartet"
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Queued"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Queued"
}
}
}
},
- "downloads.refused.exists" : {
- "extractionState" : "stale",
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Dieser Titel steht bereits in der Warteschlange."
+ "downloads.refused.exists": {
+ "extractionState": "stale",
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Dieser Titel steht bereits in der Warteschlange."
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "This title is already queued."
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "This title is already queued."
}
}
}
},
- "downloads.refused.limit %@ %@" : {
- "extractionState" : "stale",
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Das Speicherlimit ist erreicht: %@ von %@ belegt."
+ "downloads.refused.limit %@ %@": {
+ "extractionState": "stale",
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Das Speicherlimit ist erreicht: %@ von %@ belegt."
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Storage limit reached: %@ of %@ used."
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Storage limit reached: %@ of %@ used."
}
}
}
},
- "downloads.refused.space %@ %@" : {
- "extractionState" : "stale",
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Zu wenig Platz auf dem Gerät: %@ nötig, %@ frei."
+ "downloads.refused.space %@ %@": {
+ "extractionState": "stale",
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Zu wenig Platz auf dem Gerät: %@ nötig, %@ frei."
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Not enough space on this device: %@ needed, %@ free."
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Not enough space on this device: %@ needed, %@ free."
}
}
}
},
- "downloads.refused.title" : {
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Download nicht möglich"
+ "downloads.refused.title": {
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Download nicht möglich"
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Can't download"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Can't download"
}
}
}
},
- "downloads.removeConfirm" : {
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Download entfernen?"
+ "downloads.removeConfirm": {
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Download entfernen?"
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Remove download?"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Remove download?"
}
}
}
},
- "downloads.resume" : {
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Fortsetzen"
+ "downloads.resume": {
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Fortsetzen"
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Resume"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Resume"
}
}
}
},
- "downloads.season" : {
- "extractionState" : "stale",
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Staffel laden"
+ "downloads.season": {
+ "extractionState": "stale",
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Staffel laden"
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Download season"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Download season"
}
}
}
},
- "downloads.used" : {
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Belegt"
+ "downloads.used": {
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Belegt"
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Used"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Used"
}
}
}
},
- "downloads.wifiOnly" : {
- "extractionState" : "stale",
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Nur über WLAN laden"
+ "downloads.wifiOnly": {
+ "extractionState": "stale",
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Nur über WLAN laden"
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Download over Wi-Fi only"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Download over Wi-Fi only"
}
}
}
},
- "episode.short %lld %lld" : {
- "comment" : "Staffel- und Folgennummer",
- "extractionState" : "stale",
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "S%lld E%lld"
+ "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" : "S%lld E%lld"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "S%lld E%lld"
}
}
}
},
- "error.noServers" : {
- "extractionState" : "stale",
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "An deinem Plex-Konto hängt kein Server."
+ "error.noServers": {
+ "extractionState": "stale",
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "An deinem Plex-Konto hängt kein Server."
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "There's no server on your Plex account."
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "There's no server on your Plex account."
}
}
}
},
- "error.notFound" : {
- "extractionState" : "stale",
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Dieser Titel ist auf dem Server nicht mehr vorhanden."
+ "error.notFound": {
+ "extractionState": "stale",
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Dieser Titel ist auf dem Server nicht mehr vorhanden."
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "This title is no longer on the server."
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "This title is no longer on the server."
}
}
}
},
- "error.server %lld" : {
- "comment" : "Platzhalter: HTTP-Statuscode",
- "extractionState" : "stale",
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Der Server hat mit Fehler %lld geantwortet."
+ "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" : "The server responded with error %lld."
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "The server responded with error %lld."
}
}
}
},
- "error.serverUnreachable" : {
- "extractionState" : "stale",
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Der Server ist nicht erreichbar. Prüfe, ob er läuft und ob du im selben Netzwerk bist."
+ "error.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" : "The server can't be reached. Check that it's running and that you're on the same network."
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "The server can't be reached. Check that it's running and that you're on the same network."
}
}
}
},
- "error.title" : {
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Das hat nicht geklappt"
+ "error.title": {
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Das hat nicht geklappt"
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Something went wrong"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Something went wrong"
}
}
}
},
- "error.unauthorized" : {
- "extractionState" : "stale",
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Die Anmeldung ist abgelaufen. Melde dich erneut an."
+ "error.unauthorized": {
+ "extractionState": "stale",
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Die Anmeldung ist abgelaufen. Melde dich erneut an."
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Your sign-in has expired. Please sign in again."
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Your sign-in has expired. Please sign in again."
}
}
}
},
- "error.unexpectedResponse" : {
- "extractionState" : "stale",
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Die Antwort des Servers war unerwartet. Möglicherweise ist eine neuere Plex-Version installiert."
+ "error.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" : "The server's response was unexpected. It may be running a newer version of Plex."
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "The server's response was unexpected. It may be running a newer version of Plex."
}
}
}
},
- "filter.any" : {
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Alle"
+ "filter.any": {
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Alle"
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Any"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Any"
}
}
}
},
- "filter.filters" : {
- "extractionState" : "stale",
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Filter"
+ "filter.filters": {
+ "extractionState": "stale",
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Filter"
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Filters"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Filters"
}
}
}
},
- "filter.genre" : {
- "extractionState" : "stale",
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Genre"
+ "filter.genre": {
+ "extractionState": "stale",
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Genre"
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Genre"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Genre"
}
}
}
},
- "filter.reset" : {
- "extractionState" : "stale",
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Zurücksetzen"
+ "filter.reset": {
+ "extractionState": "stale",
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Zurücksetzen"
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Reset"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Reset"
}
}
}
},
- "filter.resolution" : {
- "extractionState" : "stale",
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Auflösung"
+ "filter.resolution": {
+ "extractionState": "stale",
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Auflösung"
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Resolution"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Resolution"
}
}
}
},
- "filter.sort" : {
- "extractionState" : "stale",
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Sortierung"
+ "filter.sort": {
+ "extractionState": "stale",
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Sortierung"
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Sort by"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Sort by"
}
}
}
},
- "filter.title" : {
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Filter"
+ "filter.title": {
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Filter"
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Filters"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Filters"
}
}
}
},
- "filter.unwatchedOnly" : {
- "extractionState" : "stale",
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Nur Ungesehene"
+ "filter.unwatchedOnly": {
+ "extractionState": "stale",
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Nur Ungesehene"
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Unwatched only"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Unwatched only"
}
}
}
},
- "hero.details" : {
- "extractionState" : "stale",
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Details"
+ "hero.details": {
+ "extractionState": "stale",
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Details"
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Details"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Details"
}
}
}
},
- "hero.play" : {
- "extractionState" : "stale",
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Abspielen"
+ "hero.play": {
+ "extractionState": "stale",
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Abspielen"
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Play"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Play"
}
}
}
},
- "hero.resume" : {
- "extractionState" : "stale",
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Weiterschauen"
+ "hero.resume": {
+ "extractionState": "stale",
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Weiterschauen"
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Resume"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Resume"
}
}
}
},
- "home.empty.description" : {
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Sobald dein Server Mediatheken enthält, erscheinen sie hier."
+ "home.empty.description": {
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Sobald dein Server Mediatheken enthält, erscheinen sie hier."
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Once your server has libraries, they'll show up here."
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Once your server has libraries, they'll show up here."
}
}
}
},
- "home.empty.title" : {
- "extractionState" : "stale",
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Noch nichts da"
+ "home.empty.title": {
+ "extractionState": "stale",
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Noch nichts da"
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Nothing here yet"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Nothing here yet"
}
}
}
},
- "homeLayout.empty" : {
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Der Server liefert derzeit keine Reihen."
+ "homeLayout.empty": {
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Der Server liefert derzeit keine Reihen."
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "The server isn't providing any rows right now."
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "The server isn't providing any rows right now."
}
}
}
},
- "homeLayout.explanation" : {
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Schalte Reihen aus, die du nicht brauchst, und sortiere sie über „Bearbeiten“ um. Neue Reihen vom Server erscheinen am Ende."
+ "homeLayout.explanation": {
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Schalte Reihen aus, die du nicht brauchst, und sortiere sie über „Bearbeiten“ um. Neue Reihen vom Server erscheinen am Ende."
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Turn off rows you don't need and reorder them via Edit. New rows from the server appear at the end."
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Turn off rows you don't need and reorder them via Edit. New rows from the server appear at the end."
}
}
}
},
- "homeLayout.title" : {
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Startseite"
+ "homeLayout.title": {
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Startseite"
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Home screen"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Home screen"
}
}
}
},
- "item.watched" : {
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Gesehen"
+ "item.watched": {
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Gesehen"
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Watched"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Watched"
}
}
}
},
- "library.clearFilters" : {
- "extractionState" : "stale",
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Filter zurücksetzen"
+ "library.clearFilters": {
+ "extractionState": "stale",
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Filter zurücksetzen"
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Clear filters"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Clear filters"
}
}
}
},
- "library.empty.description" : {
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Mit den aktuellen Filtern bleibt nichts übrig."
+ "library.empty.description": {
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Mit den aktuellen Filtern bleibt nichts übrig."
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Nothing matches the current filters."
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Nothing matches the current filters."
}
}
}
},
- "library.empty.title" : {
- "extractionState" : "stale",
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Keine Titel"
+ "library.empty.title": {
+ "extractionState": "stale",
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Keine Titel"
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "No titles"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "No titles"
}
}
}
},
- "library.filters" : {
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Filter"
+ "library.filters": {
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Filter"
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Filters"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Filters"
}
}
}
},
- "library.scan" : {
- "extractionState" : "stale",
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Mediathek durchsuchen"
+ "library.scan": {
+ "extractionState": "stale",
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Mediathek durchsuchen"
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Scan library"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Scan library"
}
}
}
},
- "library.toggleLayout" : {
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Ansicht wechseln"
+ "library.toggleLayout": {
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Ansicht wechseln"
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Switch layout"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Switch layout"
}
}
}
},
- "liveTV.next %@ %@" : {
- "comment" : "Platzhalter: Uhrzeit, Sendungstitel",
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Danach %@ · %@"
+ "liveTV.next %@ %@": {
+ "comment": "Platzhalter: Uhrzeit, Sendungstitel",
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Danach %@ · %@"
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Next %@ · %@"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Next %@ · %@"
}
}
}
},
- "liveTV.noChannels.description" : {
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Der Empfänger ist eingerichtet, liefert aber keine Sender."
+ "liveTV.noChannels.description": {
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Der Empfänger ist eingerichtet, liefert aber keine Sender."
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "The tuner is set up but isn't providing any channels."
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "The tuner is set up but isn't providing any channels."
}
}
}
},
- "liveTV.noChannels.title" : {
- "extractionState" : "stale",
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Keine Sender"
+ "liveTV.noChannels.title": {
+ "extractionState": "stale",
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Keine Sender"
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "No channels"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "No channels"
}
}
}
},
- "liveTV.noProgram" : {
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Kein Programm verfügbar"
+ "liveTV.noProgram": {
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Kein Programm verfügbar"
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "No guide data"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "No guide data"
}
}
}
},
- "liveTV.notConfigured.description" : {
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Live-TV braucht eine Fernsehkarte oder einen Netzwerk-Tuner am Plex-Server. Richte ihn in den Servereinstellungen ein, dann erscheinen die Sender hier."
+ "liveTV.notConfigured.description": {
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Live-TV braucht eine Fernsehkarte oder einen Netzwerk-Tuner am Plex-Server. Richte ihn in den Servereinstellungen ein, dann erscheinen die Sender hier."
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Live TV needs a tuner card or network tuner on your Plex server. Set one up in the server settings and channels will appear here."
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Live TV needs a tuner card or network tuner on your Plex server. Set one up in the server settings and channels will appear here."
}
}
}
},
- "liveTV.notConfigured.title" : {
- "extractionState" : "stale",
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Kein Empfänger eingerichtet"
+ "liveTV.notConfigured.title": {
+ "extractionState": "stale",
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Kein Empfänger eingerichtet"
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "No tuner set up"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "No tuner set up"
}
}
}
},
- "placeholder.description" : {
- "comment" : "Platzhalter-Bildschirme",
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Dieser Bereich entsteht in einer späteren Phase."
+ "placeholder.description": {
+ "comment": "Platzhalter-Bildschirme",
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Dieser Bereich entsteht in einer späteren Phase."
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "This area is coming in a later phase."
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "This area is coming in a later phase."
}
}
}
},
- "player.audioTracks" : {
- "extractionState" : "stale",
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Tonspur"
+ "player.audioTracks": {
+ "extractionState": "stale",
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Tonspur"
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Audio"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Audio"
}
}
}
},
- "player.back10" : {
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "10 Sekunden zurück"
+ "player.back10": {
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "10 Sekunden zurück"
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Back 10 seconds"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Back 10 seconds"
}
}
}
},
- "player.close" : {
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Player schließen"
+ "player.close": {
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Player schließen"
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Close player"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Close player"
}
}
}
},
- "player.forward10" : {
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "10 Sekunden vor"
+ "player.forward10": {
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "10 Sekunden vor"
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Forward 10 seconds"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Forward 10 seconds"
}
}
}
},
- "player.loadFailed" : {
- "extractionState" : "stale",
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Die Datei ließ sich nicht öffnen."
+ "player.loadFailed": {
+ "extractionState": "stale",
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Die Datei ließ sich nicht öffnen."
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "The file couldn't be opened."
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "The file couldn't be opened."
}
}
}
},
- "player.nextEpisode" : {
- "extractionState" : "stale",
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Nächste Folge"
+ "player.nextEpisode": {
+ "extractionState": "stale",
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Nächste Folge"
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Next episode"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Next episode"
}
}
}
},
- "player.noPlayableFile" : {
- "extractionState" : "stale",
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Zu diesem Titel gibt es keine abspielbare Datei auf dem Server."
+ "player.noPlayableFile": {
+ "extractionState": "stale",
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Zu diesem Titel gibt es keine abspielbare Datei auf dem Server."
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "There's no playable file for this title on the server."
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "There's no playable file for this title on the server."
}
}
}
},
- "player.pause" : {
- "extractionState" : "stale",
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Pause"
+ "player.pause": {
+ "extractionState": "stale",
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Pause"
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Pause"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Pause"
}
}
}
},
- "player.play" : {
- "extractionState" : "stale",
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Abspielen"
+ "player.play": {
+ "extractionState": "stale",
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Abspielen"
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Play"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Play"
}
}
}
},
- "player.playbackFailed" : {
- "extractionState" : "stale",
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Die Wiedergabe wurde unterbrochen."
+ "player.playbackFailed": {
+ "extractionState": "stale",
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Die Wiedergabe wurde unterbrochen."
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Playback was interrupted."
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Playback was interrupted."
}
}
}
},
- "player.preparing" : {
- "comment" : "Hinweis, während der Player die Datei öffnet",
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Wird vorbereitet …"
+ "player.preparing": {
+ "comment": "Hinweis, während der Player die Datei öffnet",
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Wird vorbereitet …"
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Preparing…"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Preparing…"
}
}
}
},
- "player.skipCredits" : {
- "extractionState" : "stale",
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Abspann überspringen"
+ "player.skipCredits": {
+ "extractionState": "stale",
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Abspann überspringen"
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Skip credits"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Skip credits"
}
}
}
},
- "player.skipIntro" : {
- "extractionState" : "stale",
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Intro überspringen"
+ "player.skipIntro": {
+ "extractionState": "stale",
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Intro überspringen"
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Skip intro"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Skip intro"
}
}
}
},
- "player.subtitlesOff" : {
- "comment" : "Eintrag, der die Untertitel abschaltet",
- "extractionState" : "stale",
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Aus"
+ "player.subtitlesOff": {
+ "comment": "Eintrag, der die Untertitel abschaltet",
+ "extractionState": "stale",
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Aus"
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Off"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Off"
}
}
}
},
- "player.subtitleTracks" : {
- "extractionState" : "stale",
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Untertitel"
+ "player.subtitleTracks": {
+ "extractionState": "stale",
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Untertitel"
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Subtitles"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Subtitles"
}
}
}
},
- "player.tracks" : {
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Ton und Untertitel"
+ "player.tracks": {
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Ton und Untertitel"
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Audio and subtitles"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Audio and subtitles"
}
}
}
},
- "player.transcodeRefused" : {
- "extractionState" : "stale",
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Der Server hat die Wiedergabe abgelehnt."
+ "player.transcodeRefused": {
+ "extractionState": "stale",
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Der Server hat die Wiedergabe abgelehnt."
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "The server refused playback."
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "The server refused playback."
}
}
}
},
- "quality.high" : {
- "extractionState" : "stale",
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Hoch (12 Mbit/s)"
+ "quality.high": {
+ "extractionState": "stale",
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Hoch (12 Mbit/s)"
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "High (12 Mbps)"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "High (12 Mbps)"
}
}
}
},
- "quality.low" : {
- "extractionState" : "stale",
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Niedrig (4 Mbit/s)"
+ "quality.low": {
+ "extractionState": "stale",
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Niedrig (4 Mbit/s)"
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Low (4 Mbps)"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Low (4 Mbps)"
}
}
}
},
- "quality.medium" : {
- "extractionState" : "stale",
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Mittel (8 Mbit/s)"
+ "quality.medium": {
+ "extractionState": "stale",
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Mittel (8 Mbit/s)"
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Medium (8 Mbps)"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Medium (8 Mbps)"
}
}
}
},
- "quality.minimal" : {
- "extractionState" : "stale",
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Sparsam (2 Mbit/s)"
+ "quality.minimal": {
+ "extractionState": "stale",
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Sparsam (2 Mbit/s)"
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Data saver (2 Mbps)"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Data saver (2 Mbps)"
}
}
}
},
- "quality.original" : {
- "extractionState" : "stale",
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Original"
+ "quality.original": {
+ "extractionState": "stale",
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Original"
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Original"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Original"
}
}
}
},
- "runtime.hoursMinutes %lld %lld" : {
- "comment" : "Laufzeit mit Stunden",
- "extractionState" : "stale",
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "%lld Std %lld Min"
+ "runtime.hoursMinutes %lld %lld": {
+ "comment": "Laufzeit mit Stunden",
+ "extractionState": "stale",
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "%lld Std %lld Min"
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "%lldh %lldm"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "%lldh %lldm"
}
}
}
},
- "runtime.minutes %lld" : {
- "comment" : "Laufzeit unter einer Stunde",
- "extractionState" : "stale",
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "%lld Min"
+ "runtime.minutes %lld": {
+ "comment": "Laufzeit unter einer Stunde",
+ "extractionState": "stale",
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "%lld Min"
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "%lldm"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "%lldm"
}
}
}
},
- "SD" : {
- "comment" : "A label for a 720p movie.",
- "isCommentAutoGenerated" : true
+ "SD": {
+ "comment": "A label for a 720p movie.",
+ "isCommentAutoGenerated": true
},
- "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.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" : "Flixy lädt fehlende Folgen nach, sobald du eine gesehen hast, und entfernt Gesehenes nach der eingestellten Frist."
+ "settings.downloadsExplanation": {
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Flixy lädt fehlende Folgen nach, sobald du eine gesehen hast, und entfernt Gesehenes nach der eingestellten Frist."
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Flixy downloads missing episodes once you finish one, and removes watched titles after the chosen delay."
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Flixy downloads missing episodes once you finish one, and removes watched titles 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.codeHint" : {
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Öffne plex.tv/link im Browser und gib diesen Code ein:"
+ "signIn.tagline": {
+ "comment": "Untertitel auf dem Anmeldebildschirm",
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Deine Plex-Mediathek, schöner."
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Open plex.tv/link in your browser and enter this code:"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Your Plex library, nicer."
}
}
}
},
- "signIn.openBrowser" : {
- "extractionState" : "stale",
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Im Browser öffnen"
+ "sort.lastViewed": {
+ "extractionState": "stale",
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Zuletzt gesehen"
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Open in browser"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Last watched"
}
}
}
},
- "signIn.tagline" : {
- "comment" : "Untertitel auf dem Anmeldebildschirm",
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Deine Plex-Mediathek, schöner."
+ "sort.rating": {
+ "extractionState": "stale",
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Bewertung"
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Your Plex library, nicer."
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Rating"
}
}
}
},
- "signIn.waiting" : {
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Warte auf Bestätigung …"
+ "sort.recentlyAdded": {
+ "extractionState": "stale",
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Zuletzt hinzugefügt"
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Waiting for confirmation…"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Recently added"
}
}
}
},
- "sort.lastViewed" : {
- "extractionState" : "stale",
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Zuletzt gesehen"
+ "sort.releaseDate": {
+ "extractionState": "stale",
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Erscheinungsjahr"
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Last watched"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Release date"
}
}
}
},
- "sort.rating" : {
- "extractionState" : "stale",
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Bewertung"
+ "sort.titleAscending": {
+ "extractionState": "stale",
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Titel (A–Z)"
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Rating"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Title (A–Z)"
}
}
}
},
- "sort.recentlyAdded" : {
- "extractionState" : "stale",
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Zuletzt hinzugefügt"
+ "sort.titleDescending": {
+ "extractionState": "stale",
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Titel (Z–A)"
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Recently added"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Title (Z–A)"
}
}
}
},
- "sort.releaseDate" : {
- "extractionState" : "stale",
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Erscheinungsjahr"
+ "technical.audio": {
+ "extractionState": "stale",
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Tonspur"
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Release date"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Audio"
}
}
}
},
- "sort.titleAscending" : {
- "extractionState" : "stale",
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Titel (A–Z)"
+ "technical.bitrate": {
+ "extractionState": "stale",
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Bitrate"
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Title (A–Z)"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Bitrate"
}
}
}
},
- "sort.titleDescending" : {
- "extractionState" : "stale",
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Titel (Z–A)"
+ "technical.container": {
+ "extractionState": "stale",
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Container"
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Title (Z–A)"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Container"
}
}
}
},
- "technical.audio" : {
- "extractionState" : "stale",
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Tonspur"
+ "technical.directPlayNative": {
+ "extractionState": "stale",
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Direkt (Systemplayer)"
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Audio"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Direct (system player)"
}
}
}
},
- "technical.bitrate" : {
- "extractionState" : "stale",
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Bitrate"
+ "technical.directPlayVLC": {
+ "extractionState": "stale",
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Direkt (VLC)"
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Bitrate"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Direct (VLC)"
}
}
}
},
- "technical.container" : {
- "extractionState" : "stale",
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Container"
+ "technical.file": {
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Datei auf dem Server"
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Container"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "File on server"
}
}
}
},
- "technical.directPlayNative" : {
- "extractionState" : "stale",
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Direkt (Systemplayer)"
+ "technical.playbackMethod": {
+ "extractionState": "stale",
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Wiedergabe"
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Direct (system player)"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Playback"
}
}
}
},
- "technical.directPlayVLC" : {
- "extractionState" : "stale",
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Direkt (VLC)"
+ "technical.reason": {
+ "extractionState": "stale",
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Grund"
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Direct (VLC)"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Reason"
}
}
}
},
- "technical.file" : {
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Datei auf dem Server"
+ "technical.reason.audioCodec %@": {
+ "extractionState": "stale",
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "iOS kann %@-Ton nicht wiedergeben."
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "File on server"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "iOS can't play %@ audio."
}
}
}
},
- "technical.playbackMethod" : {
- "extractionState" : "stale",
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Wiedergabe"
+ "technical.reason.container %@": {
+ "extractionState": "stale",
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "iOS öffnet den Container %@ nicht."
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Playback"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "iOS can't open the %@ container."
}
}
}
},
- "technical.reason" : {
- "extractionState" : "stale",
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Grund"
+ "technical.reason.unanalyzed": {
+ "extractionState": "stale",
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Der Server hat diese Datei noch nicht analysiert."
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Reason"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "The server hasn't analyzed this file yet."
}
}
}
},
- "technical.reason.audioCodec %@" : {
- "extractionState" : "stale",
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "iOS kann %@-Ton nicht wiedergeben."
+ "technical.reason.videoCodec %@": {
+ "extractionState": "stale",
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "iOS kann %@ nicht dekodieren."
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "iOS can't play %@ audio."
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "iOS can't decode %@."
}
}
}
},
- "technical.reason.container %@" : {
- "extractionState" : "stale",
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "iOS öffnet den Container %@ nicht."
+ "technical.resolution": {
+ "extractionState": "stale",
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Auflösung"
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "iOS can't open the %@ container."
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Resolution"
}
}
}
},
- "technical.reason.unanalyzed" : {
- "extractionState" : "stale",
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Der Server hat diese Datei noch nicht analysiert."
+ "technical.size": {
+ "extractionState": "stale",
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Dateigröße"
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "The server hasn't analyzed this file yet."
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "File size"
}
}
}
},
- "technical.reason.videoCodec %@" : {
- "extractionState" : "stale",
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "iOS kann %@ nicht dekodieren."
+ "technical.subtitle": {
+ "extractionState": "stale",
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Untertitel"
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "iOS can't decode %@."
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Subtitle"
}
}
}
},
- "technical.resolution" : {
- "extractionState" : "stale",
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Auflösung"
+ "technical.title": {
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Technische Angaben"
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Resolution"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Technical details"
}
}
}
},
- "technical.size" : {
- "extractionState" : "stale",
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Dateigröße"
+ "technical.transcode": {
+ "extractionState": "stale",
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Umgerechnet vom Server"
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "File size"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Transcoded by server"
}
}
}
},
- "technical.subtitle" : {
- "extractionState" : "stale",
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Untertitel"
+ "technical.videoCodec": {
+ "extractionState": "stale",
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Bildcodec"
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Subtitle"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Video codec"
}
}
}
},
- "technical.title" : {
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Technische Angaben"
+ "signIn.privacy": {
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Die Anmeldemaske kommt von Plex. Dein Passwort erreicht Flixy nie."
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Technical details"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "The sign-in page comes from Plex. Your password never reaches Flixy."
}
}
}
},
- "technical.transcode" : {
- "extractionState" : "stale",
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Umgerechnet vom Server"
+ "signIn.windowFailed": {
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Das Anmeldefenster ließ sich nicht öffnen."
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Transcoded by server"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "The sign-in window couldn't be opened."
}
}
}
},
- "technical.videoCodec" : {
- "extractionState" : "stale",
- "localizations" : {
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Bildcodec"
+ "signIn.cancelled": {
+ "localizations": {
+ "de": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Anmeldung abgebrochen."
}
},
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Video codec"
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Sign-in cancelled."
}
}
}
}
},
- "version" : "1.1"
-}
\ No newline at end of file
+ "version": "1.1"
+}
diff --git a/Flixy/Session/FlixySession.swift b/Flixy/Session/FlixySession.swift
index 688d68b..4467b89 100644
--- a/Flixy/Session/FlixySession.swift
+++ b/Flixy/Session/FlixySession.swift
@@ -14,8 +14,8 @@ final class FlixySession {
/// Beim Start: liegt ein Token im Schlüsselbund?
case launching
case signedOut
- /// Der Code steht bereit, der Nutzer bestätigt ihn im Browser.
- case awaitingAuthorization(PlexPin)
+ /// Das Anmeldefenster von Plex ist offen.
+ case authenticating
/// Mehrere Server am Konto — einer muss gewählt werden.
case choosingServer([PlexResource])
/// Ein Weg zum Server wird gesucht.
@@ -27,8 +27,8 @@ final class FlixySession {
switch (lhs, rhs) {
case (.launching, .launching), (.signedOut, .signedOut), (.ready, .ready):
true
- case (.awaitingAuthorization(let l), .awaitingAuthorization(let r)):
- l.id == r.id
+ case (.authenticating, .authenticating):
+ true
case (.choosingServer(let l), .choosingServer(let r)):
l.map(\.clientIdentifier) == r.map(\.clientIdentifier)
case (.connecting(let l), .connecting(let r)):
@@ -58,6 +58,7 @@ final class FlixySession {
private let identity: PlexClientIdentity
private let tokenStore: any TokenStoring
private let authenticator: PlexAuthenticator
+ private let webAuthenticator = PlexWebAuthenticator()
private let defaults: UserDefaults
private var authorizationTask: Task?
@@ -85,11 +86,47 @@ final class FlixySession {
// MARK: - Anmeldung
+ /// Öffnet die Anmeldeseite von Plex.
+ ///
+ /// Der PIN steckt weiterhin darunter — er ist der einzige Weg, auf dem
+ /// Plex einen Token herausgibt. Sichtbar ist er nicht mehr: Plex liefert
+ /// seine eigene Anmeldemaske aus und schickt die App danach über
+ /// `forwardUrl` zurück.
func beginSignIn() async {
+ phase = .authenticating
+
do {
let pin = try await authenticator.requestPin()
- phase = .awaitingAuthorization(pin)
- waitForAuthorization(of: pin)
+
+ guard let url = pin.authorizationURL(
+ clientIdentifier: identity.clientIdentifier,
+ product: Branding.plexProduct,
+ forwardURL: Branding.authCallbackURL
+ ) else {
+ phase = .failed(message: String(localized: "signIn.windowFailed"))
+ return
+ }
+
+ try await webAuthenticator.authenticate(
+ url: url,
+ callbackScheme: Branding.urlScheme
+ )
+
+ // Das Fenster ist zu, jetzt liegt der Token bereit. Kurz
+ // nachgefragt statt sofort erwartet: zwischen dem Rücksprung und
+ // dem Eintrag bei Plex liegen manchmal ein, zwei Sekunden.
+ let token = try await authenticator.waitForAuthorization(
+ pin: pin,
+ pollInterval: .milliseconds(500),
+ timeout: .seconds(20)
+ )
+ await connect(accountToken: token)
+
+ } catch PlexWebAuthenticator.Failure.cancelled {
+ // Fenster geschlossen. Kein Fehler — kommentarlos zurück.
+ phase = .signedOut
+ } catch let error as PlexWebAuthenticator.Failure {
+ phase = .failed(message: webAuthenticationMessage(for: error))
} catch {
phase = .failed(message: message(for: error))
}
@@ -101,17 +138,10 @@ final class FlixySession {
phase = .signedOut
}
- private func waitForAuthorization(of pin: PlexPin) {
- authorizationTask?.cancel()
- authorizationTask = Task { [authenticator] in
- do {
- let token = try await authenticator.waitForAuthorization(pin: pin)
- guard !Task.isCancelled else { return }
- await connect(accountToken: token)
- } catch {
- guard !Task.isCancelled else { return }
- phase = .failed(message: message(for: error))
- }
+ private func webAuthenticationMessage(for failure: PlexWebAuthenticator.Failure) -> String {
+ switch failure {
+ case .cancelled: String(localized: "signIn.cancelled")
+ case .presentationFailed(let description): description
}
}
diff --git a/Flixy/Session/PlexWebAuthenticator.swift b/Flixy/Session/PlexWebAuthenticator.swift
new file mode 100644
index 0000000..bb93f72
--- /dev/null
+++ b/Flixy/Session/PlexWebAuthenticator.swift
@@ -0,0 +1,74 @@
+import AuthenticationServices
+import Foundation
+import UIKit
+
+/// Öffnet die Anmeldeseite von Plex in einem Fenster innerhalb der App.
+///
+/// `ASWebAuthenticationSession` ist dafür der vorgesehene Weg: die Seite kommt
+/// von Plex, das Passwort wird dort eingegeben und erreicht die App nie. Nach
+/// dem Anmelden schickt Plex den Browser auf eine Adresse mit unserem eigenen
+/// Schema, das Fenster schließt sich von selbst, und die App weiß, dass sie
+/// den Token abholen kann.
+///
+/// Ein einfacher `SFSafariViewController` täte es nicht: er meldet nicht, wann
+/// die Rückadresse erreicht wurde, und müsste von Hand geschlossen werden.
+@MainActor
+final class PlexWebAuthenticator: NSObject {
+ enum Failure: Error, Equatable {
+ /// Der Nutzer hat das Fenster geschlossen. Kein Fehler im eigentlichen
+ /// Sinn — die Oberfläche soll darauf kommentarlos zurückgehen.
+ case cancelled
+ case presentationFailed(String)
+ }
+
+ /// Führt die Anmeldung durch und kehrt zurück, sobald Plex die App
+ /// zurückgeschickt hat.
+ func authenticate(url: URL, callbackScheme: String) async throws(Failure) {
+ do {
+ try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in
+ let session = ASWebAuthenticationSession(
+ url: url,
+ callbackURLScheme: callbackScheme
+ ) { _, error in
+ if let error {
+ continuation.resume(throwing: error)
+ } else {
+ continuation.resume()
+ }
+ }
+
+ session.presentationContextProvider = self
+ // `false`, damit eine bestehende Plex-Anmeldung in Safari
+ // mitgenutzt wird: wer dort schon angemeldet ist, braucht nur
+ // zu bestätigen statt das Passwort erneut einzugeben.
+ session.prefersEphemeralWebBrowserSession = false
+
+ guard session.start() else {
+ continuation.resume(throwing: Failure.presentationFailed(
+ String(localized: "signIn.windowFailed")
+ ))
+ return
+ }
+ }
+ } catch let error as ASWebAuthenticationSessionError where error.code == .canceledLogin {
+ throw .cancelled
+ } catch let error as Failure {
+ throw error
+ } catch {
+ throw .presentationFailed(error.localizedDescription)
+ }
+ }
+}
+
+extension PlexWebAuthenticator: ASWebAuthenticationPresentationContextProviding {
+ func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
+ // Das Fenster der App. Über die verbundenen Szenen gesucht, weil eine
+ // SwiftUI-App keinen festen Verweis darauf hat — und ein frisch
+ // erzeugtes Fenster erschiene hinter dem sichtbaren.
+ let scene = UIApplication.shared.connectedScenes
+ .compactMap { $0 as? UIWindowScene }
+ .first { $0.activationState == .foregroundActive }
+
+ return scene?.keyWindow ?? ASPresentationAnchor()
+ }
+}
diff --git a/Flixy/Views/Navigation/RootView.swift b/Flixy/Views/Navigation/RootView.swift
index 41012a6..f113785 100644
--- a/Flixy/Views/Navigation/RootView.swift
+++ b/Flixy/Views/Navigation/RootView.swift
@@ -24,12 +24,10 @@ struct RootView: View {
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.flixyCanvas)
- case .signedOut, .awaitingAuthorization:
+ case .signedOut, .authenticating:
SignInView(
phase: session.phase,
- clientIdentifier: session.clientIdentifier,
- onSignIn: { Task { await session.beginSignIn() } },
- onCancel: { session.cancelSignIn() }
+ onSignIn: { Task { await session.beginSignIn() } }
)
case .choosingServer(let servers):
diff --git a/Flixy/Views/SignIn/SignInView.swift b/Flixy/Views/SignIn/SignInView.swift
index 2137957..4a42afc 100644
--- a/Flixy/Views/SignIn/SignInView.swift
+++ b/Flixy/Views/SignIn/SignInView.swift
@@ -4,18 +4,20 @@ import PlexKit
/// Der Anmeldebildschirm.
///
-/// Zeigt den Code, den Plex vergeben hat, und schickt in den Browser. Die App
-/// bekommt Benutzername und Passwort nie zu sehen — sie fragt nur regelmäßig
-/// nach, ob der Code bestätigt wurde.
+/// Ein Knopf, mehr nicht. Die Anmeldemaske kommt von Plex und öffnet sich in
+/// einem Fenster innerhalb der App; das Passwort wird dort eingegeben und
+/// erreicht Flixy nie. Nach dem Anmelden schließt sich das Fenster von selbst.
struct SignInView: View {
let phase: FlixySession.Phase
- let clientIdentifier: String
let onSignIn: () -> Void
- let onCancel: () -> Void
- @Environment(\.openURL) private var openURL
@Environment(\.flixyAccent) private var accent
+ private var isAuthenticating: Bool {
+ if case .authenticating = phase { return true }
+ return false
+ }
+
var body: some View {
VStack(spacing: FlixySpacing.section) {
Spacer()
@@ -37,17 +39,26 @@ struct SignInView: View {
Spacer()
- if case .awaitingAuthorization(let pin) = phase {
- awaitingAuthorization(pin: pin)
- } else {
+ VStack(spacing: FlixySpacing.regular) {
Button(action: onSignIn) {
- Text("signIn.action")
- .font(.flixyRowHeader)
- .frame(maxWidth: .infinity)
- .padding(.vertical, FlixySpacing.regular)
+ Group {
+ if isAuthenticating {
+ ProgressView().tint(Color.flixyCanvas)
+ } else {
+ Text("signIn.action").font(.flixyRowHeader)
+ }
+ }
+ .frame(maxWidth: .infinity)
+ .padding(.vertical, FlixySpacing.regular)
}
.buttonStyle(.borderedProminent)
.tint(accent)
+ .disabled(isAuthenticating)
+
+ Text("signIn.privacy")
+ .font(.flixyCaption)
+ .foregroundStyle(Color.flixyTextMuted)
+ .multilineTextAlignment(.center)
}
Spacer().frame(height: FlixySpacing.section)
@@ -57,51 +68,4 @@ struct SignInView: View {
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.flixyCanvas)
}
-
- @ViewBuilder
- private func awaitingAuthorization(pin: PlexPin) -> some View {
- VStack(spacing: FlixySpacing.loose) {
- Text("signIn.codeHint")
- .font(.flixyCaption)
- .foregroundStyle(Color.flixyTextSecondary)
- .multilineTextAlignment(.center)
-
- Text(pin.code)
- // Feste Zeichenbreite, damit der Code beim Abtippen nicht
- // springt und Ziffern nicht mit Buchstaben verschwimmen.
- .font(.system(.largeTitle, design: .monospaced, weight: .semibold))
- .tracking(6)
- .foregroundStyle(Color.flixyTextPrimary)
- .textSelection(.enabled)
- .padding(.vertical, FlixySpacing.regular)
- .frame(maxWidth: .infinity)
- .background(Color.flixyElevated, in: .rect(cornerRadius: FlixyRadius.sheet))
-
- Button {
- if let url = pin.authorizationURL(
- clientIdentifier: clientIdentifier,
- product: Branding.plexProduct
- ) {
- openURL(url)
- }
- } label: {
- Label("signIn.openBrowser", systemImage: "safari")
- .frame(maxWidth: .infinity)
- .padding(.vertical, FlixySpacing.regular)
- }
- .buttonStyle(.borderedProminent)
- .tint(accent)
-
- HStack(spacing: FlixySpacing.snug) {
- ProgressView().controlSize(.small)
- Text("signIn.waiting")
- .font(.flixyCaption)
- .foregroundStyle(Color.flixyTextMuted)
- }
-
- Button("action.cancel", action: onCancel)
- .font(.flixyCaption)
- .foregroundStyle(Color.flixyTextSecondary)
- }
- }
}
diff --git a/Packages/FlixyKit/Sources/PlexKit/Auth/PlexPin.swift b/Packages/FlixyKit/Sources/PlexKit/Auth/PlexPin.swift
index 12c0d15..7e6cf9e 100644
--- a/Packages/FlixyKit/Sources/PlexKit/Auth/PlexPin.swift
+++ b/Packages/FlixyKit/Sources/PlexKit/Auth/PlexPin.swift
@@ -52,10 +52,17 @@ public struct PlexPin: Sendable, Equatable, Decodable {
/// antwortet mit „Diese Anfrage konnte nicht abgeschlossen werden".
/// Festgehalten in `AuthorizationURLTests`.
///
- /// `forwardUrl` fehlt bewusst: der Rücksprung in die App wäre bequemer,
- /// verlangt aber ein eigenes URL-Schema. Solange die App den Code ohnehin
- /// im Hintergrund abfragt, merkt niemand einen Unterschied.
- public func authorizationURL(clientIdentifier: String, product: String) -> URL? {
+ /// `forwardUrl` schickt den Browser nach der Anmeldung auf eine Adresse
+ /// mit eigenem Schema zurück. Genau darauf wartet
+ /// `ASWebAuthenticationSession`: das Anmeldefenster schließt sich von
+ /// selbst, und die App weiß, dass sie den Token abholen kann. Ohne diese
+ /// Rückadresse bliebe die Seite offen und der Nutzer müsste raten, wann er
+ /// fertig ist.
+ public func authorizationURL(
+ clientIdentifier: String,
+ product: String,
+ forwardURL: URL? = nil
+ ) -> URL? {
// `&`, `=` und `+` müssen raus: sie trennen Parameter bzw. stehen für
// ein Leerzeichen. In einem Wert würden sie die Adresse zerreißen.
let allowed = CharacterSet.urlQueryAllowed.subtracting(CharacterSet(charactersIn: "&=+"))
@@ -68,9 +75,22 @@ public struct PlexPin: Sendable, Equatable, Decodable {
let product = encode(product)
else { return nil }
- return URL(string: """
+ var text = """
https://app.plex.tv/auth#?clientID=\(client)&code=\(code)\
&context%5Bdevice%5D%5Bproduct%5D=\(product)
- """)
+ """
+
+ // Strenger kodiert als die übrigen Werte: die Rückadresse enthält
+ // `:` und `/`, und die lässt `urlQueryAllowed` durch. Als Wert
+ // innerhalb einer Adresse, die Plex selbst noch einmal zerlegt, müssen
+ // sie kodiert sein — sonst endet der Wert für Plex beim ersten
+ // Schrägstrich.
+ let unreserved = CharacterSet.alphanumerics.union(CharacterSet(charactersIn: "-._~"))
+ if let forwardURL,
+ let forward = forwardURL.absoluteString.addingPercentEncoding(withAllowedCharacters: unreserved) {
+ text += "&forwardUrl=\(forward)"
+ }
+
+ return URL(string: text)
}
}
diff --git a/Packages/FlixyKit/Tests/PlexKitTests/AuthorizationURLTests.swift b/Packages/FlixyKit/Tests/PlexKitTests/AuthorizationURLTests.swift
index eb0275d..e0c3e37 100644
--- a/Packages/FlixyKit/Tests/PlexKitTests/AuthorizationURLTests.swift
+++ b/Packages/FlixyKit/Tests/PlexKitTests/AuthorizationURLTests.swift
@@ -39,3 +39,43 @@ struct AuthorizationURLTests {
#expect(!url.absoluteString.contains("Flixy Player"))
}
}
+
+@Suite("Anmelde-Adresse mit Rücksprung")
+struct AuthorizationForwardURLTests {
+ private let pin = PlexPin(id: 42, code: "5474")
+
+ @Test("Ohne Rückadresse bleibt die Adresse wie bisher")
+ func withoutForwardURL() throws {
+ let url = try #require(pin.authorizationURL(clientIdentifier: "abc", product: "Flixy"))
+ #expect(!url.absoluteString.contains("forwardUrl"))
+ }
+
+ @Test("Die Rückadresse wird angehängt und kodiert")
+ func forwardURLIsAppendedAndEncoded() throws {
+ // Das Schema enthält Doppelpunkte und Schrägstriche. Unkodiert
+ // zerrissen sie die Adresse, und Plex führte den Rücksprung nie aus —
+ // das Anmeldefenster bliebe offen stehen.
+ let url = try #require(pin.authorizationURL(
+ clientIdentifier: "abc",
+ product: "Flixy",
+ forwardURL: URL(string: "com.scarriffleservices.flixy://auth")!
+ ))
+
+ let text = url.absoluteString
+ #expect(text.contains("forwardUrl=com.scarriffleservices.flixy%3A%2F%2Fauth"))
+ #expect(!text.contains("%25"))
+ }
+
+ @Test("Die übrigen Parameter bleiben unversehrt")
+ func otherParametersSurvive() throws {
+ let url = try #require(pin.authorizationURL(
+ clientIdentifier: "abc",
+ product: "Flixy",
+ forwardURL: URL(string: "flixy://auth")!
+ ))
+ let text = url.absoluteString
+ #expect(text.contains("clientID=abc"))
+ #expect(text.contains("code=5474"))
+ #expect(text.contains("context%5Bdevice%5D%5Bproduct%5D=Flixy"))
+ }
+}
diff --git a/project.yml b/project.yml
index 2cac1eb..19d809c 100644
--- a/project.yml
+++ b/project.yml
@@ -76,6 +76,14 @@ targets:
UIBackgroundModes:
- audio
+ # Das Schema, über das Plex nach der Anmeldung zurückschickt. Ohne
+ # diesen Eintrag kennt iOS es nicht, und ASWebAuthenticationSession
+ # käme nie zurück — das Anmeldefenster bliebe offen stehen.
+ CFBundleURLTypes:
+ - CFBundleURLName: com.scarriffleservices.flixy.auth
+ CFBundleURLSchemes:
+ - com.scarriffleservices.flixy
+
# Ohne diesen Schlüssel lehnt ActivityKit jede Anfrage ab — und
# zwar wortlos, was die Suche nach dem Grund unangenehm macht.
NSSupportsLiveActivities: true