feat(emby): add Emby as a MediaBrowser backend alongside Jellyfin

Emby is Jellyfin's upstream ancestor and speaks a near-identical MediaBrowser
API, so the existing Jellyfin stack is parameterised by a `MediaBrowserDialect`
rather than forked. `JellyfinClient`, its auth service, endpoint discovery, LAN
discovery, and the add/edit connection screens all take the dialect and keep one
implementation; `MediaBackend.emby` and `ConnectionKind.emby` carry it through
the neutral models, the Drift `kind` discriminator, downloads, and caches.

Every divergence below was measured against a live Emby 4.9.5 server, not
inferred from documentation, and each is documented at its capability getter.
Jellyfin's request strings stay byte-identical so nothing about its behaviour
changes.

Routes and auth
- Emby only accepts the pre-10.9 user-scoped item routes (`/Users/{id}/Items/…`,
  `/Users/{id}/PlayedItems/…`, `/Users/{id}/FavoriteItems/…`); the unprefixed
  forms Jellyfin 10.11 added return 404.
- The API is also served under a legacy `/emby` prefix, and both dialects accept
  the token as `X-Emby-Token` or `api_key=`.
- Emby answers only its own LAN discovery datagram ("who is EmbyServer?") and
  ignores Jellyfin's; its default HTTPS port is 8920.
- No `/QuickConnect` route exists, so Quick Connect stays Jellyfin-only.

Row fields Emby withholds
- `ProductionYear`, `OfficialRating`, `PremiereDate` and `DateCreated` are absent
  from list rows unless named in `Fields`, which would otherwise strip the year
  and age-rating badge from every card in the app.
- `UserData.LastPlayedDate` never appears on a list row under `Fields=UserData`,
  `EnableUserData=true` or the user-scoped `Ids=` form — only on the single-item
  detail route, or when the Emby-specific `UserDataLastPlayedDate` token is
  requested. Without it every recency-ordered surface silently degrades to
  library-add time, and `JellyfinApiCache.applyWatchState` stamps
  `DateTime.now()` on watched rows, so an offline watch-state pull would rewrite
  the cached play time of everything it walked.

Continue Watching and Next Up
- Emby computes Next Up per series only: the library-wide `/Shows/NextUp` query
  returns nothing under every parameter combination tried. The shelf is
  therefore reconstructed from a played-episode recency scan plus one
  `/Shows/NextUp?SeriesId=` per distinct series, bounded by a shared wall clock
  that covers the scan as well — per-request timeouts cannot bound the pass
  because `MediaServerHttpClient` times the connect and receive phases
  independently. Rows are stamped with their series' newest play from the same
  response that ordered them, so no per-series enrichment request is needed.
- `/Shows/NextUp` ignores `NextUpDateCutoff`, and no server-side played-date
  filter exists to delegate to (`MinDatePlayed` and `MinDateLastPlayed` are
  ignored; `MinDateLastSaved`, `MinDateCreated` and `MinPremiereDate` filter
  unrelated dates), so the 365-day window is applied to the scanned dates.
- The resume route returns items with no saved position, including plain next
  episodes, so the Emby resume leg reads from `/Items?Filters=IsResumable`.
- Emby is ahead of Jellyfin in one place: `/Users/{id}/Items/{id}/HideFromResume`
  makes Continue Watching removal a real capability.

Everything else
- `/Sessions/Playing` and `/Sessions/Playing/Progress` reject a body with no
  `PlaySessionId` (HTTP 400), so playback reporting always sends one.
- Passing any `MediaTypes` value to the playlist query returns an empty list.
- There is no aggregate `/Items/Filters` route; the four filter facets are
  reassembled from `/Genres`, `/OfficialRatings`, `/Studios` and `/Tags`.
- Metadata writes take name-pair lists (`Genres: [{'Name': 'Action'}]`); the
  plain string array is accepted and then silently discarded.
- Custom artwork uploads must be base64 text, not raw bytes — which was broken
  for Jellyfin too and is fixed for both.
- Trickplay, media segments and lyrics 404 on Emby, so scrub previews are absent
  and intro/credit markers fall back to chapter names.

Verified against a local Emby 4.9.5 and a Jellyfin 10.11.11 control server:
onboarding, browse, detail, playable stream URLs serving real bytes, subtitle
sidecars, watch-state write and restore, hubs, cross-server aggregation and
search across both backends simultaneously.
This commit is contained in:
edde746
2026-08-05 06:09:26 +02:00
parent f36e20bcad
commit 05fd622968
128 changed files with 4917 additions and 1429 deletions
+3
View File
@@ -0,0 +1,3 @@
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill="currentColor">
<path d="M11.041 0c-.007 0-1.456 1.43-3.219 3.176L4.615 6.352l.512.513.512.512-2.819 2.791L0 12.961l1.83 1.848c1.006 1.016 2.438 2.46 3.182 3.209l1.351 1.359.508-.496c.28-.273.515-.498.524-.498.008 0 1.266 1.264 2.794 2.808L12.97 24l.187-.182c.23-.225 5.007-4.95 5.717-5.656l.52-.516-.502-.513c-.276-.282-.5-.52-.496-.53.003-.009 1.264-1.26 2.802-2.783 1.538-1.522 2.8-2.776 2.803-2.785.005-.012-3.617-3.684-6.107-6.193L17.65 4.6l-.505.505c-.279.278-.517.501-.53.497-.013-.005-1.27-1.267-2.793-2.805A449.655 449.655 0 0011.041 0zM9.223 7.367c.091.038 7.951 4.608 7.957 4.627.003.013-1.781 1.056-3.965 2.32a999.898 999.898 0 01-3.996 2.307c-.019.006-.026-1.266-.026-4.629 0-3.7.007-4.634.03-4.625Z"/>
</svg>

After

Width:  |  Height:  |  Size: 790 B

+41 -10
View File
@@ -1,4 +1,5 @@
import '../media/media_backend.dart'; import '../media/media_backend.dart';
import '../media/media_browser_dialect.dart';
import '../models/plex/plex_home_user.dart'; import '../models/plex/plex_home_user.dart';
import '../services/plex_auth_service.dart'; import '../services/plex_auth_service.dart';
import '../utils/json_utils.dart'; import '../utils/json_utils.dart';
@@ -9,22 +10,38 @@ import '../utils/url_utils.dart';
/// (e.g. database column values). /// (e.g. database column values).
enum ConnectionKind { enum ConnectionKind {
plex, plex,
jellyfin; jellyfin,
emby;
String get id => switch (this) { String get id => switch (this) {
ConnectionKind.plex => 'plex', ConnectionKind.plex => 'plex',
ConnectionKind.jellyfin => 'jellyfin', ConnectionKind.jellyfin => 'jellyfin',
ConnectionKind.emby => 'emby',
}; };
static ConnectionKind fromId(String id) => switch (id) { static ConnectionKind fromId(String id) => switch (id) {
'plex' => ConnectionKind.plex, 'plex' => ConnectionKind.plex,
'jellyfin' => ConnectionKind.jellyfin, 'jellyfin' => ConnectionKind.jellyfin,
'emby' => ConnectionKind.emby,
_ => throw ArgumentError('Unknown ConnectionKind id: $id'), _ => throw ArgumentError('Unknown ConnectionKind id: $id'),
}; };
MediaBackend get backend => switch (this) { MediaBackend get backend => switch (this) {
ConnectionKind.plex => MediaBackend.plex, ConnectionKind.plex => MediaBackend.plex,
ConnectionKind.jellyfin => MediaBackend.jellyfin, ConnectionKind.jellyfin => MediaBackend.jellyfin,
ConnectionKind.emby => MediaBackend.emby,
};
/// The MediaBrowser dialect this kind speaks, or `null` for Plex.
MediaBrowserDialect? get dialect => switch (this) {
ConnectionKind.plex => null,
ConnectionKind.jellyfin => MediaBrowserDialect.jellyfin,
ConnectionKind.emby => MediaBrowserDialect.emby,
};
static ConnectionKind fromDialect(MediaBrowserDialect dialect) => switch (dialect) {
MediaBrowserDialect.jellyfin => ConnectionKind.jellyfin,
MediaBrowserDialect.emby => ConnectionKind.emby,
}; };
} }
@@ -188,7 +205,9 @@ class PlexAccountConnection extends Connection {
} }
} }
/// A single-server Jellyfin connection. /// A single-server connection to a MediaBrowser-family server — Jellyfin or its
/// Emby ancestor. [dialect] selects which of the two wire dialects this
/// connection speaks; every other field has the same meaning on both.
class JellyfinConnection extends Connection { class JellyfinConnection extends Connection {
@override @override
final String id; final String id;
@@ -202,10 +221,14 @@ class JellyfinConnection extends Connection {
@override @override
final DateTime? lastAuthenticatedAt; final DateTime? lastAuthenticatedAt;
/// Which MediaBrowser dialect this server speaks. Drives [kind], [backend]
/// and every route/capability delta in [JellyfinClient].
final MediaBrowserDialect dialect;
/// Active server base URL, no trailing slash. e.g. `https://jellyfin.home.lan`. /// Active server base URL, no trailing slash. e.g. `https://jellyfin.home.lan`.
final String baseUrl; final String baseUrl;
/// Candidate server URLs for this Jellyfin server, with [baseUrl] first. /// Candidate server URLs for this server, with [baseUrl] first.
/// Existing installs only have [baseUrl]; deserialization backfills this. /// Existing installs only have [baseUrl]; deserialization backfills this.
final List<String> baseUrls; final List<String> baseUrls;
@@ -215,7 +238,7 @@ class JellyfinConnection extends Connection {
/// Server's machine identifier (System/Info `Id`). /// Server's machine identifier (System/Info `Id`).
final String serverMachineId; final String serverMachineId;
/// Authenticated Jellyfin user id (UUID). /// Authenticated user id. A UUID on Jellyfin, an opaque hex string on Emby.
final String userId; final String userId;
/// Authenticated user's display name. /// Authenticated user's display name.
@@ -228,13 +251,13 @@ class JellyfinConnection extends Connection {
/// `Authorization: MediaBrowser DeviceId="..."` header). /// `Authorization: MediaBrowser DeviceId="..."` header).
final String deviceId; final String deviceId;
/// Whether this user is a Jellyfin admin (`/Users/{id}.Policy.IsAdministrator`). /// Whether this user is a server admin (`/Users/{id}.Policy.IsAdministrator`).
/// Captured at auth time so the UI can gate admin-only entries (delete, /// Captured at auth time so the UI can gate admin-only entries (delete,
/// match/unmatch, edit metadata) without an extra round-trip. /// match/unmatch, edit metadata) without an extra round-trip.
final bool isAdministrator; final bool isAdministrator;
/// The authenticated user's `PrimaryImageTag`, or `null` when they have no /// The authenticated user's `PrimaryImageTag`, or `null` when they have no
/// profile picture. Jellyfin omits the key entirely in that case, and the /// profile picture. The server omits the key entirely in that case, and the
/// tag is `MD5(imagePath + lastModified)` so it changes on every upload — /// tag is `MD5(imagePath + lastModified)` so it changes on every upload —
/// which makes the derived avatar URL self-invalidating. Captured at auth /// which makes the derived avatar URL self-invalidating. Captured at auth
/// time and refreshed by [JellyfinClient.checkHealth]. /// time and refreshed by [JellyfinClient.checkHealth].
@@ -250,6 +273,7 @@ class JellyfinConnection extends Connection {
required this.userName, required this.userName,
required this.accessToken, required this.accessToken,
required this.deviceId, required this.deviceId,
this.dialect = MediaBrowserDialect.jellyfin,
this.isAdministrator = false, this.isAdministrator = false,
this.primaryImageTag, this.primaryImageTag,
this.status = ConnectionStatus.unknown, this.status = ConnectionStatus.unknown,
@@ -259,7 +283,7 @@ class JellyfinConnection extends Connection {
baseUrls = _normalizeBaseUrls(baseUrl, baseUrls); baseUrls = _normalizeBaseUrls(baseUrl, baseUrls);
@override @override
ConnectionKind get kind => ConnectionKind.jellyfin; ConnectionKind get kind => ConnectionKind.fromDialect(dialect);
@override @override
String get displayName => '$userName · $serverName'; String get displayName => '$userName · $serverName';
@@ -306,11 +330,12 @@ class JellyfinConnection extends Connection {
String? userName, String? userName,
String? accessToken, String? accessToken,
String? deviceId, String? deviceId,
MediaBrowserDialect? dialect,
bool? isAdministrator, bool? isAdministrator,
String? primaryImageTag, String? primaryImageTag,
/// Deleting a Jellyfin profile picture drops `PrimaryImageTag` from the /// Deleting a profile picture drops `PrimaryImageTag` from the user DTO, so
/// user DTO, so a refresh must be able to null the cached value — a bare /// a refresh must be able to null the cached value — a bare
/// `primaryImageTag: null` is indistinguishable from "unchanged". /// `primaryImageTag: null` is indistinguishable from "unchanged".
bool clearPrimaryImageTag = false, bool clearPrimaryImageTag = false,
ConnectionStatus? status, ConnectionStatus? status,
@@ -328,6 +353,7 @@ class JellyfinConnection extends Connection {
userName: userName ?? this.userName, userName: userName ?? this.userName,
accessToken: accessToken ?? this.accessToken, accessToken: accessToken ?? this.accessToken,
deviceId: deviceId ?? this.deviceId, deviceId: deviceId ?? this.deviceId,
dialect: dialect ?? this.dialect,
isAdministrator: isAdministrator ?? this.isAdministrator, isAdministrator: isAdministrator ?? this.isAdministrator,
primaryImageTag: clearPrimaryImageTag ? null : (primaryImageTag ?? this.primaryImageTag), primaryImageTag: clearPrimaryImageTag ? null : (primaryImageTag ?? this.primaryImageTag),
status: status ?? this.status, status: status ?? this.status,
@@ -336,6 +362,9 @@ class JellyfinConnection extends Connection {
); );
} }
/// The persisted payload deliberately omits [dialect]: the `connections.kind`
/// column is the authoritative, indexed discriminator and
/// [JellyfinConnection.fromConfigJson] receives it from there.
@override @override
Map<String, Object?> toConfigJson() { Map<String, Object?> toConfigJson() {
return { return {
@@ -358,6 +387,7 @@ class JellyfinConnection extends Connection {
required ConnectionStatus status, required ConnectionStatus status,
required DateTime createdAt, required DateTime createdAt,
DateTime? lastAuthenticatedAt, DateTime? lastAuthenticatedAt,
MediaBrowserDialect dialect = MediaBrowserDialect.jellyfin,
}) { }) {
final rawBaseUrls = json['baseUrls']; final rawBaseUrls = json['baseUrls'];
final baseUrls = rawBaseUrls is List ? rawBaseUrls.whereType<String>().toList(growable: false) : const <String>[]; final baseUrls = rawBaseUrls is List ? rawBaseUrls.whereType<String>().toList(growable: false) : const <String>[];
@@ -369,7 +399,8 @@ class JellyfinConnection extends Connection {
id: id, id: id,
baseUrl: baseUrl, baseUrl: baseUrl,
baseUrls: baseUrls, baseUrls: baseUrls,
serverName: json['serverName'] as String? ?? 'Jellyfin', dialect: dialect,
serverName: json['serverName'] as String? ?? dialect.productName,
serverMachineId: json['serverMachineId'] as String? ?? '', serverMachineId: json['serverMachineId'] as String? ?? '',
userId: json['userId'] as String? ?? '', userId: json['userId'] as String? ?? '',
userName: json['userName'] as String? ?? '', userName: json['userName'] as String? ?? '',
+2 -1
View File
@@ -161,12 +161,13 @@ class ConnectionRegistry {
createdAt: createdAt, createdAt: createdAt,
lastAuthenticatedAt: lastAuth, lastAuthenticatedAt: lastAuth,
), ),
ConnectionKind.jellyfin => JellyfinConnection.fromConfigJson( ConnectionKind.jellyfin || ConnectionKind.emby => JellyfinConnection.fromConfigJson(
id: row.id, id: row.id,
json: revealed.config, json: revealed.config,
status: ConnectionStatus.unknown, status: ConnectionStatus.unknown,
createdAt: createdAt, createdAt: createdAt,
lastAuthenticatedAt: lastAuth, lastAuthenticatedAt: lastAuth,
dialect: kind.dialect!,
), ),
}; };
if (revealed.migrated) { if (revealed.migrated) {
+1 -1
View File
@@ -235,7 +235,7 @@ class AppDatabase extends _$AppDatabase {
static bool _containsPlaintextConnectionCredential(String kind, Map<String, dynamic> config) { static bool _containsPlaintextConnectionCredential(String kind, Map<String, dynamic> config) {
bool isPlaintext(Object? value) => value is String && value.isNotEmpty && !CredentialVault.isProtected(value); bool isPlaintext(Object? value) => value is String && value.isNotEmpty && !CredentialVault.isProtected(value);
if (kind == 'jellyfin') return isPlaintext(config['accessToken']); if (kind == 'jellyfin' || kind == 'emby') return isPlaintext(config['accessToken']);
if (kind != 'plex') return false; if (kind != 'plex') return false;
if (isPlaintext(config['accountToken'])) return true; if (isPlaintext(config['accountToken'])) return true;
final servers = config['servers']; final servers = config['servers'];
+1 -1
View File
@@ -4497,7 +4497,7 @@ class ConnectionRow extends DataClass implements Insertable<ConnectionRow> {
/// (one per account); for Jellyfin it's the server's machineId. /// (one per account); for Jellyfin it's the server's machineId.
final String id; final String id;
/// Backend kind: `'plex'` or `'jellyfin'`. /// Backend kind: `'plex'`, `'jellyfin'`, or `'emby'`.
final String kind; final String kind;
/// User-visible label (account email, server name). /// User-visible label (account email, server name).
+37 -26
View File
@@ -3,6 +3,7 @@ import 'dart:convert';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import '../media/ids.dart'; import '../media/ids.dart';
import '../media/media_backend.dart';
import 'app_database.dart'; import 'app_database.dart';
import '../models/download_models.dart'; import '../models/download_models.dart';
@@ -202,22 +203,30 @@ extension DownloadDatabaseOperations on AppDatabase {
final connectionRows = await select(connections).get(); final connectionRows = await select(connections).get();
final connectionIds = connectionRows.map((row) => row.id).toSet(); final connectionIds = connectionRows.map((row) => row.id).toSet();
final connectionKindsById = {for (final row in connectionRows) row.id: row.kind}; final connectionKindsById = {for (final row in connectionRows) row.id: row.kind};
final jellyfinIdentities = <String, ({String machineId, String? userId})>{}; final mediaBrowserIdentities = <String, ({String machineId, String? userId, String backendId})>{};
final jellyfinMachineIds = <String>{}; final mediaBrowserMachineIds = <String>{};
for (final connection in connectionRows.where((row) => row.kind == 'jellyfin')) { // `Connections.kind` is the authoritative dialect discriminator; both
final identity = _jellyfinConnectionIdentity(connection); // MediaBrowser kinds use the same compound machine/user scope shape.
jellyfinIdentities[connection.id] = identity; for (final connection in connectionRows.where(
jellyfinMachineIds.add(identity.machineId); (row) => row.kind == MediaBackend.jellyfin.id || row.kind == MediaBackend.emby.id,
)) {
final identity = _mediaBrowserConnectionIdentity(connection);
mediaBrowserIdentities[connection.id] = (
machineId: identity.machineId,
userId: identity.userId,
backendId: connection.kind,
);
mediaBrowserMachineIds.add(identity.machineId);
} }
final jellyfinScopesByProfileAndMachine = <String, Map<String, Set<String>>>{}; final mediaBrowserScopesByProfileAndMachine = <String, Map<String, Set<({String scopeId, String backendId})>>>{};
for (final binding in await select(profileConnections).get()) { for (final binding in await select(profileConnections).get()) {
if (binding.userIdentifier.isEmpty) continue; if (binding.userIdentifier.isEmpty) continue;
final identity = jellyfinIdentities[binding.connectionId]; final identity = mediaBrowserIdentities[binding.connectionId];
if (identity == null || identity.userId != null && identity.userId != binding.userIdentifier) continue; if (identity == null || identity.userId != null && identity.userId != binding.userIdentifier) continue;
jellyfinScopesByProfileAndMachine mediaBrowserScopesByProfileAndMachine
.putIfAbsent(binding.profileId, () => <String, Set<String>>{}) .putIfAbsent(binding.profileId, () => <String, Set<({String scopeId, String backendId})>>{})
.putIfAbsent(identity.machineId, () => <String>{}) .putIfAbsent(identity.machineId, () => <({String scopeId, String backendId})>{})
.add('${identity.machineId}/${binding.userIdentifier}'); .add((scopeId: '${identity.machineId}/${binding.userIdentifier}', backendId: identity.backendId));
} }
final ownedKeys = <String>{ final ownedKeys = <String>{
for (final owner in owners) for (final owner in owners)
@@ -239,35 +248,37 @@ extension DownloadDatabaseOperations on AppDatabase {
await addDownloadOwner( await addDownloadOwner(
profileId: profileId, profileId: profileId,
globalKey: row.globalKey, globalKey: row.globalKey,
backendId: 'plex', backendId: MediaBackend.plex.id,
clientScopeId: scopeId, clientScopeId: scopeId,
); );
continue; continue;
} }
final jellyfinScopes = jellyfinScopesByProfileAndMachine[profileId]?[row.serverId] ?? const <String>{}; final mediaBrowserScopes =
if (jellyfinScopes.length == 1) { mediaBrowserScopesByProfileAndMachine[profileId]?[row.serverId] ??
final adoptingScope = jellyfinScopes.single; const <({String scopeId, String backendId})>{};
if (mediaBrowserScopes.length == 1) {
final adopting = mediaBrowserScopes.single;
await transaction(() async { await transaction(() async {
if (isStillActive != null && !isStillActive()) return; if (isStillActive != null && !isStillActive()) return;
await updateDownloadedMediaClientScope(row.globalKey, adoptingScope); await updateDownloadedMediaClientScope(row.globalKey, adopting.scopeId);
await addDownloadOwner( await addDownloadOwner(
profileId: profileId, profileId: profileId,
globalKey: row.globalKey, globalKey: row.globalKey,
backendId: 'jellyfin', backendId: adopting.backendId,
clientScopeId: adoptingScope, clientScopeId: adopting.scopeId,
); );
}); });
continue; continue;
} }
// A compound non-Plex scope is a legacy Jellyfin user namespace. // A compound non-Plex scope is a legacy MediaBrowser user namespace.
// Never attach it to another profile unless that profile has exactly // Never attach it to another profile unless that profile has exactly
// one matching Jellyfin binding. The same applies when persisted // one matching MediaBrowser binding. The same applies when persisted
// Jellyfin connections identify the machine but the profile has zero // MediaBrowser connections identify the machine but the profile has
// or multiple possible users. // zero or multiple possible users.
final hasLegacyJellyfinScope = scopeId?.startsWith('${row.serverId}/') ?? false; final hasLegacyMediaBrowserScope = scopeId?.startsWith('${row.serverId}/') ?? false;
if (hasLegacyJellyfinScope || jellyfinMachineIds.contains(row.serverId)) continue; if (hasLegacyMediaBrowserScope || mediaBrowserMachineIds.contains(row.serverId)) continue;
final backendId = connectionKindsById[scopeId]; final backendId = connectionKindsById[scopeId];
await addDownloadOwner( await addDownloadOwner(
@@ -694,7 +705,7 @@ bool _isValidDownloadOwner(
return localProfileIds.isEmpty; return localProfileIds.isEmpty;
} }
({String machineId, String? userId}) _jellyfinConnectionIdentity(ConnectionRow connection) { ({String machineId, String? userId}) _mediaBrowserConnectionIdentity(ConnectionRow connection) {
final separator = connection.id.indexOf('/'); final separator = connection.id.indexOf('/');
var machineId = separator < 0 ? connection.id : connection.id.substring(0, separator); var machineId = separator < 0 ? connection.id : connection.id.substring(0, separator);
String? userId = separator < 0 || separator == connection.id.length - 1 String? userId = separator < 0 || separator == connection.id.length - 1
+3 -3
View File
@@ -131,8 +131,8 @@ class SyncRuleDownloads extends Table {
/// Persisted media-server connections. /// Persisted media-server connections.
/// ///
/// One row per "connection" the user has added — a Plex account (with its /// One row per "connection" the user has added — a Plex account (with its
/// discovered servers and active Home profile) or a single Jellyfin server. /// discovered servers and active Home profile) or a single MediaBrowser
/// The [configJson] payload is backend-specific and parsed by the /// server/user. The [configJson] payload is backend-specific and parsed by the
/// [Connection] sealed class. /// [Connection] sealed class.
@DataClassName('ConnectionRow') @DataClassName('ConnectionRow')
@TableIndex(name: 'idx_connections_kind', columns: {#kind}) @TableIndex(name: 'idx_connections_kind', columns: {#kind})
@@ -141,7 +141,7 @@ class Connections extends Table {
/// (one per account); for Jellyfin it's the server's machineId. /// (one per account); for Jellyfin it's the server's machineId.
TextColumn get id => text()(); TextColumn get id => text()();
/// Backend kind: `'plex'` or `'jellyfin'`. /// Backend kind: `'plex'`, `'jellyfin'`, or `'emby'`.
TextColumn get kind => text()(); TextColumn get kind => text()();
/// User-visible label (account email, server name). /// User-visible label (account email, server name).
+10 -10
View File
@@ -11,7 +11,7 @@
"waitingForAuth": "Təsdiqləmə gözlənilir...\nSəyahətçinizdən (brauzer) daxil olun.", "waitingForAuth": "Təsdiqləmə gözlənilir...\nSəyahətçinizdən (brauzer) daxil olun.",
"useBrowser": "Səyahətçini istifadə et", "useBrowser": "Səyahətçini istifadə et",
"or": "və ya", "or": "və ya",
"connectToJellyfin": "Jellyfin-ə qoşul", "connectToMediaBrowser": "",
"useQuickConnect": "Sürətli Qoşulmanı istifadə et", "useQuickConnect": "Sürətli Qoşulmanı istifadə et",
"quickConnectInstructions": "Jellyfin-də Sürətli Qoşulmanı açın və bu kodu daxil edin.", "quickConnectInstructions": "Jellyfin-də Sürətli Qoşulmanı açın və bu kodu daxil edin.",
"quickConnectWaiting": "Təsdiq gözlənilir…", "quickConnectWaiting": "Təsdiq gözlənilir…",
@@ -827,8 +827,8 @@
"sessionExpiredOne": "${name} üçün seansın vaxtı bitdi", "sessionExpiredOne": "${name} üçün seansın vaxtı bitdi",
"sessionExpiredMany": "${count} server üçün seansın vaxtı bitdi", "sessionExpiredMany": "${count} server üçün seansın vaxtı bitdi",
"signInAgain": "Yenidən daxil ol", "signInAgain": "Yenidən daxil ol",
"editJellyfinTitle": "Jellyfin qoşulmasını dəyişdir", "editMediaBrowserTitle": "",
"editJellyfinIntro": "${serverName} üçün URL əlavə edin və ya silin." "editMediaBrowserIntro": ""
}, },
"discover": { "discover": {
"title": "Kəşf et", "title": "Kəşf et",
@@ -1873,12 +1873,12 @@
} }
}, },
"addServer": { "addServer": {
"addJellyfinTitle": "Jellyfin serveri əlavə et", "addMediaBrowserTitle": "",
"serverUrls": "Server URL-ləri", "serverUrls": "Server URL-ləri",
"serverUrlsHelper": "Vergüllə ayrılmış bir neçə URL-ə icazə verilir.", "serverUrlsHelper": "Vergüllə ayrılmış bir neçə URL-ə icazə verilir.",
"findServer": "Server tap", "findServer": "Server tap",
"searchingLocalServers": "Yerli Jellyfin serverləri axtarılır...", "searchingLocalMediaBrowserServers": "",
"localServers": "Yerli Jellyfin serverləri", "localMediaBrowserServers": "",
"username": "İstifadəçi adı", "username": "İstifadəçi adı",
"password": "Şifrə", "password": "Şifrə",
"signIn": "Daxil ol", "signIn": "Daxil ol",
@@ -1890,15 +1890,15 @@
"addPlexTitle": "Plex ilə daxil ol", "addPlexTitle": "Plex ilə daxil ol",
"pinExpired": "PIN-in vaxtı bitdi. Lütfən təzədən cəhd edin.", "pinExpired": "PIN-in vaxtı bitdi. Lütfən təzədən cəhd edin.",
"failedToRegisterAccount": "Hesab qeydiyyatı uğursuz oldu: ${error}", "failedToRegisterAccount": "Hesab qeydiyyatı uğursuz oldu: ${error}",
"enterJellyfinUrlError": "Jellyfin server URL-inizi daxil edin", "enterMediaBrowserUrlError": "",
"addConnectionTitle": "Qoşulma əlavə et", "addConnectionTitle": "Qoşulma əlavə et",
"addConnectionTitleScoped": "${name} profilinə əlavə et", "addConnectionTitleScoped": "${name} profilinə əlavə et",
"signInWithPlexCard": "Plex ilə daxil ol", "signInWithPlexCard": "Plex ilə daxil ol",
"signInWithPlexCardSubtitle": "Bu cihazı səlahiyyətləndirin.", "signInWithPlexCardSubtitle": "Bu cihazı səlahiyyətləndirin.",
"signInWithPlexCardSubtitleScoped": "Plex hesabını səlahiyyətləndirin.", "signInWithPlexCardSubtitleScoped": "Plex hesabını səlahiyyətləndirin.",
"connectToJellyfinCard": "Jellyfin-ə qoşul", "connectToMediaBrowserCard": "",
"connectToJellyfinCardSubtitle": "Server URL, istifadəçi adı və şifrənizi daxil edin.", "connectToMediaBrowserCardSubtitle": "",
"connectToJellyfinCardSubtitleScoped": "Jellyfin serverinə daxil olun. ${name} profilinə bağlanır.", "connectToMediaBrowserCardSubtitleScoped": "",
"borrowFromAnotherProfile": "Başqa profildən götür", "borrowFromAnotherProfile": "Başqa profildən götür",
"borrowFromAnotherProfileSubtitle": "Başqa profilin qoşulmasını yenidən istifadə edin." "borrowFromAnotherProfileSubtitle": "Başqa profilin qoşulmasını yenidən istifadə edin."
} }
+10 -10
View File
@@ -11,7 +11,7 @@
"waitingForAuth": "Изчакване на удостоверяване...\nВлезте от браузъра си.", "waitingForAuth": "Изчакване на удостоверяване...\nВлезте от браузъра си.",
"useBrowser": "Използвай браузър", "useBrowser": "Използвай браузър",
"or": "или", "or": "или",
"connectToJellyfin": "Свържи се с Jellyfin", "connectToMediaBrowser": "",
"useQuickConnect": "Използвай Quick Connect", "useQuickConnect": "Използвай Quick Connect",
"quickConnectInstructions": "Отворете Quick Connect в Jellyfin и въведете този код.", "quickConnectInstructions": "Отворете Quick Connect в Jellyfin и въведете този код.",
"quickConnectWaiting": "Изчакване на одобрение…", "quickConnectWaiting": "Изчакване на одобрение…",
@@ -827,8 +827,8 @@
"sessionExpiredOne": "Сесията за ${name} е изтекла", "sessionExpiredOne": "Сесията за ${name} е изтекла",
"sessionExpiredMany": "Сесиите за ${count} сървъра са изтекли", "sessionExpiredMany": "Сесиите за ${count} сървъра са изтекли",
"signInAgain": "Влез отново", "signInAgain": "Влез отново",
"editJellyfinTitle": "Редактиране на Jellyfin връзка", "editMediaBrowserTitle": "",
"editJellyfinIntro": "Добавете или премахнете URL адреси за ${serverName}. Plezy ще използва достъпния URL адрес с най-ниска латентност." "editMediaBrowserIntro": ""
}, },
"discover": { "discover": {
"title": "Открий", "title": "Открий",
@@ -1873,12 +1873,12 @@
} }
}, },
"addServer": { "addServer": {
"addJellyfinTitle": "Добави Jellyfin сървър", "addMediaBrowserTitle": "",
"serverUrls": "URL адреси на сървъра", "serverUrls": "URL адреси на сървъра",
"serverUrlsHelper": "Позволени са няколко URL адреса, разделени със запетаи.", "serverUrlsHelper": "Позволени са няколко URL адреса, разделени със запетаи.",
"findServer": "Намери сървър", "findServer": "Намери сървър",
"searchingLocalServers": "Търсене на локални Jellyfin сървъри...", "searchingLocalMediaBrowserServers": "",
"localServers": "Локални Jellyfin сървъри", "localMediaBrowserServers": "",
"username": "Потребителско име", "username": "Потребителско име",
"password": "Парола", "password": "Парола",
"signIn": "Вход", "signIn": "Вход",
@@ -1890,15 +1890,15 @@
"addPlexTitle": "Вход с Plex", "addPlexTitle": "Вход с Plex",
"pinExpired": "PIN-ът изтече преди вход. Моля, опитайте отново.", "pinExpired": "PIN-ът изтече преди вход. Моля, опитайте отново.",
"failedToRegisterAccount": "Неуспешна регистрация на акаунт: ${error}", "failedToRegisterAccount": "Неуспешна регистрация на акаунт: ${error}",
"enterJellyfinUrlError": "Въведете URL адреса на вашия Jellyfin сървър", "enterMediaBrowserUrlError": "",
"addConnectionTitle": "Добави връзка", "addConnectionTitle": "Добави връзка",
"addConnectionTitleScoped": "Добави към ${name}", "addConnectionTitleScoped": "Добави към ${name}",
"signInWithPlexCard": "Вход с Plex", "signInWithPlexCard": "Вход с Plex",
"signInWithPlexCardSubtitle": "Удостоверете това устройство. Споделените сървъри се добавят.", "signInWithPlexCardSubtitle": "Удостоверете това устройство. Споделените сървъри се добавят.",
"signInWithPlexCardSubtitleScoped": "Удостоверете Plex акаунт. Домашните потребители стават профили.", "signInWithPlexCardSubtitleScoped": "Удостоверете Plex акаунт. Домашните потребители стават профили.",
"connectToJellyfinCard": "Свързване с Jellyfin", "connectToMediaBrowserCard": "",
"connectToJellyfinCardSubtitle": "Въведете URL адрес на сървъра, потребителско име и парола.", "connectToMediaBrowserCardSubtitle": "",
"connectToJellyfinCardSubtitleScoped": "Вход в Jellyfin сървър. Свързва се с ${name}.", "connectToMediaBrowserCardSubtitleScoped": "",
"borrowFromAnotherProfile": "Използвай от друг профил", "borrowFromAnotherProfile": "Използвай от друг профил",
"borrowFromAnotherProfileSubtitle": "Използвай връзка от друг профил. PIN-защитените профили изискват PIN." "borrowFromAnotherProfileSubtitle": "Използвай връзка от друг профил. PIN-защитените профили изискват PIN."
} }
+10 -10
View File
@@ -11,7 +11,7 @@
"waitingForAuth": "Venter på godkendelse...\nLog ind fra din browser.", "waitingForAuth": "Venter på godkendelse...\nLog ind fra din browser.",
"useBrowser": "Brug browseren", "useBrowser": "Brug browseren",
"or": "eller", "or": "eller",
"connectToJellyfin": "Forbind til Jellyfin", "connectToMediaBrowser": "",
"useQuickConnect": "Brug Quick Connect", "useQuickConnect": "Brug Quick Connect",
"quickConnectInstructions": "Åbn Quick Connect i Jellyfin, og indtast denne kode.", "quickConnectInstructions": "Åbn Quick Connect i Jellyfin, og indtast denne kode.",
"quickConnectWaiting": "Venter på godkendelse…", "quickConnectWaiting": "Venter på godkendelse…",
@@ -827,8 +827,8 @@
"sessionExpiredOne": "Sessionen er udløbet for ${name}", "sessionExpiredOne": "Sessionen er udløbet for ${name}",
"sessionExpiredMany": "Sessionerne er udløbet for ${count} servere", "sessionExpiredMany": "Sessionerne er udløbet for ${count} servere",
"signInAgain": "Log ind igen", "signInAgain": "Log ind igen",
"editJellyfinTitle": "Rediger Jellyfin-forbindelse", "editMediaBrowserTitle": "",
"editJellyfinIntro": "Tilføj eller fjern URL'er for ${serverName}. Plezy bruger den tilgængelige URL med lavest latenstid." "editMediaBrowserIntro": ""
}, },
"discover": { "discover": {
"title": "Opdag", "title": "Opdag",
@@ -1873,12 +1873,12 @@
} }
}, },
"addServer": { "addServer": {
"addJellyfinTitle": "Tilføj Jellyfin-server", "addMediaBrowserTitle": "",
"serverUrls": "Server-URL'er", "serverUrls": "Server-URL'er",
"serverUrlsHelper": "Du kan angive flere URL'er adskilt med komma.", "serverUrlsHelper": "Du kan angive flere URL'er adskilt med komma.",
"findServer": "Find server", "findServer": "Find server",
"searchingLocalServers": "Søger efter lokale Jellyfin-servere...", "searchingLocalMediaBrowserServers": "",
"localServers": "Lokale Jellyfin-servere", "localMediaBrowserServers": "",
"username": "Brugernavn", "username": "Brugernavn",
"password": "Adgangskode", "password": "Adgangskode",
"signIn": "Log ind", "signIn": "Log ind",
@@ -1890,15 +1890,15 @@
"addPlexTitle": "Log ind med Plex", "addPlexTitle": "Log ind med Plex",
"pinExpired": "PIN-koden udløb før login. Prøv igen.", "pinExpired": "PIN-koden udløb før login. Prøv igen.",
"failedToRegisterAccount": "Kunne ikke registrere kontoen: ${error}", "failedToRegisterAccount": "Kunne ikke registrere kontoen: ${error}",
"enterJellyfinUrlError": "Angiv URL'en til din Jellyfin-server", "enterMediaBrowserUrlError": "",
"addConnectionTitle": "Tilføj forbindelse", "addConnectionTitle": "Tilføj forbindelse",
"addConnectionTitleScoped": "Tilføj til ${name}", "addConnectionTitleScoped": "Tilføj til ${name}",
"signInWithPlexCard": "Log ind med Plex", "signInWithPlexCard": "Log ind med Plex",
"signInWithPlexCardSubtitle": "Godkend denne enhed. Delte servere tilføjes.", "signInWithPlexCardSubtitle": "Godkend denne enhed. Delte servere tilføjes.",
"signInWithPlexCardSubtitleScoped": "Godkend en Plex-konto. Plex Home-brugere bliver til profiler.", "signInWithPlexCardSubtitleScoped": "Godkend en Plex-konto. Plex Home-brugere bliver til profiler.",
"connectToJellyfinCard": "Forbind til Jellyfin", "connectToMediaBrowserCard": "",
"connectToJellyfinCardSubtitle": "Indtast din server-URL, dit brugernavn og din adgangskode.", "connectToMediaBrowserCardSubtitle": "",
"connectToJellyfinCardSubtitleScoped": "Log ind på en Jellyfin-server. Serveren knyttes til ${name}.", "connectToMediaBrowserCardSubtitleScoped": "",
"borrowFromAnotherProfile": "Lån fra en anden profil", "borrowFromAnotherProfile": "Lån fra en anden profil",
"borrowFromAnotherProfileSubtitle": "Genbrug en anden profils forbindelse. PIN-beskyttede profiler kræver en PIN." "borrowFromAnotherProfileSubtitle": "Genbrug en anden profils forbindelse. PIN-beskyttede profiler kræver en PIN."
} }
+10 -10
View File
@@ -11,7 +11,7 @@
"waitingForAuth": "Warte auf die Authentifizierung …\nMelde dich über deinen Browser an.", "waitingForAuth": "Warte auf die Authentifizierung …\nMelde dich über deinen Browser an.",
"useBrowser": "Browser verwenden", "useBrowser": "Browser verwenden",
"or": "oder", "or": "oder",
"connectToJellyfin": "Mit Jellyfin verbinden", "connectToMediaBrowser": "",
"useQuickConnect": "Quick Connect verwenden", "useQuickConnect": "Quick Connect verwenden",
"quickConnectInstructions": "Öffne Quick Connect in Jellyfin und gib diesen Code ein.", "quickConnectInstructions": "Öffne Quick Connect in Jellyfin und gib diesen Code ein.",
"quickConnectWaiting": "Warte auf Bestätigung…", "quickConnectWaiting": "Warte auf Bestätigung…",
@@ -827,8 +827,8 @@
"sessionExpiredOne": "Sitzung für ${name} abgelaufen", "sessionExpiredOne": "Sitzung für ${name} abgelaufen",
"sessionExpiredMany": "Sitzungen für ${count} Server abgelaufen", "sessionExpiredMany": "Sitzungen für ${count} Server abgelaufen",
"signInAgain": "Erneut anmelden", "signInAgain": "Erneut anmelden",
"editJellyfinTitle": "Jellyfin-Verbindung bearbeiten", "editMediaBrowserTitle": "",
"editJellyfinIntro": "Füge URLs für ${serverName} hinzu oder entferne sie. Plezy verwendet die erreichbare URL mit der geringsten Latenz." "editMediaBrowserIntro": ""
}, },
"discover": { "discover": {
"title": "Entdecken", "title": "Entdecken",
@@ -1873,12 +1873,12 @@
} }
}, },
"addServer": { "addServer": {
"addJellyfinTitle": "Jellyfin-Server hinzufügen", "addMediaBrowserTitle": "",
"serverUrls": "Server-URLs", "serverUrls": "Server-URLs",
"serverUrlsHelper": "Mehrere URLs möglich, durch Kommas getrennt.", "serverUrlsHelper": "Mehrere URLs möglich, durch Kommas getrennt.",
"findServer": "Server finden", "findServer": "Server finden",
"searchingLocalServers": "Suche nach lokalen Jellyfin-Servern …", "searchingLocalMediaBrowserServers": "",
"localServers": "Lokale Jellyfin-Server", "localMediaBrowserServers": "",
"username": "Benutzername", "username": "Benutzername",
"password": "Passwort", "password": "Passwort",
"signIn": "Anmelden", "signIn": "Anmelden",
@@ -1890,15 +1890,15 @@
"addPlexTitle": "Mit Plex anmelden", "addPlexTitle": "Mit Plex anmelden",
"pinExpired": "PIN ist vor der Anmeldung abgelaufen. Bitte erneut versuchen.", "pinExpired": "PIN ist vor der Anmeldung abgelaufen. Bitte erneut versuchen.",
"failedToRegisterAccount": "Konto konnte nicht registriert werden: ${error}", "failedToRegisterAccount": "Konto konnte nicht registriert werden: ${error}",
"enterJellyfinUrlError": "Gib die URL deines Jellyfin-Servers ein", "enterMediaBrowserUrlError": "",
"addConnectionTitle": "Verbindung hinzufügen", "addConnectionTitle": "Verbindung hinzufügen",
"addConnectionTitleScoped": "Zu ${name} hinzufügen", "addConnectionTitleScoped": "Zu ${name} hinzufügen",
"signInWithPlexCard": "Mit Plex anmelden", "signInWithPlexCard": "Mit Plex anmelden",
"signInWithPlexCardSubtitle": "Dieses Gerät autorisieren. Geteilte Server werden hinzugefügt.", "signInWithPlexCardSubtitle": "Dieses Gerät autorisieren. Geteilte Server werden hinzugefügt.",
"signInWithPlexCardSubtitleScoped": "Ein Plex-Konto autorisieren. Home-Benutzer werden zu Profilen.", "signInWithPlexCardSubtitleScoped": "Ein Plex-Konto autorisieren. Home-Benutzer werden zu Profilen.",
"connectToJellyfinCard": "Mit Jellyfin verbinden", "connectToMediaBrowserCard": "",
"connectToJellyfinCardSubtitle": "Gib Server-URL, Benutzername und Passwort ein.", "connectToMediaBrowserCardSubtitle": "",
"connectToJellyfinCardSubtitleScoped": "Bei einem Jellyfin-Server anmelden. Wird mit ${name} verknüpft.", "connectToMediaBrowserCardSubtitleScoped": "",
"borrowFromAnotherProfile": "Von einem anderen Profil ausleihen", "borrowFromAnotherProfile": "Von einem anderen Profil ausleihen",
"borrowFromAnotherProfileSubtitle": "Verbindung eines anderen Profils wiederverwenden. PIN-geschützte Profile erfordern eine PIN." "borrowFromAnotherProfileSubtitle": "Verbindung eines anderen Profils wiederverwenden. PIN-geschützte Profile erfordern eine PIN."
} }
+14 -14
View File
@@ -11,7 +11,7 @@
"waitingForAuth": "Waiting for authentication...\nSign in from your browser.", "waitingForAuth": "Waiting for authentication...\nSign in from your browser.",
"useBrowser": "Use browser", "useBrowser": "Use browser",
"or": "or", "or": "or",
"connectToJellyfin": "Connect to Jellyfin", "connectToMediaBrowser": "Connect to ${product}",
"useQuickConnect": "Use Quick Connect", "useQuickConnect": "Use Quick Connect",
"quickConnectInstructions": "Open Quick Connect in Jellyfin and enter this code.", "quickConnectInstructions": "Open Quick Connect in Jellyfin and enter this code.",
"quickConnectWaiting": "Waiting for approval…", "quickConnectWaiting": "Waiting for approval…",
@@ -802,7 +802,7 @@
"borrowAddTo": "Add to ${displayName}", "borrowAddTo": "Add to ${displayName}",
"borrowExplain": "Borrow another profile's connection. PIN-protected profiles require a PIN.", "borrowExplain": "Borrow another profile's connection. PIN-protected profiles require a PIN.",
"borrowEmpty": "Nothing to borrow yet.", "borrowEmpty": "Nothing to borrow yet.",
"borrowEmptySubtitle": "Connect Plex or Jellyfin to another profile first.", "borrowEmptySubtitle": "Connect Plex, Jellyfin, or Emby to another profile first.",
"borrowLoadFailed": "Available connections could not be loaded. Try again.", "borrowLoadFailed": "Available connections could not be loaded. Try again.",
"borrowFromProfile": "From ${displayName}", "borrowFromProfile": "From ${displayName}",
"borrowConnectionBorrowed": "Connection borrowed.", "borrowConnectionBorrowed": "Connection borrowed.",
@@ -822,13 +822,13 @@
"connections": { "connections": {
"sectionTitle": "Connections", "sectionTitle": "Connections",
"addConnection": "Add connection", "addConnection": "Add connection",
"addConnectionSubtitleNoProfile": "Sign in with Plex or connect a Jellyfin server", "addConnectionSubtitleNoProfile": "Sign in with Plex or connect a Jellyfin or Emby server",
"addConnectionSubtitleScoped": "Add to ${displayName}: Plex, Jellyfin, or another profile connection", "addConnectionSubtitleScoped": "Add to ${displayName}: Plex, Jellyfin, Emby, or another profile connection",
"sessionExpiredOne": "Session expired for ${name}", "sessionExpiredOne": "Session expired for ${name}",
"sessionExpiredMany": "Session expired for ${count} servers", "sessionExpiredMany": "Session expired for ${count} servers",
"signInAgain": "Sign in again", "signInAgain": "Sign in again",
"editJellyfinTitle": "Edit Jellyfin connection", "editMediaBrowserTitle": "Edit ${product} connection",
"editJellyfinIntro": "Add or remove URLs for ${serverName}. Plezy will use the reachable URL with the lowest latency." "editMediaBrowserIntro": "Add or remove URLs for ${serverName}. Plezy will use the reachable URL with the lowest latency."
}, },
"discover": { "discover": {
"title": "Discover", "title": "Discover",
@@ -966,7 +966,7 @@
"title": "About", "title": "About",
"openSourceLicenses": "Open Source Licenses", "openSourceLicenses": "Open Source Licenses",
"versionLabel": "Version ${version}", "versionLabel": "Version ${version}",
"appDescription": "A beautiful Plex and Jellyfin client for Flutter", "appDescription": "A beautiful Plex, Jellyfin, and Emby client for Flutter",
"viewLicensesDescription": "View licenses of third-party libraries" "viewLicensesDescription": "View licenses of third-party libraries"
}, },
"serverSelection": { "serverSelection": {
@@ -1873,12 +1873,12 @@
} }
}, },
"addServer": { "addServer": {
"addJellyfinTitle": "Add Jellyfin server", "addMediaBrowserTitle": "Add ${product} server",
"serverUrls": "Server URLs", "serverUrls": "Server URLs",
"serverUrlsHelper": "Multiple URLs allowed, separated by commas.", "serverUrlsHelper": "Multiple URLs allowed, separated by commas.",
"findServer": "Find server", "findServer": "Find server",
"searchingLocalServers": "Looking for local Jellyfin servers...", "searchingLocalMediaBrowserServers": "Looking for local ${product} servers...",
"localServers": "Local Jellyfin servers", "localMediaBrowserServers": "Local ${product} servers",
"username": "Username", "username": "Username",
"password": "Password", "password": "Password",
"signIn": "Sign in", "signIn": "Sign in",
@@ -1890,15 +1890,15 @@
"addPlexTitle": "Sign in with Plex", "addPlexTitle": "Sign in with Plex",
"pinExpired": "PIN expired before sign-in. Please try again.", "pinExpired": "PIN expired before sign-in. Please try again.",
"failedToRegisterAccount": "Failed to register account: ${error}", "failedToRegisterAccount": "Failed to register account: ${error}",
"enterJellyfinUrlError": "Enter your Jellyfin server URL", "enterMediaBrowserUrlError": "Enter your ${product} server URL",
"addConnectionTitle": "Add connection", "addConnectionTitle": "Add connection",
"addConnectionTitleScoped": "Add to ${name}", "addConnectionTitleScoped": "Add to ${name}",
"signInWithPlexCard": "Sign in with Plex", "signInWithPlexCard": "Sign in with Plex",
"signInWithPlexCardSubtitle": "Authorize this device. Shared servers are added.", "signInWithPlexCardSubtitle": "Authorize this device. Shared servers are added.",
"signInWithPlexCardSubtitleScoped": "Authorize a Plex account. Home users become profiles.", "signInWithPlexCardSubtitleScoped": "Authorize a Plex account. Home users become profiles.",
"connectToJellyfinCard": "Connect to Jellyfin", "connectToMediaBrowserCard": "Connect to ${product}",
"connectToJellyfinCardSubtitle": "Enter your server URL, username, and password.", "connectToMediaBrowserCardSubtitle": "Enter your server URL, username, and password.",
"connectToJellyfinCardSubtitleScoped": "Sign in to a Jellyfin server. Binds to ${name}.", "connectToMediaBrowserCardSubtitleScoped": "Sign in to your ${product} server. Binds to ${name}.",
"borrowFromAnotherProfile": "Borrow from another profile", "borrowFromAnotherProfile": "Borrow from another profile",
"borrowFromAnotherProfileSubtitle": "Reuse another profile's connection. PIN-protected profiles require a PIN." "borrowFromAnotherProfileSubtitle": "Reuse another profile's connection. PIN-protected profiles require a PIN."
} }
+10 -10
View File
@@ -11,7 +11,7 @@
"waitingForAuth": "Esperando autenticación...\nInicia sesión desde tu navegador.", "waitingForAuth": "Esperando autenticación...\nInicia sesión desde tu navegador.",
"useBrowser": "Usar navegador", "useBrowser": "Usar navegador",
"or": "o", "or": "o",
"connectToJellyfin": "Conectar a Jellyfin", "connectToMediaBrowser": "",
"useQuickConnect": "Usar Quick Connect", "useQuickConnect": "Usar Quick Connect",
"quickConnectInstructions": "Abre Quick Connect en Jellyfin e introduce este código.", "quickConnectInstructions": "Abre Quick Connect en Jellyfin e introduce este código.",
"quickConnectWaiting": "Esperando aprobación…", "quickConnectWaiting": "Esperando aprobación…",
@@ -827,8 +827,8 @@
"sessionExpiredOne": "Sesión caducada para ${name}", "sessionExpiredOne": "Sesión caducada para ${name}",
"sessionExpiredMany": "Sesión caducada para ${count} servidores", "sessionExpiredMany": "Sesión caducada para ${count} servidores",
"signInAgain": "Iniciar sesión de nuevo", "signInAgain": "Iniciar sesión de nuevo",
"editJellyfinTitle": "Editar conexión de Jellyfin", "editMediaBrowserTitle": "",
"editJellyfinIntro": "Añade o elimina direcciones URL para ${serverName}. Plezy usará la dirección accesible con menor latencia." "editMediaBrowserIntro": ""
}, },
"discover": { "discover": {
"title": "Descubrir", "title": "Descubrir",
@@ -1873,12 +1873,12 @@
} }
}, },
"addServer": { "addServer": {
"addJellyfinTitle": "Añadir servidor Jellyfin", "addMediaBrowserTitle": "",
"serverUrls": "Direcciones URL del servidor", "serverUrls": "Direcciones URL del servidor",
"serverUrlsHelper": "Se permiten varias URL, separadas por comas.", "serverUrlsHelper": "Se permiten varias URL, separadas por comas.",
"findServer": "Buscar servidor", "findServer": "Buscar servidor",
"searchingLocalServers": "Buscando servidores Jellyfin locales...", "searchingLocalMediaBrowserServers": "",
"localServers": "Servidores Jellyfin locales", "localMediaBrowserServers": "",
"username": "Usuario", "username": "Usuario",
"password": "Contraseña", "password": "Contraseña",
"signIn": "Iniciar sesión", "signIn": "Iniciar sesión",
@@ -1890,15 +1890,15 @@
"addPlexTitle": "Iniciar sesión con Plex", "addPlexTitle": "Iniciar sesión con Plex",
"pinExpired": "El PIN caducó antes de iniciar sesión. Inténtalo de nuevo.", "pinExpired": "El PIN caducó antes de iniciar sesión. Inténtalo de nuevo.",
"failedToRegisterAccount": "No se pudo registrar la cuenta: ${error}", "failedToRegisterAccount": "No se pudo registrar la cuenta: ${error}",
"enterJellyfinUrlError": "Introduce la URL de tu servidor Jellyfin", "enterMediaBrowserUrlError": "",
"addConnectionTitle": "Añadir conexión", "addConnectionTitle": "Añadir conexión",
"addConnectionTitleScoped": "Añadir a ${name}", "addConnectionTitleScoped": "Añadir a ${name}",
"signInWithPlexCard": "Iniciar sesión con Plex", "signInWithPlexCard": "Iniciar sesión con Plex",
"signInWithPlexCardSubtitle": "Autoriza este dispositivo. Se añaden servidores compartidos.", "signInWithPlexCardSubtitle": "Autoriza este dispositivo. Se añaden servidores compartidos.",
"signInWithPlexCardSubtitleScoped": "Autoriza una cuenta Plex. Los usuarios de Home se convierten en perfiles.", "signInWithPlexCardSubtitleScoped": "Autoriza una cuenta Plex. Los usuarios de Home se convierten en perfiles.",
"connectToJellyfinCard": "Conectar a Jellyfin", "connectToMediaBrowserCard": "",
"connectToJellyfinCardSubtitle": "Introduce la URL del servidor, usuario y contraseña.", "connectToMediaBrowserCardSubtitle": "",
"connectToJellyfinCardSubtitleScoped": "Inicia sesión en un servidor Jellyfin. Se vincula a ${name}.", "connectToMediaBrowserCardSubtitleScoped": "",
"borrowFromAnotherProfile": "Tomar prestado de otro perfil", "borrowFromAnotherProfile": "Tomar prestado de otro perfil",
"borrowFromAnotherProfileSubtitle": "Reutiliza la conexión de otro perfil. Los perfiles protegidos con PIN requieren un PIN." "borrowFromAnotherProfileSubtitle": "Reutiliza la conexión de otro perfil. Los perfiles protegidos con PIN requieren un PIN."
} }
+10 -10
View File
@@ -11,7 +11,7 @@
"waitingForAuth": "En attente d'authentification...\nConnectez-vous depuis votre navigateur.", "waitingForAuth": "En attente d'authentification...\nConnectez-vous depuis votre navigateur.",
"useBrowser": "Utiliser le navigateur", "useBrowser": "Utiliser le navigateur",
"or": "ou", "or": "ou",
"connectToJellyfin": "Se connecter à Jellyfin", "connectToMediaBrowser": "",
"useQuickConnect": "Utiliser Quick Connect", "useQuickConnect": "Utiliser Quick Connect",
"quickConnectInstructions": "Ouvrez Quick Connect dans Jellyfin et saisissez ce code.", "quickConnectInstructions": "Ouvrez Quick Connect dans Jellyfin et saisissez ce code.",
"quickConnectWaiting": "En attente d'approbation…", "quickConnectWaiting": "En attente d'approbation…",
@@ -827,8 +827,8 @@
"sessionExpiredOne": "Session expirée pour ${name}", "sessionExpiredOne": "Session expirée pour ${name}",
"sessionExpiredMany": "Session expirée pour ${count} serveurs", "sessionExpiredMany": "Session expirée pour ${count} serveurs",
"signInAgain": "Se reconnecter", "signInAgain": "Se reconnecter",
"editJellyfinTitle": "Modifier la connexion Jellyfin", "editMediaBrowserTitle": "",
"editJellyfinIntro": "Ajoutez ou supprimez des URL pour ${serverName}. Plezy utilisera l'URL joignable avec la latence la plus faible." "editMediaBrowserIntro": ""
}, },
"discover": { "discover": {
"title": "Découvrir", "title": "Découvrir",
@@ -1873,12 +1873,12 @@
} }
}, },
"addServer": { "addServer": {
"addJellyfinTitle": "Ajouter un serveur Jellyfin", "addMediaBrowserTitle": "",
"serverUrls": "URL du serveur", "serverUrls": "URL du serveur",
"serverUrlsHelper": "Plusieurs URL possibles, séparées par des virgules.", "serverUrlsHelper": "Plusieurs URL possibles, séparées par des virgules.",
"findServer": "Rechercher un serveur", "findServer": "Rechercher un serveur",
"searchingLocalServers": "Recherche de serveurs Jellyfin locaux...", "searchingLocalMediaBrowserServers": "",
"localServers": "Serveurs Jellyfin locaux", "localMediaBrowserServers": "",
"username": "Nom d'utilisateur", "username": "Nom d'utilisateur",
"password": "Mot de passe", "password": "Mot de passe",
"signIn": "Se connecter", "signIn": "Se connecter",
@@ -1890,15 +1890,15 @@
"addPlexTitle": "Se connecter avec Plex", "addPlexTitle": "Se connecter avec Plex",
"pinExpired": "Le PIN a expiré avant la connexion. Veuillez réessayer.", "pinExpired": "Le PIN a expiré avant la connexion. Veuillez réessayer.",
"failedToRegisterAccount": "Échec de l'enregistrement du compte : ${error}", "failedToRegisterAccount": "Échec de l'enregistrement du compte : ${error}",
"enterJellyfinUrlError": "Saisissez l'URL de votre serveur Jellyfin", "enterMediaBrowserUrlError": "",
"addConnectionTitle": "Ajouter une connexion", "addConnectionTitle": "Ajouter une connexion",
"addConnectionTitleScoped": "Ajouter à ${name}", "addConnectionTitleScoped": "Ajouter à ${name}",
"signInWithPlexCard": "Se connecter avec Plex", "signInWithPlexCard": "Se connecter avec Plex",
"signInWithPlexCardSubtitle": "Autorisez cet appareil. Les serveurs partagés sont ajoutés.", "signInWithPlexCardSubtitle": "Autorisez cet appareil. Les serveurs partagés sont ajoutés.",
"signInWithPlexCardSubtitleScoped": "Autorisez un compte Plex. Les utilisateurs Home deviennent des profils.", "signInWithPlexCardSubtitleScoped": "Autorisez un compte Plex. Les utilisateurs Home deviennent des profils.",
"connectToJellyfinCard": "Se connecter à Jellyfin", "connectToMediaBrowserCard": "",
"connectToJellyfinCardSubtitle": "Saisissez l'URL du serveur, le nom d'utilisateur et le mot de passe.", "connectToMediaBrowserCardSubtitle": "",
"connectToJellyfinCardSubtitleScoped": "Connectez-vous à un serveur Jellyfin. Cette connexion sera liée à ${name}.", "connectToMediaBrowserCardSubtitleScoped": "",
"borrowFromAnotherProfile": "Emprunter à un autre profil", "borrowFromAnotherProfile": "Emprunter à un autre profil",
"borrowFromAnotherProfileSubtitle": "Réutiliser la connexion d'un autre profil. Les profils protégés par PIN exigent un PIN." "borrowFromAnotherProfileSubtitle": "Réutiliser la connexion d'un autre profil. Les profils protégés par PIN exigent un PIN."
} }
+10 -10
View File
@@ -11,7 +11,7 @@
"waitingForAuth": "Várakozás a hitelesítésre...\nJelentkezz be a böngésződben.", "waitingForAuth": "Várakozás a hitelesítésre...\nJelentkezz be a böngésződben.",
"useBrowser": "Böngésző használata", "useBrowser": "Böngésző használata",
"or": "vagy", "or": "vagy",
"connectToJellyfin": "Csatlakozás Jellyfinhez", "connectToMediaBrowser": "",
"useQuickConnect": "Quick Connect használata", "useQuickConnect": "Quick Connect használata",
"quickConnectInstructions": "Nyisd meg a Quick Connect-et a Jellyfinben, és add meg ezt a kódot.", "quickConnectInstructions": "Nyisd meg a Quick Connect-et a Jellyfinben, és add meg ezt a kódot.",
"quickConnectWaiting": "Várakozás a jóváhagyásra…", "quickConnectWaiting": "Várakozás a jóváhagyásra…",
@@ -827,8 +827,8 @@
"sessionExpiredOne": "A(z) ${name} munkamenete lejárt", "sessionExpiredOne": "A(z) ${name} munkamenete lejárt",
"sessionExpiredMany": "${count} szerver munkamenete lejárt", "sessionExpiredMany": "${count} szerver munkamenete lejárt",
"signInAgain": "Bejelentkezés újra", "signInAgain": "Bejelentkezés újra",
"editJellyfinTitle": "Jellyfin kapcsolat szerkesztése", "editMediaBrowserTitle": "",
"editJellyfinIntro": "URL-ek hozzáadása vagy eltávolítása ehhez: ${serverName}. A Plezy a legalacsonyabb késleltetésű, elérhető URL-t fogja használni." "editMediaBrowserIntro": ""
}, },
"discover": { "discover": {
"title": "Felfedezés", "title": "Felfedezés",
@@ -1873,12 +1873,12 @@
} }
}, },
"addServer": { "addServer": {
"addJellyfinTitle": "Jellyfin szerver hozzáadása", "addMediaBrowserTitle": "",
"serverUrls": "Szerver URL-címei", "serverUrls": "Szerver URL-címei",
"serverUrlsHelper": "Több URL is megadható, vesszővel elválasztva.", "serverUrlsHelper": "Több URL is megadható, vesszővel elválasztva.",
"findServer": "Szerver keresése", "findServer": "Szerver keresése",
"searchingLocalServers": "Helyi Jellyfin-szerverek keresése...", "searchingLocalMediaBrowserServers": "",
"localServers": "Helyi Jellyfin-szerverek", "localMediaBrowserServers": "",
"username": "Felhasználónév", "username": "Felhasználónév",
"password": "Jelszó", "password": "Jelszó",
"signIn": "Bejelentkezés", "signIn": "Bejelentkezés",
@@ -1890,15 +1890,15 @@
"addPlexTitle": "Bejelentkezés Plexszel", "addPlexTitle": "Bejelentkezés Plexszel",
"pinExpired": "A PIN-kód a bejelentkezés előtt lejárt. Próbáld újra.", "pinExpired": "A PIN-kód a bejelentkezés előtt lejárt. Próbáld újra.",
"failedToRegisterAccount": "Nem sikerült a fiók regisztrálása: ${error}", "failedToRegisterAccount": "Nem sikerült a fiók regisztrálása: ${error}",
"enterJellyfinUrlError": "Add meg a Jellyfin szervered URL-jét", "enterMediaBrowserUrlError": "",
"addConnectionTitle": "Kapcsolat hozzáadása", "addConnectionTitle": "Kapcsolat hozzáadása",
"addConnectionTitleScoped": "Hozzáadás a következőhöz: ${name}", "addConnectionTitleScoped": "Hozzáadás a következőhöz: ${name}",
"signInWithPlexCard": "Bejelentkezés Plexszel", "signInWithPlexCard": "Bejelentkezés Plexszel",
"signInWithPlexCardSubtitle": "Eszköz engedélyezése. A megosztott szerverek hozzáadásra kerülnek.", "signInWithPlexCardSubtitle": "Eszköz engedélyezése. A megosztott szerverek hozzáadásra kerülnek.",
"signInWithPlexCardSubtitleScoped": "Plex-fiók engedélyezése. A Plex Home-felhasználókból profilok lesznek.", "signInWithPlexCardSubtitleScoped": "Plex-fiók engedélyezése. A Plex Home-felhasználókból profilok lesznek.",
"connectToJellyfinCard": "Csatlakozás Jellyfinhez", "connectToMediaBrowserCard": "",
"connectToJellyfinCardSubtitle": "Add meg a szerver URL-jét, felhasználónevedet és jelszavadat.", "connectToMediaBrowserCardSubtitle": "",
"connectToJellyfinCardSubtitleScoped": "Bejelentkezés egy Jellyfin-szerverre. Hozzárendelés ehhez: ${name}.", "connectToMediaBrowserCardSubtitleScoped": "",
"borrowFromAnotherProfile": "Kapcsolat használata másik profilból", "borrowFromAnotherProfile": "Kapcsolat használata másik profilból",
"borrowFromAnotherProfileSubtitle": "Egy másik profil kapcsolatának használata. A PIN-kóddal védett profilokhoz PIN-kód szükséges." "borrowFromAnotherProfileSubtitle": "Egy másik profil kapcsolatának használata. A PIN-kóddal védett profilokhoz PIN-kód szükséges."
} }
+10 -10
View File
@@ -11,7 +11,7 @@
"waitingForAuth": "In attesa di autenticazione...\nAccedi dal browser.", "waitingForAuth": "In attesa di autenticazione...\nAccedi dal browser.",
"useBrowser": "Usa il browser", "useBrowser": "Usa il browser",
"or": "o", "or": "o",
"connectToJellyfin": "Connettiti a Jellyfin", "connectToMediaBrowser": "",
"useQuickConnect": "Usa Quick Connect", "useQuickConnect": "Usa Quick Connect",
"quickConnectInstructions": "Apri Quick Connect in Jellyfin e inserisci questo codice.", "quickConnectInstructions": "Apri Quick Connect in Jellyfin e inserisci questo codice.",
"quickConnectWaiting": "In attesa di approvazione…", "quickConnectWaiting": "In attesa di approvazione…",
@@ -827,8 +827,8 @@
"sessionExpiredOne": "Sessione scaduta per ${name}", "sessionExpiredOne": "Sessione scaduta per ${name}",
"sessionExpiredMany": "Sessione scaduta per ${count} server", "sessionExpiredMany": "Sessione scaduta per ${count} server",
"signInAgain": "Accedi di nuovo", "signInAgain": "Accedi di nuovo",
"editJellyfinTitle": "Modifica connessione Jellyfin", "editMediaBrowserTitle": "",
"editJellyfinIntro": "Aggiungi o rimuovi URL per ${serverName}. Plezy userà l'URL raggiungibile con la latenza più bassa." "editMediaBrowserIntro": ""
}, },
"discover": { "discover": {
"title": "Esplora", "title": "Esplora",
@@ -1873,12 +1873,12 @@
} }
}, },
"addServer": { "addServer": {
"addJellyfinTitle": "Aggiungi server Jellyfin", "addMediaBrowserTitle": "",
"serverUrls": "URL del server", "serverUrls": "URL del server",
"serverUrlsHelper": "Sono consentiti più URL, separati da virgole.", "serverUrlsHelper": "Sono consentiti più URL, separati da virgole.",
"findServer": "Trova il server", "findServer": "Trova il server",
"searchingLocalServers": "Ricerca dei server Jellyfin locali...", "searchingLocalMediaBrowserServers": "",
"localServers": "Server Jellyfin locali", "localMediaBrowserServers": "",
"username": "Nome utente", "username": "Nome utente",
"password": "Password", "password": "Password",
"signIn": "Accedi", "signIn": "Accedi",
@@ -1890,15 +1890,15 @@
"addPlexTitle": "Accedi con Plex", "addPlexTitle": "Accedi con Plex",
"pinExpired": "PIN scaduto prima dell'accesso. Riprova.", "pinExpired": "PIN scaduto prima dell'accesso. Riprova.",
"failedToRegisterAccount": "Registrazione account non riuscita: ${error}", "failedToRegisterAccount": "Registrazione account non riuscita: ${error}",
"enterJellyfinUrlError": "Inserisci l'URL del tuo server Jellyfin", "enterMediaBrowserUrlError": "",
"addConnectionTitle": "Aggiungi connessione", "addConnectionTitle": "Aggiungi connessione",
"addConnectionTitleScoped": "Aggiungi a ${name}", "addConnectionTitleScoped": "Aggiungi a ${name}",
"signInWithPlexCard": "Accedi con Plex", "signInWithPlexCard": "Accedi con Plex",
"signInWithPlexCardSubtitle": "Autorizza questo dispositivo. I server condivisi vengono aggiunti.", "signInWithPlexCardSubtitle": "Autorizza questo dispositivo. I server condivisi vengono aggiunti.",
"signInWithPlexCardSubtitleScoped": "Autorizza un account Plex. Gli utenti Home diventano profili.", "signInWithPlexCardSubtitleScoped": "Autorizza un account Plex. Gli utenti Home diventano profili.",
"connectToJellyfinCard": "Connettiti a Jellyfin", "connectToMediaBrowserCard": "",
"connectToJellyfinCardSubtitle": "Inserisci l'URL del server, il nome utente e la password.", "connectToMediaBrowserCardSubtitle": "",
"connectToJellyfinCardSubtitleScoped": "Accedi a un server Jellyfin. Verrà associato a ${name}.", "connectToMediaBrowserCardSubtitleScoped": "",
"borrowFromAnotherProfile": "Prendi in prestito da un altro profilo", "borrowFromAnotherProfile": "Prendi in prestito da un altro profilo",
"borrowFromAnotherProfileSubtitle": "Riutilizza la connessione di un altro profilo. I profili protetti da PIN richiedono un PIN." "borrowFromAnotherProfileSubtitle": "Riutilizza la connessione di un altro profilo. I profili protetti da PIN richiedono un PIN."
} }
+10 -10
View File
@@ -11,7 +11,7 @@
"waitingForAuth": "認証を待っています…\nブラウザでサインインしてください。", "waitingForAuth": "認証を待っています…\nブラウザでサインインしてください。",
"useBrowser": "ブラウザを使用", "useBrowser": "ブラウザを使用",
"or": "または", "or": "または",
"connectToJellyfin": "Jellyfinに接続", "connectToMediaBrowser": "",
"useQuickConnect": "Quick Connect を使う", "useQuickConnect": "Quick Connect を使う",
"quickConnectInstructions": "JellyfinでQuick Connectを開き、このコードを入力してください。", "quickConnectInstructions": "JellyfinでQuick Connectを開き、このコードを入力してください。",
"quickConnectWaiting": "承認を待っています…", "quickConnectWaiting": "承認を待っています…",
@@ -826,8 +826,8 @@
"sessionExpiredOne": "${name} のセッションの有効期限が切れました", "sessionExpiredOne": "${name} のセッションの有効期限が切れました",
"sessionExpiredMany": "${count} 台のサーバーのセッションの有効期限が切れました", "sessionExpiredMany": "${count} 台のサーバーのセッションの有効期限が切れました",
"signInAgain": "再度サインイン", "signInAgain": "再度サインイン",
"editJellyfinTitle": "Jellyfin接続を編集", "editMediaBrowserTitle": "",
"editJellyfinIntro": "${serverName}のURLを追加または削除します。Plezyは接続可能なURLのうち遅延が最も少ないものを使用します。" "editMediaBrowserIntro": ""
}, },
"discover": { "discover": {
"title": "探す", "title": "探す",
@@ -1867,12 +1867,12 @@
} }
}, },
"addServer": { "addServer": {
"addJellyfinTitle": "Jellyfinサーバーを追加", "addMediaBrowserTitle": "",
"serverUrls": "サーバーURL", "serverUrls": "サーバーURL",
"serverUrlsHelper": "複数のURLをカンマ区切りで入力できます。", "serverUrlsHelper": "複数のURLをカンマ区切りで入力できます。",
"findServer": "サーバーを検索", "findServer": "サーバーを検索",
"searchingLocalServers": "ローカルのJellyfinサーバーを検索中…", "searchingLocalMediaBrowserServers": "",
"localServers": "ローカルのJellyfinサーバー", "localMediaBrowserServers": "",
"username": "ユーザー名", "username": "ユーザー名",
"password": "パスワード", "password": "パスワード",
"signIn": "サインイン", "signIn": "サインイン",
@@ -1884,15 +1884,15 @@
"addPlexTitle": "Plexでサインイン", "addPlexTitle": "Plexでサインイン",
"pinExpired": "サインイン前にPINの有効期限が切れました。もう一度お試しください。", "pinExpired": "サインイン前にPINの有効期限が切れました。もう一度お試しください。",
"failedToRegisterAccount": "アカウントの登録に失敗しました: ${error}", "failedToRegisterAccount": "アカウントの登録に失敗しました: ${error}",
"enterJellyfinUrlError": "JellyfinサーバーのURLを入力してください", "enterMediaBrowserUrlError": "",
"addConnectionTitle": "接続を追加", "addConnectionTitle": "接続を追加",
"addConnectionTitleScoped": "${name}に追加", "addConnectionTitleScoped": "${name}に追加",
"signInWithPlexCard": "Plexでサインイン", "signInWithPlexCard": "Plexでサインイン",
"signInWithPlexCardSubtitle": "このデバイスを承認します。共有サーバーが追加されます。", "signInWithPlexCardSubtitle": "このデバイスを承認します。共有サーバーが追加されます。",
"signInWithPlexCardSubtitleScoped": "Plexアカウントを承認します。Homeユーザーはプロフィールになります。", "signInWithPlexCardSubtitleScoped": "Plexアカウントを承認します。Homeユーザーはプロフィールになります。",
"connectToJellyfinCard": "Jellyfinに接続", "connectToMediaBrowserCard": "",
"connectToJellyfinCardSubtitle": "サーバーURL、ユーザー名、パスワードを入力してください。", "connectToMediaBrowserCardSubtitle": "",
"connectToJellyfinCardSubtitleScoped": "Jellyfinサーバーにサインインします。${name}にひも付けられます。", "connectToMediaBrowserCardSubtitleScoped": "",
"borrowFromAnotherProfile": "別のプロフィールの接続を利用", "borrowFromAnotherProfile": "別のプロフィールの接続を利用",
"borrowFromAnotherProfileSubtitle": "別のプロフィールの接続を再利用します。PINで保護されたプロフィールにはPINが必要です。" "borrowFromAnotherProfileSubtitle": "別のプロフィールの接続を再利用します。PINで保護されたプロフィールにはPINが必要です。"
} }
+10 -10
View File
@@ -11,7 +11,7 @@
"waitingForAuth": "Растау күтілуде...\nБраузеріңізден кіріңіз.", "waitingForAuth": "Растау күтілуде...\nБраузеріңізден кіріңіз.",
"useBrowser": "Браузерді пайдалану", "useBrowser": "Браузерді пайдалану",
"or": "немесе", "or": "немесе",
"connectToJellyfin": "Jellyfin-ге қосылу", "connectToMediaBrowser": "",
"useQuickConnect": "Жылдам қосылуды пайдалану", "useQuickConnect": "Жылдам қосылуды пайдалану",
"quickConnectInstructions": "Jellyfin-де Жылдам қосылуды ашып, осы кодты енгізіңіз.", "quickConnectInstructions": "Jellyfin-де Жылдам қосылуды ашып, осы кодты енгізіңіз.",
"quickConnectWaiting": "Растау күтілуде…", "quickConnectWaiting": "Растау күтілуде…",
@@ -827,8 +827,8 @@
"sessionExpiredOne": "${name} үшін сеанс мерзімі өтті", "sessionExpiredOne": "${name} үшін сеанс мерзімі өтті",
"sessionExpiredMany": "${count} сервер үшін сеанс мерзімі өтті", "sessionExpiredMany": "${count} сервер үшін сеанс мерзімі өтті",
"signInAgain": "Қайтадан кіру", "signInAgain": "Қайтадан кіру",
"editJellyfinTitle": "Jellyfin қосылымын өңдеу", "editMediaBrowserTitle": "",
"editJellyfinIntro": "${serverName} үшін URL мекенжайын қосыңыз немесе өшіріңіз." "editMediaBrowserIntro": ""
}, },
"discover": { "discover": {
"title": "Шолу", "title": "Шолу",
@@ -1873,12 +1873,12 @@
} }
}, },
"addServer": { "addServer": {
"addJellyfinTitle": "Jellyfin серверін қосу", "addMediaBrowserTitle": "",
"serverUrls": "Сервер URL-дері", "serverUrls": "Сервер URL-дері",
"serverUrlsHelper": "Үтірмен бөлінген бірнеше URL мекенжайына рұқсат етіледі.", "serverUrlsHelper": "Үтірмен бөлінген бірнеше URL мекенжайына рұқсат етіледі.",
"findServer": "Серверді табу", "findServer": "Серверді табу",
"searchingLocalServers": "Жергілікті Jellyfin серверлері ізделуде...", "searchingLocalMediaBrowserServers": "",
"localServers": "Жергілікті Jellyfin серверлері", "localMediaBrowserServers": "",
"username": "Пайдаланушы аты", "username": "Пайдаланушы аты",
"password": "Құпия сөз", "password": "Құпия сөз",
"signIn": "Кіру", "signIn": "Кіру",
@@ -1890,15 +1890,15 @@
"addPlexTitle": "Plex арқылы кіру", "addPlexTitle": "Plex арқылы кіру",
"pinExpired": "PIN код мерзімі өтті.", "pinExpired": "PIN код мерзімі өтті.",
"failedToRegisterAccount": "Тіркелгіні тіркеу қатесі: ${error}", "failedToRegisterAccount": "Тіркелгіні тіркеу қатесі: ${error}",
"enterJellyfinUrlError": "Jellyfin сервер URL-ін енгізіңіз", "enterMediaBrowserUrlError": "",
"addConnectionTitle": "Қосылым қосу", "addConnectionTitle": "Қосылым қосу",
"addConnectionTitleScoped": "${name} профиліне қосу", "addConnectionTitleScoped": "${name} профиліне қосу",
"signInWithPlexCard": "Plex арқылы кіру", "signInWithPlexCard": "Plex арқылы кіру",
"signInWithPlexCardSubtitle": "Осы құрылғыны авторизациялау.", "signInWithPlexCardSubtitle": "Осы құрылғыны авторизациялау.",
"signInWithPlexCardSubtitleScoped": "Plex тіркелгісін авторизациялау.", "signInWithPlexCardSubtitleScoped": "Plex тіркелгісін авторизациялау.",
"connectToJellyfinCard": "Jellyfin-ге қосылу", "connectToMediaBrowserCard": "",
"connectToJellyfinCardSubtitle": "Сервер URL-ін, пайдаланушы атын енгізіңіз.", "connectToMediaBrowserCardSubtitle": "",
"connectToJellyfinCardSubtitleScoped": "Jellyfin серверіне кіру. ${name} профиліне жалғануда.", "connectToMediaBrowserCardSubtitleScoped": "",
"borrowFromAnotherProfile": "Басқа профильден алу", "borrowFromAnotherProfile": "Басқа профильден алу",
"borrowFromAnotherProfileSubtitle": "Басқа профильдің қосылымын қайта пайдалану." "borrowFromAnotherProfileSubtitle": "Басқа профильдің қосылымын қайта пайдалану."
} }
+10 -10
View File
@@ -11,7 +11,7 @@
"waitingForAuth": "인증 대기 중...\n브라우저에서 로그인하세요.", "waitingForAuth": "인증 대기 중...\n브라우저에서 로그인하세요.",
"useBrowser": "브라우저 사용", "useBrowser": "브라우저 사용",
"or": "또는", "or": "또는",
"connectToJellyfin": "Jellyfin에 연결", "connectToMediaBrowser": "",
"useQuickConnect": "Quick Connect 사용", "useQuickConnect": "Quick Connect 사용",
"quickConnectInstructions": "Jellyfin에서 Quick Connect를 열고 이 코드를 입력하세요.", "quickConnectInstructions": "Jellyfin에서 Quick Connect를 열고 이 코드를 입력하세요.",
"quickConnectWaiting": "승인 대기 중…", "quickConnectWaiting": "승인 대기 중…",
@@ -826,8 +826,8 @@
"sessionExpiredOne": "${name}의 세션이 만료되었습니다", "sessionExpiredOne": "${name}의 세션이 만료되었습니다",
"sessionExpiredMany": "${count}개 서버의 세션이 만료되었습니다", "sessionExpiredMany": "${count}개 서버의 세션이 만료되었습니다",
"signInAgain": "다시 로그인", "signInAgain": "다시 로그인",
"editJellyfinTitle": "Jellyfin 연결 편집", "editMediaBrowserTitle": "",
"editJellyfinIntro": "${serverName}의 URL을 추가하거나 제거하세요. Plezy는 연결 가능한 URL 중 지연 시간이 가장 낮은 URL을 사용합니다." "editMediaBrowserIntro": ""
}, },
"discover": { "discover": {
"title": "둘러보기", "title": "둘러보기",
@@ -1867,12 +1867,12 @@
} }
}, },
"addServer": { "addServer": {
"addJellyfinTitle": "Jellyfin 서버 추가", "addMediaBrowserTitle": "",
"serverUrls": "서버 URL", "serverUrls": "서버 URL",
"serverUrlsHelper": "쉼표로 구분하여 여러 URL을 입력할 수 있습니다.", "serverUrlsHelper": "쉼표로 구분하여 여러 URL을 입력할 수 있습니다.",
"findServer": "서버 찾기", "findServer": "서버 찾기",
"searchingLocalServers": "로컬 Jellyfin 서버 검색 중...", "searchingLocalMediaBrowserServers": "",
"localServers": "로컬 Jellyfin 서버", "localMediaBrowserServers": "",
"username": "사용자 이름", "username": "사용자 이름",
"password": "비밀번호", "password": "비밀번호",
"signIn": "로그인", "signIn": "로그인",
@@ -1884,15 +1884,15 @@
"addPlexTitle": "Plex로 로그인", "addPlexTitle": "Plex로 로그인",
"pinExpired": "로그인 전에 PIN이 만료되었습니다. 다시 시도하세요.", "pinExpired": "로그인 전에 PIN이 만료되었습니다. 다시 시도하세요.",
"failedToRegisterAccount": "계정 등록 실패: ${error}", "failedToRegisterAccount": "계정 등록 실패: ${error}",
"enterJellyfinUrlError": "Jellyfin 서버 URL을 입력하세요", "enterMediaBrowserUrlError": "",
"addConnectionTitle": "연결 추가", "addConnectionTitle": "연결 추가",
"addConnectionTitleScoped": "${name}에 추가", "addConnectionTitleScoped": "${name}에 추가",
"signInWithPlexCard": "Plex로 로그인", "signInWithPlexCard": "Plex로 로그인",
"signInWithPlexCardSubtitle": "이 기기를 승인합니다. 공유 서버가 추가됩니다.", "signInWithPlexCardSubtitle": "이 기기를 승인합니다. 공유 서버가 추가됩니다.",
"signInWithPlexCardSubtitleScoped": "Plex 계정을 승인합니다. Home 사용자는 프로필이 됩니다.", "signInWithPlexCardSubtitleScoped": "Plex 계정을 승인합니다. Home 사용자는 프로필이 됩니다.",
"connectToJellyfinCard": "Jellyfin에 연결", "connectToMediaBrowserCard": "",
"connectToJellyfinCardSubtitle": "서버 URL, 사용자 이름, 비밀번호를 입력하세요.", "connectToMediaBrowserCardSubtitle": "",
"connectToJellyfinCardSubtitleScoped": "Jellyfin 서버에 로그인합니다. ${name}에 연결됩니다.", "connectToMediaBrowserCardSubtitleScoped": "",
"borrowFromAnotherProfile": "다른 프로필에서 빌리기", "borrowFromAnotherProfile": "다른 프로필에서 빌리기",
"borrowFromAnotherProfileSubtitle": "다른 프로필의 연결을 재사용합니다. PIN으로 보호된 프로필에는 PIN이 필요합니다." "borrowFromAnotherProfileSubtitle": "다른 프로필의 연결을 재사용합니다. PIN으로 보호된 프로필에는 PIN이 필요합니다."
} }
+10 -10
View File
@@ -11,7 +11,7 @@
"waitingForAuth": "Venter på autentisering...\nLogg inn fra nettleseren.", "waitingForAuth": "Venter på autentisering...\nLogg inn fra nettleseren.",
"useBrowser": "Bruk nettleser", "useBrowser": "Bruk nettleser",
"or": "eller", "or": "eller",
"connectToJellyfin": "Koble til Jellyfin", "connectToMediaBrowser": "",
"useQuickConnect": "Bruk Quick Connect", "useQuickConnect": "Bruk Quick Connect",
"quickConnectInstructions": "Åpne Quick Connect i Jellyfin og skriv inn denne koden.", "quickConnectInstructions": "Åpne Quick Connect i Jellyfin og skriv inn denne koden.",
"quickConnectWaiting": "Venter på godkjenning…", "quickConnectWaiting": "Venter på godkjenning…",
@@ -827,8 +827,8 @@
"sessionExpiredOne": "Økten er utløpt for ${name}", "sessionExpiredOne": "Økten er utløpt for ${name}",
"sessionExpiredMany": "Økten er utløpt for ${count} servere", "sessionExpiredMany": "Økten er utløpt for ${count} servere",
"signInAgain": "Logg inn igjen", "signInAgain": "Logg inn igjen",
"editJellyfinTitle": "Rediger Jellyfin-tilkobling", "editMediaBrowserTitle": "",
"editJellyfinIntro": "Legg til eller fjern URL-er for ${serverName}. Plezy bruker den tilgjengelige URL-en med lavest forsinkelse." "editMediaBrowserIntro": ""
}, },
"discover": { "discover": {
"title": "Oppdag", "title": "Oppdag",
@@ -1873,12 +1873,12 @@
} }
}, },
"addServer": { "addServer": {
"addJellyfinTitle": "Legg til Jellyfin-server", "addMediaBrowserTitle": "",
"serverUrls": "Server-URL-er", "serverUrls": "Server-URL-er",
"serverUrlsHelper": "Flere URL-er er tillatt, atskilt med komma.", "serverUrlsHelper": "Flere URL-er er tillatt, atskilt med komma.",
"findServer": "Finn server", "findServer": "Finn server",
"searchingLocalServers": "Søker etter lokale Jellyfin-servere...", "searchingLocalMediaBrowserServers": "",
"localServers": "Lokale Jellyfin-servere", "localMediaBrowserServers": "",
"username": "Brukernavn", "username": "Brukernavn",
"password": "Passord", "password": "Passord",
"signIn": "Logg inn", "signIn": "Logg inn",
@@ -1890,15 +1890,15 @@
"addPlexTitle": "Logg inn med Plex", "addPlexTitle": "Logg inn med Plex",
"pinExpired": "PIN-koden utløp før innloggingen var fullført. Prøv igjen.", "pinExpired": "PIN-koden utløp før innloggingen var fullført. Prøv igjen.",
"failedToRegisterAccount": "Kunne ikke registrere kontoen: ${error}", "failedToRegisterAccount": "Kunne ikke registrere kontoen: ${error}",
"enterJellyfinUrlError": "Oppgi URL-en til Jellyfin-serveren din", "enterMediaBrowserUrlError": "",
"addConnectionTitle": "Legg til tilkobling", "addConnectionTitle": "Legg til tilkobling",
"addConnectionTitleScoped": "Legg til for ${name}", "addConnectionTitleScoped": "Legg til for ${name}",
"signInWithPlexCard": "Logg inn med Plex", "signInWithPlexCard": "Logg inn med Plex",
"signInWithPlexCardSubtitle": "Autoriser denne enheten. Delte servere legges til.", "signInWithPlexCardSubtitle": "Autoriser denne enheten. Delte servere legges til.",
"signInWithPlexCardSubtitleScoped": "Autoriser en Plex-konto. Home-brukere blir profiler.", "signInWithPlexCardSubtitleScoped": "Autoriser en Plex-konto. Home-brukere blir profiler.",
"connectToJellyfinCard": "Koble til Jellyfin", "connectToMediaBrowserCard": "",
"connectToJellyfinCardSubtitle": "Skriv inn server-URL, brukernavn og passord.", "connectToMediaBrowserCardSubtitle": "",
"connectToJellyfinCardSubtitleScoped": "Logg på en Jellyfin-server. Knyttes til ${name}.", "connectToMediaBrowserCardSubtitleScoped": "",
"borrowFromAnotherProfile": "Lån fra en annen profil", "borrowFromAnotherProfile": "Lån fra en annen profil",
"borrowFromAnotherProfileSubtitle": "Gjenbruk en annen profils tilkobling. PIN-beskyttede profiler krever PIN." "borrowFromAnotherProfileSubtitle": "Gjenbruk en annen profils tilkobling. PIN-beskyttede profiler krever PIN."
} }
+10 -10
View File
@@ -11,7 +11,7 @@
"waitingForAuth": "Wachten op authenticatie...\nMeld je aan via je browser.", "waitingForAuth": "Wachten op authenticatie...\nMeld je aan via je browser.",
"useBrowser": "Gebruik browser", "useBrowser": "Gebruik browser",
"or": "of", "or": "of",
"connectToJellyfin": "Verbinden met Jellyfin", "connectToMediaBrowser": "",
"useQuickConnect": "Quick Connect gebruiken", "useQuickConnect": "Quick Connect gebruiken",
"quickConnectInstructions": "Open Quick Connect in Jellyfin en voer deze code in.", "quickConnectInstructions": "Open Quick Connect in Jellyfin en voer deze code in.",
"quickConnectWaiting": "Wachten op goedkeuring…", "quickConnectWaiting": "Wachten op goedkeuring…",
@@ -827,8 +827,8 @@
"sessionExpiredOne": "Sessie verlopen voor ${name}", "sessionExpiredOne": "Sessie verlopen voor ${name}",
"sessionExpiredMany": "Sessie verlopen voor ${count} servers", "sessionExpiredMany": "Sessie verlopen voor ${count} servers",
"signInAgain": "Opnieuw aanmelden", "signInAgain": "Opnieuw aanmelden",
"editJellyfinTitle": "Jellyfin-verbinding bewerken", "editMediaBrowserTitle": "",
"editJellyfinIntro": "Voeg URL's voor ${serverName} toe of verwijder ze. Plezy gebruikt de bereikbare URL met de laagste latentie." "editMediaBrowserIntro": ""
}, },
"discover": { "discover": {
"title": "Ontdekken", "title": "Ontdekken",
@@ -1873,12 +1873,12 @@
} }
}, },
"addServer": { "addServer": {
"addJellyfinTitle": "Jellyfin-server toevoegen", "addMediaBrowserTitle": "",
"serverUrls": "Server-URL's", "serverUrls": "Server-URL's",
"serverUrlsHelper": "Meerdere URL's toegestaan, gescheiden door komma's.", "serverUrlsHelper": "Meerdere URL's toegestaan, gescheiden door komma's.",
"findServer": "Server zoeken", "findServer": "Server zoeken",
"searchingLocalServers": "Lokale Jellyfin-servers zoeken...", "searchingLocalMediaBrowserServers": "",
"localServers": "Lokale Jellyfin-servers", "localMediaBrowserServers": "",
"username": "Gebruikersnaam", "username": "Gebruikersnaam",
"password": "Wachtwoord", "password": "Wachtwoord",
"signIn": "Inloggen", "signIn": "Inloggen",
@@ -1890,15 +1890,15 @@
"addPlexTitle": "Inloggen met Plex", "addPlexTitle": "Inloggen met Plex",
"pinExpired": "De pincode verliep voordat je kon inloggen. Probeer het opnieuw.", "pinExpired": "De pincode verliep voordat je kon inloggen. Probeer het opnieuw.",
"failedToRegisterAccount": "Account registreren mislukt: ${error}", "failedToRegisterAccount": "Account registreren mislukt: ${error}",
"enterJellyfinUrlError": "Voer de URL van je Jellyfin-server in", "enterMediaBrowserUrlError": "",
"addConnectionTitle": "Verbinding toevoegen", "addConnectionTitle": "Verbinding toevoegen",
"addConnectionTitleScoped": "Toevoegen aan ${name}", "addConnectionTitleScoped": "Toevoegen aan ${name}",
"signInWithPlexCard": "Inloggen met Plex", "signInWithPlexCard": "Inloggen met Plex",
"signInWithPlexCardSubtitle": "Autoriseer dit apparaat. Gedeelde servers worden toegevoegd.", "signInWithPlexCardSubtitle": "Autoriseer dit apparaat. Gedeelde servers worden toegevoegd.",
"signInWithPlexCardSubtitleScoped": "Autoriseer een Plex-account. Home-gebruikers worden profielen.", "signInWithPlexCardSubtitleScoped": "Autoriseer een Plex-account. Home-gebruikers worden profielen.",
"connectToJellyfinCard": "Verbinden met Jellyfin", "connectToMediaBrowserCard": "",
"connectToJellyfinCardSubtitle": "Voer je server-URL, gebruikersnaam en wachtwoord in.", "connectToMediaBrowserCardSubtitle": "",
"connectToJellyfinCardSubtitleScoped": "Log in op een Jellyfin-server. Wordt gekoppeld aan ${name}.", "connectToMediaBrowserCardSubtitleScoped": "",
"borrowFromAnotherProfile": "Van een ander profiel lenen", "borrowFromAnotherProfile": "Van een ander profiel lenen",
"borrowFromAnotherProfileSubtitle": "Hergebruik de verbinding van een ander profiel. Voor profielen met pincodebeveiliging is een pincode vereist." "borrowFromAnotherProfileSubtitle": "Hergebruik de verbinding van een ander profiel. Voor profielen met pincodebeveiliging is een pincode vereist."
} }
+10 -10
View File
@@ -11,7 +11,7 @@
"waitingForAuth": "Oczekiwanie na uwierzytelnienie...\nZaloguj się w przeglądarce.", "waitingForAuth": "Oczekiwanie na uwierzytelnienie...\nZaloguj się w przeglądarce.",
"useBrowser": "Użyj przeglądarki", "useBrowser": "Użyj przeglądarki",
"or": "lub", "or": "lub",
"connectToJellyfin": "Połącz z Jellyfin", "connectToMediaBrowser": "",
"useQuickConnect": "Użyj Quick Connect", "useQuickConnect": "Użyj Quick Connect",
"quickConnectInstructions": "Otwórz Quick Connect w Jellyfin i wpisz ten kod.", "quickConnectInstructions": "Otwórz Quick Connect w Jellyfin i wpisz ten kod.",
"quickConnectWaiting": "Oczekiwanie na zatwierdzenie…", "quickConnectWaiting": "Oczekiwanie na zatwierdzenie…",
@@ -829,8 +829,8 @@
"sessionExpiredOne": "Sesja wygasła dla ${name}", "sessionExpiredOne": "Sesja wygasła dla ${name}",
"sessionExpiredMany": "Sesja wygasła dla ${count} serwerów", "sessionExpiredMany": "Sesja wygasła dla ${count} serwerów",
"signInAgain": "Zaloguj się ponownie", "signInAgain": "Zaloguj się ponownie",
"editJellyfinTitle": "Edytuj połączenie Jellyfin", "editMediaBrowserTitle": "",
"editJellyfinIntro": "Dodaj lub usuń adresy URL dla ${serverName}. Plezy użyje osiągalnego URL-a o najniższym opóźnieniu." "editMediaBrowserIntro": ""
}, },
"discover": { "discover": {
"title": "Odkryj", "title": "Odkryj",
@@ -1885,12 +1885,12 @@
} }
}, },
"addServer": { "addServer": {
"addJellyfinTitle": "Dodaj serwer Jellyfin", "addMediaBrowserTitle": "",
"serverUrls": "Adresy URL serwera", "serverUrls": "Adresy URL serwera",
"serverUrlsHelper": "Można podać wiele adresów URL rozdzielonych przecinkami.", "serverUrlsHelper": "Można podać wiele adresów URL rozdzielonych przecinkami.",
"findServer": "Znajdź serwer", "findServer": "Znajdź serwer",
"searchingLocalServers": "Szukanie lokalnych serwerów Jellyfin...", "searchingLocalMediaBrowserServers": "",
"localServers": "Lokalne serwery Jellyfin", "localMediaBrowserServers": "",
"username": "Nazwa użytkownika", "username": "Nazwa użytkownika",
"password": "Hasło", "password": "Hasło",
"signIn": "Zaloguj się", "signIn": "Zaloguj się",
@@ -1902,15 +1902,15 @@
"addPlexTitle": "Zaloguj się przez Plex", "addPlexTitle": "Zaloguj się przez Plex",
"pinExpired": "PIN wygasł przed zalogowaniem. Spróbuj ponownie.", "pinExpired": "PIN wygasł przed zalogowaniem. Spróbuj ponownie.",
"failedToRegisterAccount": "Nie udało się zarejestrować konta: ${error}", "failedToRegisterAccount": "Nie udało się zarejestrować konta: ${error}",
"enterJellyfinUrlError": "Podaj URL serwera Jellyfin", "enterMediaBrowserUrlError": "",
"addConnectionTitle": "Dodaj połączenie", "addConnectionTitle": "Dodaj połączenie",
"addConnectionTitleScoped": "Dodaj do ${name}", "addConnectionTitleScoped": "Dodaj do ${name}",
"signInWithPlexCard": "Zaloguj się przez Plex", "signInWithPlexCard": "Zaloguj się przez Plex",
"signInWithPlexCardSubtitle": "Autoryzuj to urządzenie. Serwery udostępnione zostaną dodane.", "signInWithPlexCardSubtitle": "Autoryzuj to urządzenie. Serwery udostępnione zostaną dodane.",
"signInWithPlexCardSubtitleScoped": "Autoryzuj konto Plex. Użytkownicy Home staną się profilami.", "signInWithPlexCardSubtitleScoped": "Autoryzuj konto Plex. Użytkownicy Home staną się profilami.",
"connectToJellyfinCard": "Połącz z Jellyfin", "connectToMediaBrowserCard": "",
"connectToJellyfinCardSubtitle": "Wpisz URL serwera, nazwę użytkownika i hasło.", "connectToMediaBrowserCardSubtitle": "",
"connectToJellyfinCardSubtitleScoped": "Zaloguj się do serwera Jellyfin. Powiązane z ${name}.", "connectToMediaBrowserCardSubtitleScoped": "",
"borrowFromAnotherProfile": "Pożycz z innego profilu", "borrowFromAnotherProfile": "Pożycz z innego profilu",
"borrowFromAnotherProfileSubtitle": "Użyj połączenia innego profilu. Profile chronione PIN-em wymagają podania PIN-u." "borrowFromAnotherProfileSubtitle": "Użyj połączenia innego profilu. Profile chronione PIN-em wymagają podania PIN-u."
} }
+10 -10
View File
@@ -11,7 +11,7 @@
"waitingForAuth": "Aguardando autenticação...\nEntre pelo navegador.", "waitingForAuth": "Aguardando autenticação...\nEntre pelo navegador.",
"useBrowser": "Usar navegador", "useBrowser": "Usar navegador",
"or": "ou", "or": "ou",
"connectToJellyfin": "Conectar ao Jellyfin", "connectToMediaBrowser": "",
"useQuickConnect": "Usar Quick Connect", "useQuickConnect": "Usar Quick Connect",
"quickConnectInstructions": "Abra o Quick Connect no Jellyfin e insira este código.", "quickConnectInstructions": "Abra o Quick Connect no Jellyfin e insira este código.",
"quickConnectWaiting": "Aguardando aprovação…", "quickConnectWaiting": "Aguardando aprovação…",
@@ -827,8 +827,8 @@
"sessionExpiredOne": "Sessão de ${name} expirada", "sessionExpiredOne": "Sessão de ${name} expirada",
"sessionExpiredMany": "Sessões expiradas em ${count} servidores", "sessionExpiredMany": "Sessões expiradas em ${count} servidores",
"signInAgain": "Entrar novamente", "signInAgain": "Entrar novamente",
"editJellyfinTitle": "Editar conexão Jellyfin", "editMediaBrowserTitle": "",
"editJellyfinIntro": "Adicione ou remova URLs de ${serverName}. O Plezy usará a URL acessível com a menor latência." "editMediaBrowserIntro": ""
}, },
"discover": { "discover": {
"title": "Descobrir", "title": "Descobrir",
@@ -1873,12 +1873,12 @@
} }
}, },
"addServer": { "addServer": {
"addJellyfinTitle": "Adicionar servidor Jellyfin", "addMediaBrowserTitle": "",
"serverUrls": "URLs do servidor", "serverUrls": "URLs do servidor",
"serverUrlsHelper": "Várias URLs são permitidas, separadas por vírgulas.", "serverUrlsHelper": "Várias URLs são permitidas, separadas por vírgulas.",
"findServer": "Encontrar servidor", "findServer": "Encontrar servidor",
"searchingLocalServers": "Procurando servidores Jellyfin locais...", "searchingLocalMediaBrowserServers": "",
"localServers": "Servidores Jellyfin locais", "localMediaBrowserServers": "",
"username": "Usuário", "username": "Usuário",
"password": "Senha", "password": "Senha",
"signIn": "Entrar", "signIn": "Entrar",
@@ -1890,15 +1890,15 @@
"addPlexTitle": "Entrar com Plex", "addPlexTitle": "Entrar com Plex",
"pinExpired": "O PIN expirou antes de entrar. Tente novamente.", "pinExpired": "O PIN expirou antes de entrar. Tente novamente.",
"failedToRegisterAccount": "Falha ao registrar a conta: ${error}", "failedToRegisterAccount": "Falha ao registrar a conta: ${error}",
"enterJellyfinUrlError": "Insira a URL do seu servidor Jellyfin", "enterMediaBrowserUrlError": "",
"addConnectionTitle": "Adicionar conexão", "addConnectionTitle": "Adicionar conexão",
"addConnectionTitleScoped": "Adicionar a ${name}", "addConnectionTitleScoped": "Adicionar a ${name}",
"signInWithPlexCard": "Entrar com Plex", "signInWithPlexCard": "Entrar com Plex",
"signInWithPlexCardSubtitle": "Autorize este dispositivo. Servidores compartilhados são adicionados.", "signInWithPlexCardSubtitle": "Autorize este dispositivo. Servidores compartilhados são adicionados.",
"signInWithPlexCardSubtitleScoped": "Autorize uma conta Plex. Os usuários do Plex Home se tornam perfis.", "signInWithPlexCardSubtitleScoped": "Autorize uma conta Plex. Os usuários do Plex Home se tornam perfis.",
"connectToJellyfinCard": "Conectar ao Jellyfin", "connectToMediaBrowserCard": "",
"connectToJellyfinCardSubtitle": "Insira URL do servidor, usuário e senha.", "connectToMediaBrowserCardSubtitle": "",
"connectToJellyfinCardSubtitleScoped": "Entre em um servidor Jellyfin. A conexão será vinculada a ${name}.", "connectToMediaBrowserCardSubtitleScoped": "",
"borrowFromAnotherProfile": "Pegar emprestado de outro perfil", "borrowFromAnotherProfile": "Pegar emprestado de outro perfil",
"borrowFromAnotherProfileSubtitle": "Reutilize a conexão de outro perfil. Perfis protegidos por PIN exigem PIN." "borrowFromAnotherProfileSubtitle": "Reutilize a conexão de outro perfil. Perfis protegidos por PIN exigem PIN."
} }
+10 -10
View File
@@ -11,7 +11,7 @@
"waitingForAuth": "Ожидание аутентификации...\nВыполните вход в браузере.", "waitingForAuth": "Ожидание аутентификации...\nВыполните вход в браузере.",
"useBrowser": "Использовать браузер", "useBrowser": "Использовать браузер",
"or": "или", "or": "или",
"connectToJellyfin": "Подключиться к Jellyfin", "connectToMediaBrowser": "",
"useQuickConnect": "Использовать Quick Connect", "useQuickConnect": "Использовать Quick Connect",
"quickConnectInstructions": "Откройте Quick Connect в Jellyfin и введите этот код.", "quickConnectInstructions": "Откройте Quick Connect в Jellyfin и введите этот код.",
"quickConnectWaiting": "Ожидание подтверждения…", "quickConnectWaiting": "Ожидание подтверждения…",
@@ -829,8 +829,8 @@
"sessionExpiredOne": "Сессия истекла для ${name}", "sessionExpiredOne": "Сессия истекла для ${name}",
"sessionExpiredMany": "Сессия истекла для ${count} серверов", "sessionExpiredMany": "Сессия истекла для ${count} серверов",
"signInAgain": "Войти снова", "signInAgain": "Войти снова",
"editJellyfinTitle": "Изменить подключение Jellyfin", "editMediaBrowserTitle": "",
"editJellyfinIntro": "Добавьте или удалите URL для ${serverName}. Plezy будет использовать доступный URL с минимальной задержкой." "editMediaBrowserIntro": ""
}, },
"discover": { "discover": {
"title": "Обзор", "title": "Обзор",
@@ -1885,12 +1885,12 @@
} }
}, },
"addServer": { "addServer": {
"addJellyfinTitle": "Добавить сервер Jellyfin", "addMediaBrowserTitle": "",
"serverUrls": "URL-адреса сервера", "serverUrls": "URL-адреса сервера",
"serverUrlsHelper": "Можно указать несколько URL через запятую.", "serverUrlsHelper": "Можно указать несколько URL через запятую.",
"findServer": "Найти сервер", "findServer": "Найти сервер",
"searchingLocalServers": "Поиск локальных серверов Jellyfin...", "searchingLocalMediaBrowserServers": "",
"localServers": "Локальные серверы Jellyfin", "localMediaBrowserServers": "",
"username": "Имя пользователя", "username": "Имя пользователя",
"password": "Пароль", "password": "Пароль",
"signIn": "Войти", "signIn": "Войти",
@@ -1902,15 +1902,15 @@
"addPlexTitle": "Войти через Plex", "addPlexTitle": "Войти через Plex",
"pinExpired": "Срок действия PIN истёк до входа. Попробуйте снова.", "pinExpired": "Срок действия PIN истёк до входа. Попробуйте снова.",
"failedToRegisterAccount": "Не удалось зарегистрировать учётную запись: ${error}", "failedToRegisterAccount": "Не удалось зарегистрировать учётную запись: ${error}",
"enterJellyfinUrlError": "Введите URL вашего сервера Jellyfin", "enterMediaBrowserUrlError": "",
"addConnectionTitle": "Добавить подключение", "addConnectionTitle": "Добавить подключение",
"addConnectionTitleScoped": "Добавить в ${name}", "addConnectionTitleScoped": "Добавить в ${name}",
"signInWithPlexCard": "Войти через Plex", "signInWithPlexCard": "Войти через Plex",
"signInWithPlexCardSubtitle": "Авторизуйте это устройство. Общие серверы будут добавлены.", "signInWithPlexCardSubtitle": "Авторизуйте это устройство. Общие серверы будут добавлены.",
"signInWithPlexCardSubtitleScoped": "Авторизуйте аккаунт Plex. Пользователи Home станут профилями.", "signInWithPlexCardSubtitleScoped": "Авторизуйте аккаунт Plex. Пользователи Home станут профилями.",
"connectToJellyfinCard": "Подключиться к Jellyfin", "connectToMediaBrowserCard": "",
"connectToJellyfinCardSubtitle": "Введите URL сервера, имя пользователя и пароль.", "connectToMediaBrowserCardSubtitle": "",
"connectToJellyfinCardSubtitleScoped": "Войдите на сервер Jellyfin. Привязывается к ${name}.", "connectToMediaBrowserCardSubtitleScoped": "",
"borrowFromAnotherProfile": "Использовать подключение другого профиля", "borrowFromAnotherProfile": "Использовать подключение другого профиля",
"borrowFromAnotherProfileSubtitle": "Повторно используйте подключение другого профиля. Для защищённых профилей потребуется PIN." "borrowFromAnotherProfileSubtitle": "Повторно используйте подключение другого профиля. Для защищённых профилей потребуется PIN."
} }
+1 -1
View File
@@ -4,7 +4,7 @@
/// To regenerate, run: `dart run slang` /// To regenerate, run: `dart run slang`
/// ///
/// Locales: 22 /// Locales: 22
/// Strings: 32946 (1497 per locale) /// Strings: 32736 (1488 per locale)
// coverage:ignore-file // coverage:ignore-file
// ignore_for_file: type=lint, unused_import // ignore_for_file: type=lint, unused_import
+3 -23
View File
@@ -115,7 +115,6 @@ class _Translations$auth$az extends Translations$auth$en {
@override String get waitingForAuth => 'Təsdiqləmə gözlənilir...\nSəyahətçinizdən (brauzer) daxil olun.'; @override String get waitingForAuth => 'Təsdiqləmə gözlənilir...\nSəyahətçinizdən (brauzer) daxil olun.';
@override String get useBrowser => 'Səyahətçini istifadə et'; @override String get useBrowser => 'Səyahətçini istifadə et';
@override String get or => 'və ya'; @override String get or => 'və ya';
@override String get connectToJellyfin => 'Jellyfin-ə qoşul';
@override String get useQuickConnect => 'Sürətli Qoşulmanı istifadə et'; @override String get useQuickConnect => 'Sürətli Qoşulmanı istifadə et';
@override String get quickConnectInstructions => 'Jellyfin-də Sürətli Qoşulmanı açın və bu kodu daxil edin.'; @override String get quickConnectInstructions => 'Jellyfin-də Sürətli Qoşulmanı açın və bu kodu daxil edin.';
@override String get quickConnectWaiting => 'Təsdiq gözlənilir…'; @override String get quickConnectWaiting => 'Təsdiq gözlənilir…';
@@ -922,8 +921,6 @@ class _Translations$connections$az extends Translations$connections$en {
@override String sessionExpiredOne({required Object name}) => '${name} üçün seansın vaxtı bitdi'; @override String sessionExpiredOne({required Object name}) => '${name} üçün seansın vaxtı bitdi';
@override String sessionExpiredMany({required Object count}) => '${count} server üçün seansın vaxtı bitdi'; @override String sessionExpiredMany({required Object count}) => '${count} server üçün seansın vaxtı bitdi';
@override String get signInAgain => 'Yenidən daxil ol'; @override String get signInAgain => 'Yenidən daxil ol';
@override String get editJellyfinTitle => 'Jellyfin qoşulmasını dəyişdir';
@override String editJellyfinIntro({required Object serverName}) => '${serverName} üçün URL əlavə edin və ya silin.';
} }
// Path: discover // Path: discover
@@ -1814,12 +1811,9 @@ class _Translations$addServer$az extends Translations$addServer$en {
final TranslationsAz _root; // ignore: unused_field final TranslationsAz _root; // ignore: unused_field
// Translations // Translations
@override String get addJellyfinTitle => 'Jellyfin serveri əlavə et';
@override String get serverUrls => 'Server URL-ləri'; @override String get serverUrls => 'Server URL-ləri';
@override String get serverUrlsHelper => 'Vergüllə ayrılmış bir neçə URL-ə icazə verilir.'; @override String get serverUrlsHelper => 'Vergüllə ayrılmış bir neçə URL-ə icazə verilir.';
@override String get findServer => 'Server tap'; @override String get findServer => 'Server tap';
@override String get searchingLocalServers => 'Yerli Jellyfin serverləri axtarılır...';
@override String get localServers => 'Yerli Jellyfin serverləri';
@override String get username => 'İstifadəçi adı'; @override String get username => 'İstifadəçi adı';
@override String get password => 'Şifrə'; @override String get password => 'Şifrə';
@override String get signIn => 'Daxil ol'; @override String get signIn => 'Daxil ol';
@@ -1831,15 +1825,11 @@ class _Translations$addServer$az extends Translations$addServer$en {
@override String get addPlexTitle => 'Plex ilə daxil ol'; @override String get addPlexTitle => 'Plex ilə daxil ol';
@override String get pinExpired => 'PIN-in vaxtı bitdi. Lütfən təzədən cəhd edin.'; @override String get pinExpired => 'PIN-in vaxtı bitdi. Lütfən təzədən cəhd edin.';
@override String failedToRegisterAccount({required Object error}) => 'Hesab qeydiyyatı uğursuz oldu: ${error}'; @override String failedToRegisterAccount({required Object error}) => 'Hesab qeydiyyatı uğursuz oldu: ${error}';
@override String get enterJellyfinUrlError => 'Jellyfin server URL-inizi daxil edin';
@override String get addConnectionTitle => 'Qoşulma əlavə et'; @override String get addConnectionTitle => 'Qoşulma əlavə et';
@override String addConnectionTitleScoped({required Object name}) => '${name} profilinə əlavə et'; @override String addConnectionTitleScoped({required Object name}) => '${name} profilinə əlavə et';
@override String get signInWithPlexCard => 'Plex ilə daxil ol'; @override String get signInWithPlexCard => 'Plex ilə daxil ol';
@override String get signInWithPlexCardSubtitle => 'Bu cihazı səlahiyyətləndirin.'; @override String get signInWithPlexCardSubtitle => 'Bu cihazı səlahiyyətləndirin.';
@override String get signInWithPlexCardSubtitleScoped => 'Plex hesabını səlahiyyətləndirin.'; @override String get signInWithPlexCardSubtitleScoped => 'Plex hesabını səlahiyyətləndirin.';
@override String get connectToJellyfinCard => 'Jellyfin-ə qoşul';
@override String get connectToJellyfinCardSubtitle => 'Server URL, istifadəçi adı və şifrənizi daxil edin.';
@override String connectToJellyfinCardSubtitleScoped({required Object name}) => 'Jellyfin serverinə daxil olun. ${name} profilinə bağlanır.';
@override String get borrowFromAnotherProfile => 'Başqa profildən götür'; @override String get borrowFromAnotherProfile => 'Başqa profildən götür';
@override String get borrowFromAnotherProfileSubtitle => 'Başqa profilin qoşulmasını yenidən istifadə edin.'; @override String get borrowFromAnotherProfileSubtitle => 'Başqa profilin qoşulmasını yenidən istifadə edin.';
} }
@@ -2219,7 +2209,6 @@ extension on TranslationsAz {
'auth.waitingForAuth' => 'Təsdiqləmə gözlənilir...\nSəyahətçinizdən (brauzer) daxil olun.', 'auth.waitingForAuth' => 'Təsdiqləmə gözlənilir...\nSəyahətçinizdən (brauzer) daxil olun.',
'auth.useBrowser' => 'Səyahətçini istifadə et', 'auth.useBrowser' => 'Səyahətçini istifadə et',
'auth.or' => 'və ya', 'auth.or' => 'və ya',
'auth.connectToJellyfin' => 'Jellyfin-ə qoşul',
'auth.useQuickConnect' => 'Sürətli Qoşulmanı istifadə et', 'auth.useQuickConnect' => 'Sürətli Qoşulmanı istifadə et',
'auth.quickConnectInstructions' => 'Jellyfin-də Sürətli Qoşulmanı açın və bu kodu daxil edin.', 'auth.quickConnectInstructions' => 'Jellyfin-də Sürətli Qoşulmanı açın və bu kodu daxil edin.',
'auth.quickConnectWaiting' => 'Təsdiq gözlənilir…', 'auth.quickConnectWaiting' => 'Təsdiq gözlənilir…',
@@ -2722,9 +2711,9 @@ extension on TranslationsAz {
'videoControls.noChaptersAvailable' => 'Hissələr əlçatan deyil', 'videoControls.noChaptersAvailable' => 'Hissələr əlçatan deyil',
'videoControls.queue' => 'Növbə', 'videoControls.queue' => 'Növbə',
'videoControls.noQueueItems' => 'Növbədə element yoxdur', 'videoControls.noQueueItems' => 'Növbədə element yoxdur',
'videoControls.searchSubtitles' => 'Altyazı axtar',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'videoControls.searchSubtitles' => 'Altyazı axtar',
'videoControls.language' => 'Dil', 'videoControls.language' => 'Dil',
'videoControls.noSubtitlesFound' => 'Altyazı tapılmadı', 'videoControls.noSubtitlesFound' => 'Altyazı tapılmadı',
'videoControls.subtitleDownloaded' => 'Altyazı yükləndi', 'videoControls.subtitleDownloaded' => 'Altyazı yükləndi',
@@ -2884,8 +2873,6 @@ extension on TranslationsAz {
'connections.sessionExpiredOne' => ({required Object name}) => '${name} üçün seansın vaxtı bitdi', 'connections.sessionExpiredOne' => ({required Object name}) => '${name} üçün seansın vaxtı bitdi',
'connections.sessionExpiredMany' => ({required Object count}) => '${count} server üçün seansın vaxtı bitdi', 'connections.sessionExpiredMany' => ({required Object count}) => '${count} server üçün seansın vaxtı bitdi',
'connections.signInAgain' => 'Yenidən daxil ol', 'connections.signInAgain' => 'Yenidən daxil ol',
'connections.editJellyfinTitle' => 'Jellyfin qoşulmasını dəyişdir',
'connections.editJellyfinIntro' => ({required Object serverName}) => '${serverName} üçün URL əlavə edin və ya silin.',
'discover.title' => 'Kəşf et', 'discover.title' => 'Kəşf et',
'discover.noContentAvailable' => 'Məzmun əlçatan deyil', 'discover.noContentAvailable' => 'Məzmun əlçatan deyil',
'discover.addMediaToLibraries' => 'Kitabxanalarınıza bir az media əlavə edin', 'discover.addMediaToLibraries' => 'Kitabxanalarınıza bir az media əlavə edin',
@@ -3236,11 +3223,11 @@ extension on TranslationsAz {
'watchTogether.enterCodeHint' => '5 rəqəmli/hərfli kodu daxil edin', 'watchTogether.enterCodeHint' => '5 rəqəmli/hərfli kodu daxil edin',
'watchTogether.pasteFromClipboard' => 'Buferdən yapışdır', 'watchTogether.pasteFromClipboard' => 'Buferdən yapışdır',
'watchTogether.pleaseEnterCode' => 'Lütfən seans kodunu daxil edin', 'watchTogether.pleaseEnterCode' => 'Lütfən seans kodunu daxil edin',
_ => null,
} ?? switch (path) {
'watchTogether.codeMustBe5Chars' => 'Seans kodu 5 simvol olmalıdır', 'watchTogether.codeMustBe5Chars' => 'Seans kodu 5 simvol olmalıdır',
'watchTogether.joinInstructions' => 'Qoşulmaq üçün təşkilatçının seans kodunu daxil edin.', 'watchTogether.joinInstructions' => 'Qoşulmaq üçün təşkilatçının seans kodunu daxil edin.',
'watchTogether.failedToCreate' => 'Seans yaradıla bilmədi', 'watchTogether.failedToCreate' => 'Seans yaradıla bilmədi',
_ => null,
} ?? switch (path) {
'watchTogether.failedToJoin' => 'Seansa qoşuluna bilmədi', 'watchTogether.failedToJoin' => 'Seansa qoşuluna bilmədi',
'watchTogether.sessionCodeCopied' => 'Seans kodu buferə kopyalandı', 'watchTogether.sessionCodeCopied' => 'Seans kodu buferə kopyalandı',
'watchTogether.relayUnreachable' => 'Rele serverinə çatmaq olmur. İnternet provayderinin bloklaması Birlikdə İzləməyə mane ola bilər.', 'watchTogether.relayUnreachable' => 'Rele serverinə çatmaq olmur. İnternet provayderinin bloklaması Birlikdə İzləməyə mane ola bilər.',
@@ -3678,12 +3665,9 @@ extension on TranslationsAz {
'services.libraryFilter.modeHintWhitelist' => 'Yalnız aşağıda seçilən kitabxanaları eyniləşdir.', 'services.libraryFilter.modeHintWhitelist' => 'Yalnız aşağıda seçilən kitabxanaları eyniləşdir.',
'services.libraryFilter.libraries' => 'Kitabxanalar', 'services.libraryFilter.libraries' => 'Kitabxanalar',
'services.libraryFilter.noLibraries' => 'Kitabxana yoxdur', 'services.libraryFilter.noLibraries' => 'Kitabxana yoxdur',
'addServer.addJellyfinTitle' => 'Jellyfin serveri əlavə et',
'addServer.serverUrls' => 'Server URL-ləri', 'addServer.serverUrls' => 'Server URL-ləri',
'addServer.serverUrlsHelper' => 'Vergüllə ayrılmış bir neçə URL-ə icazə verilir.', 'addServer.serverUrlsHelper' => 'Vergüllə ayrılmış bir neçə URL-ə icazə verilir.',
'addServer.findServer' => 'Server tap', 'addServer.findServer' => 'Server tap',
'addServer.searchingLocalServers' => 'Yerli Jellyfin serverləri axtarılır...',
'addServer.localServers' => 'Yerli Jellyfin serverləri',
'addServer.username' => 'İstifadəçi adı', 'addServer.username' => 'İstifadəçi adı',
'addServer.password' => 'Şifrə', 'addServer.password' => 'Şifrə',
'addServer.signIn' => 'Daxil ol', 'addServer.signIn' => 'Daxil ol',
@@ -3695,15 +3679,11 @@ extension on TranslationsAz {
'addServer.addPlexTitle' => 'Plex ilə daxil ol', 'addServer.addPlexTitle' => 'Plex ilə daxil ol',
'addServer.pinExpired' => 'PIN-in vaxtı bitdi. Lütfən təzədən cəhd edin.', 'addServer.pinExpired' => 'PIN-in vaxtı bitdi. Lütfən təzədən cəhd edin.',
'addServer.failedToRegisterAccount' => ({required Object error}) => 'Hesab qeydiyyatı uğursuz oldu: ${error}', 'addServer.failedToRegisterAccount' => ({required Object error}) => 'Hesab qeydiyyatı uğursuz oldu: ${error}',
'addServer.enterJellyfinUrlError' => 'Jellyfin server URL-inizi daxil edin',
'addServer.addConnectionTitle' => 'Qoşulma əlavə et', 'addServer.addConnectionTitle' => 'Qoşulma əlavə et',
'addServer.addConnectionTitleScoped' => ({required Object name}) => '${name} profilinə əlavə et', 'addServer.addConnectionTitleScoped' => ({required Object name}) => '${name} profilinə əlavə et',
'addServer.signInWithPlexCard' => 'Plex ilə daxil ol', 'addServer.signInWithPlexCard' => 'Plex ilə daxil ol',
'addServer.signInWithPlexCardSubtitle' => 'Bu cihazı səlahiyyətləndirin.', 'addServer.signInWithPlexCardSubtitle' => 'Bu cihazı səlahiyyətləndirin.',
'addServer.signInWithPlexCardSubtitleScoped' => 'Plex hesabını səlahiyyətləndirin.', 'addServer.signInWithPlexCardSubtitleScoped' => 'Plex hesabını səlahiyyətləndirin.',
'addServer.connectToJellyfinCard' => 'Jellyfin-ə qoşul',
'addServer.connectToJellyfinCardSubtitle' => 'Server URL, istifadəçi adı və şifrənizi daxil edin.',
'addServer.connectToJellyfinCardSubtitleScoped' => ({required Object name}) => 'Jellyfin serverinə daxil olun. ${name} profilinə bağlanır.',
'addServer.borrowFromAnotherProfile' => 'Başqa profildən götür', 'addServer.borrowFromAnotherProfile' => 'Başqa profildən götür',
'addServer.borrowFromAnotherProfileSubtitle' => 'Başqa profilin qoşulmasını yenidən istifadə edin.', 'addServer.borrowFromAnotherProfileSubtitle' => 'Başqa profilin qoşulmasını yenidən istifadə edin.',
_ => null, _ => null,
+3 -23
View File
@@ -115,7 +115,6 @@ class _Translations$auth$bg extends Translations$auth$en {
@override String get waitingForAuth => 'Изчакване на удостоверяване...\nВлезте от браузъра си.'; @override String get waitingForAuth => 'Изчакване на удостоверяване...\nВлезте от браузъра си.';
@override String get useBrowser => 'Използвай браузър'; @override String get useBrowser => 'Използвай браузър';
@override String get or => 'или'; @override String get or => 'или';
@override String get connectToJellyfin => 'Свържи се с Jellyfin';
@override String get useQuickConnect => 'Използвай Quick Connect'; @override String get useQuickConnect => 'Използвай Quick Connect';
@override String get quickConnectInstructions => 'Отворете Quick Connect в Jellyfin и въведете този код.'; @override String get quickConnectInstructions => 'Отворете Quick Connect в Jellyfin и въведете този код.';
@override String get quickConnectWaiting => 'Изчакване на одобрение…'; @override String get quickConnectWaiting => 'Изчакване на одобрение…';
@@ -918,8 +917,6 @@ class _Translations$connections$bg extends Translations$connections$en {
@override String sessionExpiredOne({required Object name}) => 'Сесията за ${name} е изтекла'; @override String sessionExpiredOne({required Object name}) => 'Сесията за ${name} е изтекла';
@override String sessionExpiredMany({required Object count}) => 'Сесиите за ${count} сървъра са изтекли'; @override String sessionExpiredMany({required Object count}) => 'Сесиите за ${count} сървъра са изтекли';
@override String get signInAgain => 'Влез отново'; @override String get signInAgain => 'Влез отново';
@override String get editJellyfinTitle => 'Редактиране на Jellyfin връзка';
@override String editJellyfinIntro({required Object serverName}) => 'Добавете или премахнете URL адреси за ${serverName}. Plezy ще използва достъпния URL адрес с най-ниска латентност.';
} }
// Path: discover // Path: discover
@@ -1803,12 +1800,9 @@ class _Translations$addServer$bg extends Translations$addServer$en {
final TranslationsBg _root; // ignore: unused_field final TranslationsBg _root; // ignore: unused_field
// Translations // Translations
@override String get addJellyfinTitle => 'Добави Jellyfin сървър';
@override String get serverUrls => 'URL адреси на сървъра'; @override String get serverUrls => 'URL адреси на сървъра';
@override String get serverUrlsHelper => 'Позволени са няколко URL адреса, разделени със запетаи.'; @override String get serverUrlsHelper => 'Позволени са няколко URL адреса, разделени със запетаи.';
@override String get findServer => 'Намери сървър'; @override String get findServer => 'Намери сървър';
@override String get searchingLocalServers => 'Търсене на локални Jellyfin сървъри...';
@override String get localServers => 'Локални Jellyfin сървъри';
@override String get username => 'Потребителско име'; @override String get username => 'Потребителско име';
@override String get password => 'Парола'; @override String get password => 'Парола';
@override String get signIn => 'Вход'; @override String get signIn => 'Вход';
@@ -1820,15 +1814,11 @@ class _Translations$addServer$bg extends Translations$addServer$en {
@override String get addPlexTitle => 'Вход с Plex'; @override String get addPlexTitle => 'Вход с Plex';
@override String get pinExpired => 'PIN-ът изтече преди вход. Моля, опитайте отново.'; @override String get pinExpired => 'PIN-ът изтече преди вход. Моля, опитайте отново.';
@override String failedToRegisterAccount({required Object error}) => 'Неуспешна регистрация на акаунт: ${error}'; @override String failedToRegisterAccount({required Object error}) => 'Неуспешна регистрация на акаунт: ${error}';
@override String get enterJellyfinUrlError => 'Въведете URL адреса на вашия Jellyfin сървър';
@override String get addConnectionTitle => 'Добави връзка'; @override String get addConnectionTitle => 'Добави връзка';
@override String addConnectionTitleScoped({required Object name}) => 'Добави към ${name}'; @override String addConnectionTitleScoped({required Object name}) => 'Добави към ${name}';
@override String get signInWithPlexCard => 'Вход с Plex'; @override String get signInWithPlexCard => 'Вход с Plex';
@override String get signInWithPlexCardSubtitle => 'Удостоверете това устройство. Споделените сървъри се добавят.'; @override String get signInWithPlexCardSubtitle => 'Удостоверете това устройство. Споделените сървъри се добавят.';
@override String get signInWithPlexCardSubtitleScoped => 'Удостоверете Plex акаунт. Домашните потребители стават профили.'; @override String get signInWithPlexCardSubtitleScoped => 'Удостоверете Plex акаунт. Домашните потребители стават профили.';
@override String get connectToJellyfinCard => 'Свързване с Jellyfin';
@override String get connectToJellyfinCardSubtitle => 'Въведете URL адрес на сървъра, потребителско име и парола.';
@override String connectToJellyfinCardSubtitleScoped({required Object name}) => 'Вход в Jellyfin сървър. Свързва се с ${name}.';
@override String get borrowFromAnotherProfile => 'Използвай от друг профил'; @override String get borrowFromAnotherProfile => 'Използвай от друг профил';
@override String get borrowFromAnotherProfileSubtitle => 'Използвай връзка от друг профил. PIN-защитените профили изискват PIN.'; @override String get borrowFromAnotherProfileSubtitle => 'Използвай връзка от друг профил. PIN-защитените профили изискват PIN.';
} }
@@ -2208,7 +2198,6 @@ extension on TranslationsBg {
'auth.waitingForAuth' => 'Изчакване на удостоверяване...\nВлезте от браузъра си.', 'auth.waitingForAuth' => 'Изчакване на удостоверяване...\nВлезте от браузъра си.',
'auth.useBrowser' => 'Използвай браузър', 'auth.useBrowser' => 'Използвай браузър',
'auth.or' => 'или', 'auth.or' => 'или',
'auth.connectToJellyfin' => 'Свържи се с Jellyfin',
'auth.useQuickConnect' => 'Използвай Quick Connect', 'auth.useQuickConnect' => 'Използвай Quick Connect',
'auth.quickConnectInstructions' => 'Отворете Quick Connect в Jellyfin и въведете този код.', 'auth.quickConnectInstructions' => 'Отворете Quick Connect в Jellyfin и въведете този код.',
'auth.quickConnectWaiting' => 'Изчакване на одобрение…', 'auth.quickConnectWaiting' => 'Изчакване на одобрение…',
@@ -2711,9 +2700,9 @@ extension on TranslationsBg {
'videoControls.searchSubtitles' => 'Търсене на субтитри', 'videoControls.searchSubtitles' => 'Търсене на субтитри',
'videoControls.language' => 'Език', 'videoControls.language' => 'Език',
'videoControls.noSubtitlesFound' => 'Не са намерени субтитри', 'videoControls.noSubtitlesFound' => 'Не са намерени субтитри',
'videoControls.subtitleDownloaded' => 'Субтитърът е изтеглен',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'videoControls.subtitleDownloaded' => 'Субтитърът е изтеглен',
'videoControls.subtitleDownloadedNotApplied' => 'Субтитрите са изтеглени, но не можаха да бъдат избрани', 'videoControls.subtitleDownloadedNotApplied' => 'Субтитрите са изтеглени, но не можаха да бъдат избрани',
'videoControls.subtitleDownloadFailed' => 'Неуспешно изтегляне на субтитър', 'videoControls.subtitleDownloadFailed' => 'Неуспешно изтегляне на субтитър',
'videoControls.searchLanguages' => 'Търсене на езици...', 'videoControls.searchLanguages' => 'Търсене на езици...',
@@ -2869,8 +2858,6 @@ extension on TranslationsBg {
'connections.sessionExpiredOne' => ({required Object name}) => 'Сесията за ${name} е изтекла', 'connections.sessionExpiredOne' => ({required Object name}) => 'Сесията за ${name} е изтекла',
'connections.sessionExpiredMany' => ({required Object count}) => 'Сесиите за ${count} сървъра са изтекли', 'connections.sessionExpiredMany' => ({required Object count}) => 'Сесиите за ${count} сървъра са изтекли',
'connections.signInAgain' => 'Влез отново', 'connections.signInAgain' => 'Влез отново',
'connections.editJellyfinTitle' => 'Редактиране на Jellyfin връзка',
'connections.editJellyfinIntro' => ({required Object serverName}) => 'Добавете или премахнете URL адреси за ${serverName}. Plezy ще използва достъпния URL адрес с най-ниска латентност.',
'discover.title' => 'Открий', 'discover.title' => 'Открий',
'discover.noContentAvailable' => 'Няма налично съдържание', 'discover.noContentAvailable' => 'Няма налично съдържание',
'discover.addMediaToLibraries' => 'Добавете медия към библиотеките си', 'discover.addMediaToLibraries' => 'Добавете медия към библиотеките си',
@@ -3225,11 +3212,11 @@ extension on TranslationsBg {
'watchTogether.failedToCreate' => 'Неуспешно създаване на сесия', 'watchTogether.failedToCreate' => 'Неуспешно създаване на сесия',
'watchTogether.failedToJoin' => 'Неуспешно присъединяване към сесия', 'watchTogether.failedToJoin' => 'Неуспешно присъединяване към сесия',
'watchTogether.sessionCodeCopied' => 'Кодът на сесията е копиран в клипборда', 'watchTogether.sessionCodeCopied' => 'Кодът на сесията е копиран в клипборда',
_ => null,
} ?? switch (path) {
'watchTogether.relayUnreachable' => 'Релейният сървър е недостъпен. Възможно е интернет доставчикът да блокира гледането заедно.', 'watchTogether.relayUnreachable' => 'Релейният сървър е недостъпен. Възможно е интернет доставчикът да блокира гледането заедно.',
'watchTogether.reconnectingToHost' => 'Повторно свързване с организатора...', 'watchTogether.reconnectingToHost' => 'Повторно свързване с организатора...',
'watchTogether.currentPlayback' => 'Текущо възпроизвеждане', 'watchTogether.currentPlayback' => 'Текущо възпроизвеждане',
_ => null,
} ?? switch (path) {
'watchTogether.joinCurrentPlayback' => 'Присъедини се към текущото възпроизвеждане', 'watchTogether.joinCurrentPlayback' => 'Присъедини се към текущото възпроизвеждане',
'watchTogether.joinCurrentPlaybackDescription' => 'Върнете се към това, което организаторът гледа в момента', 'watchTogether.joinCurrentPlaybackDescription' => 'Върнете се към това, което организаторът гледа в момента',
'watchTogether.failedToOpenCurrentPlayback' => 'Неуспешно отваряне на текущото възпроизвеждане', 'watchTogether.failedToOpenCurrentPlayback' => 'Неуспешно отваряне на текущото възпроизвеждане',
@@ -3656,12 +3643,9 @@ extension on TranslationsBg {
'services.libraryFilter.modeHintWhitelist' => 'Синхронизирай само отметнатите по-долу библиотеки.', 'services.libraryFilter.modeHintWhitelist' => 'Синхронизирай само отметнатите по-долу библиотеки.',
'services.libraryFilter.libraries' => 'Библиотеки', 'services.libraryFilter.libraries' => 'Библиотеки',
'services.libraryFilter.noLibraries' => 'Няма налични библиотеки', 'services.libraryFilter.noLibraries' => 'Няма налични библиотеки',
'addServer.addJellyfinTitle' => 'Добави Jellyfin сървър',
'addServer.serverUrls' => 'URL адреси на сървъра', 'addServer.serverUrls' => 'URL адреси на сървъра',
'addServer.serverUrlsHelper' => 'Позволени са няколко URL адреса, разделени със запетаи.', 'addServer.serverUrlsHelper' => 'Позволени са няколко URL адреса, разделени със запетаи.',
'addServer.findServer' => 'Намери сървър', 'addServer.findServer' => 'Намери сървър',
'addServer.searchingLocalServers' => 'Търсене на локални Jellyfin сървъри...',
'addServer.localServers' => 'Локални Jellyfin сървъри',
'addServer.username' => 'Потребителско име', 'addServer.username' => 'Потребителско име',
'addServer.password' => 'Парола', 'addServer.password' => 'Парола',
'addServer.signIn' => 'Вход', 'addServer.signIn' => 'Вход',
@@ -3673,15 +3657,11 @@ extension on TranslationsBg {
'addServer.addPlexTitle' => 'Вход с Plex', 'addServer.addPlexTitle' => 'Вход с Plex',
'addServer.pinExpired' => 'PIN-ът изтече преди вход. Моля, опитайте отново.', 'addServer.pinExpired' => 'PIN-ът изтече преди вход. Моля, опитайте отново.',
'addServer.failedToRegisterAccount' => ({required Object error}) => 'Неуспешна регистрация на акаунт: ${error}', 'addServer.failedToRegisterAccount' => ({required Object error}) => 'Неуспешна регистрация на акаунт: ${error}',
'addServer.enterJellyfinUrlError' => 'Въведете URL адреса на вашия Jellyfin сървър',
'addServer.addConnectionTitle' => 'Добави връзка', 'addServer.addConnectionTitle' => 'Добави връзка',
'addServer.addConnectionTitleScoped' => ({required Object name}) => 'Добави към ${name}', 'addServer.addConnectionTitleScoped' => ({required Object name}) => 'Добави към ${name}',
'addServer.signInWithPlexCard' => 'Вход с Plex', 'addServer.signInWithPlexCard' => 'Вход с Plex',
'addServer.signInWithPlexCardSubtitle' => 'Удостоверете това устройство. Споделените сървъри се добавят.', 'addServer.signInWithPlexCardSubtitle' => 'Удостоверете това устройство. Споделените сървъри се добавят.',
'addServer.signInWithPlexCardSubtitleScoped' => 'Удостоверете Plex акаунт. Домашните потребители стават профили.', 'addServer.signInWithPlexCardSubtitleScoped' => 'Удостоверете Plex акаунт. Домашните потребители стават профили.',
'addServer.connectToJellyfinCard' => 'Свързване с Jellyfin',
'addServer.connectToJellyfinCardSubtitle' => 'Въведете URL адрес на сървъра, потребителско име и парола.',
'addServer.connectToJellyfinCardSubtitleScoped' => ({required Object name}) => 'Вход в Jellyfin сървър. Свързва се с ${name}.',
'addServer.borrowFromAnotherProfile' => 'Използвай от друг профил', 'addServer.borrowFromAnotherProfile' => 'Използвай от друг профил',
'addServer.borrowFromAnotherProfileSubtitle' => 'Използвай връзка от друг профил. PIN-защитените профили изискват PIN.', 'addServer.borrowFromAnotherProfileSubtitle' => 'Използвай връзка от друг профил. PIN-защитените профили изискват PIN.',
_ => null, _ => null,
+3 -23
View File
@@ -115,7 +115,6 @@ class _Translations$auth$da extends Translations$auth$en {
@override String get waitingForAuth => 'Venter på godkendelse...\nLog ind fra din browser.'; @override String get waitingForAuth => 'Venter på godkendelse...\nLog ind fra din browser.';
@override String get useBrowser => 'Brug browseren'; @override String get useBrowser => 'Brug browseren';
@override String get or => 'eller'; @override String get or => 'eller';
@override String get connectToJellyfin => 'Forbind til Jellyfin';
@override String get useQuickConnect => 'Brug Quick Connect'; @override String get useQuickConnect => 'Brug Quick Connect';
@override String get quickConnectInstructions => 'Åbn Quick Connect i Jellyfin, og indtast denne kode.'; @override String get quickConnectInstructions => 'Åbn Quick Connect i Jellyfin, og indtast denne kode.';
@override String get quickConnectWaiting => 'Venter på godkendelse…'; @override String get quickConnectWaiting => 'Venter på godkendelse…';
@@ -918,8 +917,6 @@ class _Translations$connections$da extends Translations$connections$en {
@override String sessionExpiredOne({required Object name}) => 'Sessionen er udløbet for ${name}'; @override String sessionExpiredOne({required Object name}) => 'Sessionen er udløbet for ${name}';
@override String sessionExpiredMany({required Object count}) => 'Sessionerne er udløbet for ${count} servere'; @override String sessionExpiredMany({required Object count}) => 'Sessionerne er udløbet for ${count} servere';
@override String get signInAgain => 'Log ind igen'; @override String get signInAgain => 'Log ind igen';
@override String get editJellyfinTitle => 'Rediger Jellyfin-forbindelse';
@override String editJellyfinIntro({required Object serverName}) => 'Tilføj eller fjern URL\'er for ${serverName}. Plezy bruger den tilgængelige URL med lavest latenstid.';
} }
// Path: discover // Path: discover
@@ -1803,12 +1800,9 @@ class _Translations$addServer$da extends Translations$addServer$en {
final TranslationsDa _root; // ignore: unused_field final TranslationsDa _root; // ignore: unused_field
// Translations // Translations
@override String get addJellyfinTitle => 'Tilføj Jellyfin-server';
@override String get serverUrls => 'Server-URL\'er'; @override String get serverUrls => 'Server-URL\'er';
@override String get serverUrlsHelper => 'Du kan angive flere URL\'er adskilt med komma.'; @override String get serverUrlsHelper => 'Du kan angive flere URL\'er adskilt med komma.';
@override String get findServer => 'Find server'; @override String get findServer => 'Find server';
@override String get searchingLocalServers => 'Søger efter lokale Jellyfin-servere...';
@override String get localServers => 'Lokale Jellyfin-servere';
@override String get username => 'Brugernavn'; @override String get username => 'Brugernavn';
@override String get password => 'Adgangskode'; @override String get password => 'Adgangskode';
@override String get signIn => 'Log ind'; @override String get signIn => 'Log ind';
@@ -1820,15 +1814,11 @@ class _Translations$addServer$da extends Translations$addServer$en {
@override String get addPlexTitle => 'Log ind med Plex'; @override String get addPlexTitle => 'Log ind med Plex';
@override String get pinExpired => 'PIN-koden udløb før login. Prøv igen.'; @override String get pinExpired => 'PIN-koden udløb før login. Prøv igen.';
@override String failedToRegisterAccount({required Object error}) => 'Kunne ikke registrere kontoen: ${error}'; @override String failedToRegisterAccount({required Object error}) => 'Kunne ikke registrere kontoen: ${error}';
@override String get enterJellyfinUrlError => 'Angiv URL\'en til din Jellyfin-server';
@override String get addConnectionTitle => 'Tilføj forbindelse'; @override String get addConnectionTitle => 'Tilføj forbindelse';
@override String addConnectionTitleScoped({required Object name}) => 'Tilføj til ${name}'; @override String addConnectionTitleScoped({required Object name}) => 'Tilføj til ${name}';
@override String get signInWithPlexCard => 'Log ind med Plex'; @override String get signInWithPlexCard => 'Log ind med Plex';
@override String get signInWithPlexCardSubtitle => 'Godkend denne enhed. Delte servere tilføjes.'; @override String get signInWithPlexCardSubtitle => 'Godkend denne enhed. Delte servere tilføjes.';
@override String get signInWithPlexCardSubtitleScoped => 'Godkend en Plex-konto. Plex Home-brugere bliver til profiler.'; @override String get signInWithPlexCardSubtitleScoped => 'Godkend en Plex-konto. Plex Home-brugere bliver til profiler.';
@override String get connectToJellyfinCard => 'Forbind til Jellyfin';
@override String get connectToJellyfinCardSubtitle => 'Indtast din server-URL, dit brugernavn og din adgangskode.';
@override String connectToJellyfinCardSubtitleScoped({required Object name}) => 'Log ind på en Jellyfin-server. Serveren knyttes til ${name}.';
@override String get borrowFromAnotherProfile => 'Lån fra en anden profil'; @override String get borrowFromAnotherProfile => 'Lån fra en anden profil';
@override String get borrowFromAnotherProfileSubtitle => 'Genbrug en anden profils forbindelse. PIN-beskyttede profiler kræver en PIN.'; @override String get borrowFromAnotherProfileSubtitle => 'Genbrug en anden profils forbindelse. PIN-beskyttede profiler kræver en PIN.';
} }
@@ -2208,7 +2198,6 @@ extension on TranslationsDa {
'auth.waitingForAuth' => 'Venter på godkendelse...\nLog ind fra din browser.', 'auth.waitingForAuth' => 'Venter på godkendelse...\nLog ind fra din browser.',
'auth.useBrowser' => 'Brug browseren', 'auth.useBrowser' => 'Brug browseren',
'auth.or' => 'eller', 'auth.or' => 'eller',
'auth.connectToJellyfin' => 'Forbind til Jellyfin',
'auth.useQuickConnect' => 'Brug Quick Connect', 'auth.useQuickConnect' => 'Brug Quick Connect',
'auth.quickConnectInstructions' => 'Åbn Quick Connect i Jellyfin, og indtast denne kode.', 'auth.quickConnectInstructions' => 'Åbn Quick Connect i Jellyfin, og indtast denne kode.',
'auth.quickConnectWaiting' => 'Venter på godkendelse…', 'auth.quickConnectWaiting' => 'Venter på godkendelse…',
@@ -2711,9 +2700,9 @@ extension on TranslationsDa {
'videoControls.searchSubtitles' => 'Søg undertekster', 'videoControls.searchSubtitles' => 'Søg undertekster',
'videoControls.language' => 'Sprog', 'videoControls.language' => 'Sprog',
'videoControls.noSubtitlesFound' => 'Ingen undertekster fundet', 'videoControls.noSubtitlesFound' => 'Ingen undertekster fundet',
'videoControls.subtitleDownloaded' => 'Undertekst downloadet',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'videoControls.subtitleDownloaded' => 'Undertekst downloadet',
'videoControls.subtitleDownloadedNotApplied' => 'Underteksten blev downloadet, men kunne ikke vælges', 'videoControls.subtitleDownloadedNotApplied' => 'Underteksten blev downloadet, men kunne ikke vælges',
'videoControls.subtitleDownloadFailed' => 'Kunne ikke downloade undertekst', 'videoControls.subtitleDownloadFailed' => 'Kunne ikke downloade undertekst',
'videoControls.searchLanguages' => 'Søg sprog...', 'videoControls.searchLanguages' => 'Søg sprog...',
@@ -2869,8 +2858,6 @@ extension on TranslationsDa {
'connections.sessionExpiredOne' => ({required Object name}) => 'Sessionen er udløbet for ${name}', 'connections.sessionExpiredOne' => ({required Object name}) => 'Sessionen er udløbet for ${name}',
'connections.sessionExpiredMany' => ({required Object count}) => 'Sessionerne er udløbet for ${count} servere', 'connections.sessionExpiredMany' => ({required Object count}) => 'Sessionerne er udløbet for ${count} servere',
'connections.signInAgain' => 'Log ind igen', 'connections.signInAgain' => 'Log ind igen',
'connections.editJellyfinTitle' => 'Rediger Jellyfin-forbindelse',
'connections.editJellyfinIntro' => ({required Object serverName}) => 'Tilføj eller fjern URL\'er for ${serverName}. Plezy bruger den tilgængelige URL med lavest latenstid.',
'discover.title' => 'Opdag', 'discover.title' => 'Opdag',
'discover.noContentAvailable' => 'Intet indhold tilgængeligt', 'discover.noContentAvailable' => 'Intet indhold tilgængeligt',
'discover.addMediaToLibraries' => 'Tilføj medier til dine biblioteker', 'discover.addMediaToLibraries' => 'Tilføj medier til dine biblioteker',
@@ -3225,11 +3212,11 @@ extension on TranslationsDa {
'watchTogether.failedToCreate' => 'Kunne ikke oprette session', 'watchTogether.failedToCreate' => 'Kunne ikke oprette session',
'watchTogether.failedToJoin' => 'Kunne ikke deltage i session', 'watchTogether.failedToJoin' => 'Kunne ikke deltage i session',
'watchTogether.sessionCodeCopied' => 'Sessionskode kopieret til udklipsholder', 'watchTogether.sessionCodeCopied' => 'Sessionskode kopieret til udklipsholder',
_ => null,
} ?? switch (path) {
'watchTogether.relayUnreachable' => 'Relayserveren kan ikke nås. Blokering hos internetudbyderen kan forhindre Se sammen.', 'watchTogether.relayUnreachable' => 'Relayserveren kan ikke nås. Blokering hos internetudbyderen kan forhindre Se sammen.',
'watchTogether.reconnectingToHost' => 'Genopretter forbindelse til vært...', 'watchTogether.reconnectingToHost' => 'Genopretter forbindelse til vært...',
'watchTogether.currentPlayback' => 'Nuværende afspilning', 'watchTogether.currentPlayback' => 'Nuværende afspilning',
_ => null,
} ?? switch (path) {
'watchTogether.joinCurrentPlayback' => 'Deltag i nuværende afspilning', 'watchTogether.joinCurrentPlayback' => 'Deltag i nuværende afspilning',
'watchTogether.joinCurrentPlaybackDescription' => 'Hop tilbage til det værten ser nu', 'watchTogether.joinCurrentPlaybackDescription' => 'Hop tilbage til det værten ser nu',
'watchTogether.failedToOpenCurrentPlayback' => 'Kunne ikke åbne nuværende afspilning', 'watchTogether.failedToOpenCurrentPlayback' => 'Kunne ikke åbne nuværende afspilning',
@@ -3656,12 +3643,9 @@ extension on TranslationsDa {
'services.libraryFilter.modeHintWhitelist' => 'Synkroniser kun de biblioteker, du markerer nedenfor.', 'services.libraryFilter.modeHintWhitelist' => 'Synkroniser kun de biblioteker, du markerer nedenfor.',
'services.libraryFilter.libraries' => 'Biblioteker', 'services.libraryFilter.libraries' => 'Biblioteker',
'services.libraryFilter.noLibraries' => 'Ingen biblioteker tilgængelige', 'services.libraryFilter.noLibraries' => 'Ingen biblioteker tilgængelige',
'addServer.addJellyfinTitle' => 'Tilføj Jellyfin-server',
'addServer.serverUrls' => 'Server-URL\'er', 'addServer.serverUrls' => 'Server-URL\'er',
'addServer.serverUrlsHelper' => 'Du kan angive flere URL\'er adskilt med komma.', 'addServer.serverUrlsHelper' => 'Du kan angive flere URL\'er adskilt med komma.',
'addServer.findServer' => 'Find server', 'addServer.findServer' => 'Find server',
'addServer.searchingLocalServers' => 'Søger efter lokale Jellyfin-servere...',
'addServer.localServers' => 'Lokale Jellyfin-servere',
'addServer.username' => 'Brugernavn', 'addServer.username' => 'Brugernavn',
'addServer.password' => 'Adgangskode', 'addServer.password' => 'Adgangskode',
'addServer.signIn' => 'Log ind', 'addServer.signIn' => 'Log ind',
@@ -3673,15 +3657,11 @@ extension on TranslationsDa {
'addServer.addPlexTitle' => 'Log ind med Plex', 'addServer.addPlexTitle' => 'Log ind med Plex',
'addServer.pinExpired' => 'PIN-koden udløb før login. Prøv igen.', 'addServer.pinExpired' => 'PIN-koden udløb før login. Prøv igen.',
'addServer.failedToRegisterAccount' => ({required Object error}) => 'Kunne ikke registrere kontoen: ${error}', 'addServer.failedToRegisterAccount' => ({required Object error}) => 'Kunne ikke registrere kontoen: ${error}',
'addServer.enterJellyfinUrlError' => 'Angiv URL\'en til din Jellyfin-server',
'addServer.addConnectionTitle' => 'Tilføj forbindelse', 'addServer.addConnectionTitle' => 'Tilføj forbindelse',
'addServer.addConnectionTitleScoped' => ({required Object name}) => 'Tilføj til ${name}', 'addServer.addConnectionTitleScoped' => ({required Object name}) => 'Tilføj til ${name}',
'addServer.signInWithPlexCard' => 'Log ind med Plex', 'addServer.signInWithPlexCard' => 'Log ind med Plex',
'addServer.signInWithPlexCardSubtitle' => 'Godkend denne enhed. Delte servere tilføjes.', 'addServer.signInWithPlexCardSubtitle' => 'Godkend denne enhed. Delte servere tilføjes.',
'addServer.signInWithPlexCardSubtitleScoped' => 'Godkend en Plex-konto. Plex Home-brugere bliver til profiler.', 'addServer.signInWithPlexCardSubtitleScoped' => 'Godkend en Plex-konto. Plex Home-brugere bliver til profiler.',
'addServer.connectToJellyfinCard' => 'Forbind til Jellyfin',
'addServer.connectToJellyfinCardSubtitle' => 'Indtast din server-URL, dit brugernavn og din adgangskode.',
'addServer.connectToJellyfinCardSubtitleScoped' => ({required Object name}) => 'Log ind på en Jellyfin-server. Serveren knyttes til ${name}.',
'addServer.borrowFromAnotherProfile' => 'Lån fra en anden profil', 'addServer.borrowFromAnotherProfile' => 'Lån fra en anden profil',
'addServer.borrowFromAnotherProfileSubtitle' => 'Genbrug en anden profils forbindelse. PIN-beskyttede profiler kræver en PIN.', 'addServer.borrowFromAnotherProfileSubtitle' => 'Genbrug en anden profils forbindelse. PIN-beskyttede profiler kræver en PIN.',
_ => null, _ => null,
+3 -23
View File
@@ -115,7 +115,6 @@ class _Translations$auth$de extends Translations$auth$en {
@override String get waitingForAuth => 'Warte auf die Authentifizierung …\nMelde dich über deinen Browser an.'; @override String get waitingForAuth => 'Warte auf die Authentifizierung …\nMelde dich über deinen Browser an.';
@override String get useBrowser => 'Browser verwenden'; @override String get useBrowser => 'Browser verwenden';
@override String get or => 'oder'; @override String get or => 'oder';
@override String get connectToJellyfin => 'Mit Jellyfin verbinden';
@override String get useQuickConnect => 'Quick Connect verwenden'; @override String get useQuickConnect => 'Quick Connect verwenden';
@override String get quickConnectInstructions => 'Öffne Quick Connect in Jellyfin und gib diesen Code ein.'; @override String get quickConnectInstructions => 'Öffne Quick Connect in Jellyfin und gib diesen Code ein.';
@override String get quickConnectWaiting => 'Warte auf Bestätigung…'; @override String get quickConnectWaiting => 'Warte auf Bestätigung…';
@@ -918,8 +917,6 @@ class _Translations$connections$de extends Translations$connections$en {
@override String sessionExpiredOne({required Object name}) => 'Sitzung für ${name} abgelaufen'; @override String sessionExpiredOne({required Object name}) => 'Sitzung für ${name} abgelaufen';
@override String sessionExpiredMany({required Object count}) => 'Sitzungen für ${count} Server abgelaufen'; @override String sessionExpiredMany({required Object count}) => 'Sitzungen für ${count} Server abgelaufen';
@override String get signInAgain => 'Erneut anmelden'; @override String get signInAgain => 'Erneut anmelden';
@override String get editJellyfinTitle => 'Jellyfin-Verbindung bearbeiten';
@override String editJellyfinIntro({required Object serverName}) => 'Füge URLs für ${serverName} hinzu oder entferne sie. Plezy verwendet die erreichbare URL mit der geringsten Latenz.';
} }
// Path: discover // Path: discover
@@ -1803,12 +1800,9 @@ class _Translations$addServer$de extends Translations$addServer$en {
final TranslationsDe _root; // ignore: unused_field final TranslationsDe _root; // ignore: unused_field
// Translations // Translations
@override String get addJellyfinTitle => 'Jellyfin-Server hinzufügen';
@override String get serverUrls => 'Server-URLs'; @override String get serverUrls => 'Server-URLs';
@override String get serverUrlsHelper => 'Mehrere URLs möglich, durch Kommas getrennt.'; @override String get serverUrlsHelper => 'Mehrere URLs möglich, durch Kommas getrennt.';
@override String get findServer => 'Server finden'; @override String get findServer => 'Server finden';
@override String get searchingLocalServers => 'Suche nach lokalen Jellyfin-Servern …';
@override String get localServers => 'Lokale Jellyfin-Server';
@override String get username => 'Benutzername'; @override String get username => 'Benutzername';
@override String get password => 'Passwort'; @override String get password => 'Passwort';
@override String get signIn => 'Anmelden'; @override String get signIn => 'Anmelden';
@@ -1820,15 +1814,11 @@ class _Translations$addServer$de extends Translations$addServer$en {
@override String get addPlexTitle => 'Mit Plex anmelden'; @override String get addPlexTitle => 'Mit Plex anmelden';
@override String get pinExpired => 'PIN ist vor der Anmeldung abgelaufen. Bitte erneut versuchen.'; @override String get pinExpired => 'PIN ist vor der Anmeldung abgelaufen. Bitte erneut versuchen.';
@override String failedToRegisterAccount({required Object error}) => 'Konto konnte nicht registriert werden: ${error}'; @override String failedToRegisterAccount({required Object error}) => 'Konto konnte nicht registriert werden: ${error}';
@override String get enterJellyfinUrlError => 'Gib die URL deines Jellyfin-Servers ein';
@override String get addConnectionTitle => 'Verbindung hinzufügen'; @override String get addConnectionTitle => 'Verbindung hinzufügen';
@override String addConnectionTitleScoped({required Object name}) => 'Zu ${name} hinzufügen'; @override String addConnectionTitleScoped({required Object name}) => 'Zu ${name} hinzufügen';
@override String get signInWithPlexCard => 'Mit Plex anmelden'; @override String get signInWithPlexCard => 'Mit Plex anmelden';
@override String get signInWithPlexCardSubtitle => 'Dieses Gerät autorisieren. Geteilte Server werden hinzugefügt.'; @override String get signInWithPlexCardSubtitle => 'Dieses Gerät autorisieren. Geteilte Server werden hinzugefügt.';
@override String get signInWithPlexCardSubtitleScoped => 'Ein Plex-Konto autorisieren. Home-Benutzer werden zu Profilen.'; @override String get signInWithPlexCardSubtitleScoped => 'Ein Plex-Konto autorisieren. Home-Benutzer werden zu Profilen.';
@override String get connectToJellyfinCard => 'Mit Jellyfin verbinden';
@override String get connectToJellyfinCardSubtitle => 'Gib Server-URL, Benutzername und Passwort ein.';
@override String connectToJellyfinCardSubtitleScoped({required Object name}) => 'Bei einem Jellyfin-Server anmelden. Wird mit ${name} verknüpft.';
@override String get borrowFromAnotherProfile => 'Von einem anderen Profil ausleihen'; @override String get borrowFromAnotherProfile => 'Von einem anderen Profil ausleihen';
@override String get borrowFromAnotherProfileSubtitle => 'Verbindung eines anderen Profils wiederverwenden. PIN-geschützte Profile erfordern eine PIN.'; @override String get borrowFromAnotherProfileSubtitle => 'Verbindung eines anderen Profils wiederverwenden. PIN-geschützte Profile erfordern eine PIN.';
} }
@@ -2208,7 +2198,6 @@ extension on TranslationsDe {
'auth.waitingForAuth' => 'Warte auf die Authentifizierung …\nMelde dich über deinen Browser an.', 'auth.waitingForAuth' => 'Warte auf die Authentifizierung …\nMelde dich über deinen Browser an.',
'auth.useBrowser' => 'Browser verwenden', 'auth.useBrowser' => 'Browser verwenden',
'auth.or' => 'oder', 'auth.or' => 'oder',
'auth.connectToJellyfin' => 'Mit Jellyfin verbinden',
'auth.useQuickConnect' => 'Quick Connect verwenden', 'auth.useQuickConnect' => 'Quick Connect verwenden',
'auth.quickConnectInstructions' => 'Öffne Quick Connect in Jellyfin und gib diesen Code ein.', 'auth.quickConnectInstructions' => 'Öffne Quick Connect in Jellyfin und gib diesen Code ein.',
'auth.quickConnectWaiting' => 'Warte auf Bestätigung…', 'auth.quickConnectWaiting' => 'Warte auf Bestätigung…',
@@ -2711,9 +2700,9 @@ extension on TranslationsDe {
'videoControls.searchSubtitles' => 'Untertitel suchen', 'videoControls.searchSubtitles' => 'Untertitel suchen',
'videoControls.language' => 'Sprache', 'videoControls.language' => 'Sprache',
'videoControls.noSubtitlesFound' => 'Keine Untertitel gefunden', 'videoControls.noSubtitlesFound' => 'Keine Untertitel gefunden',
'videoControls.subtitleDownloaded' => 'Untertitel heruntergeladen',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'videoControls.subtitleDownloaded' => 'Untertitel heruntergeladen',
'videoControls.subtitleDownloadedNotApplied' => 'Der Untertitel wurde heruntergeladen, konnte aber nicht ausgewählt werden', 'videoControls.subtitleDownloadedNotApplied' => 'Der Untertitel wurde heruntergeladen, konnte aber nicht ausgewählt werden',
'videoControls.subtitleDownloadFailed' => 'Untertitel konnte nicht heruntergeladen werden', 'videoControls.subtitleDownloadFailed' => 'Untertitel konnte nicht heruntergeladen werden',
'videoControls.searchLanguages' => 'Sprachen suchen...', 'videoControls.searchLanguages' => 'Sprachen suchen...',
@@ -2869,8 +2858,6 @@ extension on TranslationsDe {
'connections.sessionExpiredOne' => ({required Object name}) => 'Sitzung für ${name} abgelaufen', 'connections.sessionExpiredOne' => ({required Object name}) => 'Sitzung für ${name} abgelaufen',
'connections.sessionExpiredMany' => ({required Object count}) => 'Sitzungen für ${count} Server abgelaufen', 'connections.sessionExpiredMany' => ({required Object count}) => 'Sitzungen für ${count} Server abgelaufen',
'connections.signInAgain' => 'Erneut anmelden', 'connections.signInAgain' => 'Erneut anmelden',
'connections.editJellyfinTitle' => 'Jellyfin-Verbindung bearbeiten',
'connections.editJellyfinIntro' => ({required Object serverName}) => 'Füge URLs für ${serverName} hinzu oder entferne sie. Plezy verwendet die erreichbare URL mit der geringsten Latenz.',
'discover.title' => 'Entdecken', 'discover.title' => 'Entdecken',
'discover.noContentAvailable' => 'Kein Inhalt verfügbar', 'discover.noContentAvailable' => 'Kein Inhalt verfügbar',
'discover.addMediaToLibraries' => 'Medien zur Mediathek hinzufügen', 'discover.addMediaToLibraries' => 'Medien zur Mediathek hinzufügen',
@@ -3225,11 +3212,11 @@ extension on TranslationsDe {
'watchTogether.failedToCreate' => 'Sitzung konnte nicht erstellt werden', 'watchTogether.failedToCreate' => 'Sitzung konnte nicht erstellt werden',
'watchTogether.failedToJoin' => 'Beitritt zur Sitzung fehlgeschlagen', 'watchTogether.failedToJoin' => 'Beitritt zur Sitzung fehlgeschlagen',
'watchTogether.sessionCodeCopied' => 'Sitzungscode in Zwischenablage kopiert', 'watchTogether.sessionCodeCopied' => 'Sitzungscode in Zwischenablage kopiert',
_ => null,
} ?? switch (path) {
'watchTogether.relayUnreachable' => 'Relay-Server nicht erreichbar. Eine Sperre durch den Internetanbieter kann gemeinsames Schauen verhindern.', 'watchTogether.relayUnreachable' => 'Relay-Server nicht erreichbar. Eine Sperre durch den Internetanbieter kann gemeinsames Schauen verhindern.',
'watchTogether.reconnectingToHost' => 'Verbindung zum Host wird wiederhergestellt …', 'watchTogether.reconnectingToHost' => 'Verbindung zum Host wird wiederhergestellt …',
'watchTogether.currentPlayback' => 'Aktuelle Wiedergabe', 'watchTogether.currentPlayback' => 'Aktuelle Wiedergabe',
_ => null,
} ?? switch (path) {
'watchTogether.joinCurrentPlayback' => 'Aktueller Wiedergabe beitreten', 'watchTogether.joinCurrentPlayback' => 'Aktueller Wiedergabe beitreten',
'watchTogether.joinCurrentPlaybackDescription' => 'Zu dem Inhalt wechseln, den der Host gerade ansieht', 'watchTogether.joinCurrentPlaybackDescription' => 'Zu dem Inhalt wechseln, den der Host gerade ansieht',
'watchTogether.failedToOpenCurrentPlayback' => 'Aktuelle Wiedergabe konnte nicht geöffnet werden', 'watchTogether.failedToOpenCurrentPlayback' => 'Aktuelle Wiedergabe konnte nicht geöffnet werden',
@@ -3656,12 +3643,9 @@ extension on TranslationsDe {
'services.libraryFilter.modeHintWhitelist' => 'Nur die unten markierten Mediatheken synchronisieren.', 'services.libraryFilter.modeHintWhitelist' => 'Nur die unten markierten Mediatheken synchronisieren.',
'services.libraryFilter.libraries' => 'Mediatheken', 'services.libraryFilter.libraries' => 'Mediatheken',
'services.libraryFilter.noLibraries' => 'Keine Mediatheken verfügbar', 'services.libraryFilter.noLibraries' => 'Keine Mediatheken verfügbar',
'addServer.addJellyfinTitle' => 'Jellyfin-Server hinzufügen',
'addServer.serverUrls' => 'Server-URLs', 'addServer.serverUrls' => 'Server-URLs',
'addServer.serverUrlsHelper' => 'Mehrere URLs möglich, durch Kommas getrennt.', 'addServer.serverUrlsHelper' => 'Mehrere URLs möglich, durch Kommas getrennt.',
'addServer.findServer' => 'Server finden', 'addServer.findServer' => 'Server finden',
'addServer.searchingLocalServers' => 'Suche nach lokalen Jellyfin-Servern …',
'addServer.localServers' => 'Lokale Jellyfin-Server',
'addServer.username' => 'Benutzername', 'addServer.username' => 'Benutzername',
'addServer.password' => 'Passwort', 'addServer.password' => 'Passwort',
'addServer.signIn' => 'Anmelden', 'addServer.signIn' => 'Anmelden',
@@ -3673,15 +3657,11 @@ extension on TranslationsDe {
'addServer.addPlexTitle' => 'Mit Plex anmelden', 'addServer.addPlexTitle' => 'Mit Plex anmelden',
'addServer.pinExpired' => 'PIN ist vor der Anmeldung abgelaufen. Bitte erneut versuchen.', 'addServer.pinExpired' => 'PIN ist vor der Anmeldung abgelaufen. Bitte erneut versuchen.',
'addServer.failedToRegisterAccount' => ({required Object error}) => 'Konto konnte nicht registriert werden: ${error}', 'addServer.failedToRegisterAccount' => ({required Object error}) => 'Konto konnte nicht registriert werden: ${error}',
'addServer.enterJellyfinUrlError' => 'Gib die URL deines Jellyfin-Servers ein',
'addServer.addConnectionTitle' => 'Verbindung hinzufügen', 'addServer.addConnectionTitle' => 'Verbindung hinzufügen',
'addServer.addConnectionTitleScoped' => ({required Object name}) => 'Zu ${name} hinzufügen', 'addServer.addConnectionTitleScoped' => ({required Object name}) => 'Zu ${name} hinzufügen',
'addServer.signInWithPlexCard' => 'Mit Plex anmelden', 'addServer.signInWithPlexCard' => 'Mit Plex anmelden',
'addServer.signInWithPlexCardSubtitle' => 'Dieses Gerät autorisieren. Geteilte Server werden hinzugefügt.', 'addServer.signInWithPlexCardSubtitle' => 'Dieses Gerät autorisieren. Geteilte Server werden hinzugefügt.',
'addServer.signInWithPlexCardSubtitleScoped' => 'Ein Plex-Konto autorisieren. Home-Benutzer werden zu Profilen.', 'addServer.signInWithPlexCardSubtitleScoped' => 'Ein Plex-Konto autorisieren. Home-Benutzer werden zu Profilen.',
'addServer.connectToJellyfinCard' => 'Mit Jellyfin verbinden',
'addServer.connectToJellyfinCardSubtitle' => 'Gib Server-URL, Benutzername und Passwort ein.',
'addServer.connectToJellyfinCardSubtitleScoped' => ({required Object name}) => 'Bei einem Jellyfin-Server anmelden. Wird mit ${name} verknüpft.',
'addServer.borrowFromAnotherProfile' => 'Von einem anderen Profil ausleihen', 'addServer.borrowFromAnotherProfile' => 'Von einem anderen Profil ausleihen',
'addServer.borrowFromAnotherProfileSubtitle' => 'Verbindung eines anderen Profils wiederverwenden. PIN-geschützte Profile erfordern eine PIN.', 'addServer.borrowFromAnotherProfileSubtitle' => 'Verbindung eines anderen Profils wiederverwenden. PIN-geschützte Profile erfordern eine PIN.',
_ => null, _ => null,
+40 -40
View File
@@ -136,8 +136,8 @@ class Translations$auth$en {
/// en: 'or' /// en: 'or'
String get or => 'or'; String get or => 'or';
/// en: 'Connect to Jellyfin' /// en: 'Connect to ${product}'
String get connectToJellyfin => 'Connect to Jellyfin'; String connectToMediaBrowser({required Object product}) => 'Connect to ${product}';
/// en: 'Use Quick Connect' /// en: 'Use Quick Connect'
String get useQuickConnect => 'Use Quick Connect'; String get useQuickConnect => 'Use Quick Connect';
@@ -2413,8 +2413,8 @@ class Translations$profiles$en {
/// en: 'Nothing to borrow yet.' /// en: 'Nothing to borrow yet.'
String get borrowEmpty => 'Nothing to borrow yet.'; String get borrowEmpty => 'Nothing to borrow yet.';
/// en: 'Connect Plex or Jellyfin to another profile first.' /// en: 'Connect Plex, Jellyfin, or Emby to another profile first.'
String get borrowEmptySubtitle => 'Connect Plex or Jellyfin to another profile first.'; String get borrowEmptySubtitle => 'Connect Plex, Jellyfin, or Emby to another profile first.';
/// en: 'Available connections could not be loaded. Try again.' /// en: 'Available connections could not be loaded. Try again.'
String get borrowLoadFailed => 'Available connections could not be loaded. Try again.'; String get borrowLoadFailed => 'Available connections could not be loaded. Try again.';
@@ -2476,11 +2476,11 @@ class Translations$connections$en {
/// en: 'Add connection' /// en: 'Add connection'
String get addConnection => 'Add connection'; String get addConnection => 'Add connection';
/// en: 'Sign in with Plex or connect a Jellyfin server' /// en: 'Sign in with Plex or connect a Jellyfin or Emby server'
String get addConnectionSubtitleNoProfile => 'Sign in with Plex or connect a Jellyfin server'; String get addConnectionSubtitleNoProfile => 'Sign in with Plex or connect a Jellyfin or Emby server';
/// en: 'Add to ${displayName}: Plex, Jellyfin, or another profile connection' /// en: 'Add to ${displayName}: Plex, Jellyfin, Emby, or another profile connection'
String addConnectionSubtitleScoped({required Object displayName}) => 'Add to ${displayName}: Plex, Jellyfin, or another profile connection'; String addConnectionSubtitleScoped({required Object displayName}) => 'Add to ${displayName}: Plex, Jellyfin, Emby, or another profile connection';
/// en: 'Session expired for ${name}' /// en: 'Session expired for ${name}'
String sessionExpiredOne({required Object name}) => 'Session expired for ${name}'; String sessionExpiredOne({required Object name}) => 'Session expired for ${name}';
@@ -2491,11 +2491,11 @@ class Translations$connections$en {
/// en: 'Sign in again' /// en: 'Sign in again'
String get signInAgain => 'Sign in again'; String get signInAgain => 'Sign in again';
/// en: 'Edit Jellyfin connection' /// en: 'Edit ${product} connection'
String get editJellyfinTitle => 'Edit Jellyfin connection'; String editMediaBrowserTitle({required Object product}) => 'Edit ${product} connection';
/// en: 'Add or remove URLs for ${serverName}. Plezy will use the reachable URL with the lowest latency.' /// en: 'Add or remove URLs for ${serverName}. Plezy will use the reachable URL with the lowest latency.'
String editJellyfinIntro({required Object serverName}) => 'Add or remove URLs for ${serverName}. Plezy will use the reachable URL with the lowest latency.'; String editMediaBrowserIntro({required Object serverName}) => 'Add or remove URLs for ${serverName}. Plezy will use the reachable URL with the lowest latency.';
} }
// Path: discover // Path: discover
@@ -2778,8 +2778,8 @@ class Translations$about$en {
/// en: 'Version ${version}' /// en: 'Version ${version}'
String versionLabel({required Object version}) => 'Version ${version}'; String versionLabel({required Object version}) => 'Version ${version}';
/// en: 'A beautiful Plex and Jellyfin client for Flutter' /// en: 'A beautiful Plex, Jellyfin, and Emby client for Flutter'
String get appDescription => 'A beautiful Plex and Jellyfin client for Flutter'; String get appDescription => 'A beautiful Plex, Jellyfin, and Emby client for Flutter';
/// en: 'View licenses of third-party libraries' /// en: 'View licenses of third-party libraries'
String get viewLicensesDescription => 'View licenses of third-party libraries'; String get viewLicensesDescription => 'View licenses of third-party libraries';
@@ -4747,8 +4747,8 @@ class Translations$addServer$en {
// Translations // Translations
/// en: 'Add Jellyfin server' /// en: 'Add ${product} server'
String get addJellyfinTitle => 'Add Jellyfin server'; String addMediaBrowserTitle({required Object product}) => 'Add ${product} server';
/// en: 'Server URLs' /// en: 'Server URLs'
String get serverUrls => 'Server URLs'; String get serverUrls => 'Server URLs';
@@ -4759,11 +4759,11 @@ class Translations$addServer$en {
/// en: 'Find server' /// en: 'Find server'
String get findServer => 'Find server'; String get findServer => 'Find server';
/// en: 'Looking for local Jellyfin servers...' /// en: 'Looking for local ${product} servers...'
String get searchingLocalServers => 'Looking for local Jellyfin servers...'; String searchingLocalMediaBrowserServers({required Object product}) => 'Looking for local ${product} servers...';
/// en: 'Local Jellyfin servers' /// en: 'Local ${product} servers'
String get localServers => 'Local Jellyfin servers'; String localMediaBrowserServers({required Object product}) => 'Local ${product} servers';
/// en: 'Username' /// en: 'Username'
String get username => 'Username'; String get username => 'Username';
@@ -4798,8 +4798,8 @@ class Translations$addServer$en {
/// en: 'Failed to register account: ${error}' /// en: 'Failed to register account: ${error}'
String failedToRegisterAccount({required Object error}) => 'Failed to register account: ${error}'; String failedToRegisterAccount({required Object error}) => 'Failed to register account: ${error}';
/// en: 'Enter your Jellyfin server URL' /// en: 'Enter your ${product} server URL'
String get enterJellyfinUrlError => 'Enter your Jellyfin server URL'; String enterMediaBrowserUrlError({required Object product}) => 'Enter your ${product} server URL';
/// en: 'Add connection' /// en: 'Add connection'
String get addConnectionTitle => 'Add connection'; String get addConnectionTitle => 'Add connection';
@@ -4816,14 +4816,14 @@ class Translations$addServer$en {
/// en: 'Authorize a Plex account. Home users become profiles.' /// en: 'Authorize a Plex account. Home users become profiles.'
String get signInWithPlexCardSubtitleScoped => 'Authorize a Plex account. Home users become profiles.'; String get signInWithPlexCardSubtitleScoped => 'Authorize a Plex account. Home users become profiles.';
/// en: 'Connect to Jellyfin' /// en: 'Connect to ${product}'
String get connectToJellyfinCard => 'Connect to Jellyfin'; String connectToMediaBrowserCard({required Object product}) => 'Connect to ${product}';
/// en: 'Enter your server URL, username, and password.' /// en: 'Enter your server URL, username, and password.'
String get connectToJellyfinCardSubtitle => 'Enter your server URL, username, and password.'; String get connectToMediaBrowserCardSubtitle => 'Enter your server URL, username, and password.';
/// en: 'Sign in to a Jellyfin server. Binds to ${name}.' /// en: 'Sign in to your ${product} server. Binds to ${name}.'
String connectToJellyfinCardSubtitleScoped({required Object name}) => 'Sign in to a Jellyfin server. Binds to ${name}.'; String connectToMediaBrowserCardSubtitleScoped({required Object product, required Object name}) => 'Sign in to your ${product} server. Binds to ${name}.';
/// en: 'Borrow from another profile' /// en: 'Borrow from another profile'
String get borrowFromAnotherProfile => 'Borrow from another profile'; String get borrowFromAnotherProfile => 'Borrow from another profile';
@@ -6043,7 +6043,7 @@ extension on Translations {
'auth.waitingForAuth' => 'Waiting for authentication...\nSign in from your browser.', 'auth.waitingForAuth' => 'Waiting for authentication...\nSign in from your browser.',
'auth.useBrowser' => 'Use browser', 'auth.useBrowser' => 'Use browser',
'auth.or' => 'or', 'auth.or' => 'or',
'auth.connectToJellyfin' => 'Connect to Jellyfin', 'auth.connectToMediaBrowser' => ({required Object product}) => 'Connect to ${product}',
'auth.useQuickConnect' => 'Use Quick Connect', 'auth.useQuickConnect' => 'Use Quick Connect',
'auth.quickConnectInstructions' => 'Open Quick Connect in Jellyfin and enter this code.', 'auth.quickConnectInstructions' => 'Open Quick Connect in Jellyfin and enter this code.',
'auth.quickConnectWaiting' => 'Waiting for approval…', 'auth.quickConnectWaiting' => 'Waiting for approval…',
@@ -6790,7 +6790,7 @@ extension on Translations {
'profiles.borrowAddTo' => ({required Object displayName}) => 'Add to ${displayName}', 'profiles.borrowAddTo' => ({required Object displayName}) => 'Add to ${displayName}',
'profiles.borrowExplain' => 'Borrow another profile\'s connection. PIN-protected profiles require a PIN.', 'profiles.borrowExplain' => 'Borrow another profile\'s connection. PIN-protected profiles require a PIN.',
'profiles.borrowEmpty' => 'Nothing to borrow yet.', 'profiles.borrowEmpty' => 'Nothing to borrow yet.',
'profiles.borrowEmptySubtitle' => 'Connect Plex or Jellyfin to another profile first.', 'profiles.borrowEmptySubtitle' => 'Connect Plex, Jellyfin, or Emby to another profile first.',
'profiles.borrowLoadFailed' => 'Available connections could not be loaded. Try again.', 'profiles.borrowLoadFailed' => 'Available connections could not be loaded. Try again.',
'profiles.borrowFromProfile' => ({required Object displayName}) => 'From ${displayName}', 'profiles.borrowFromProfile' => ({required Object displayName}) => 'From ${displayName}',
'profiles.borrowConnectionBorrowed' => 'Connection borrowed.', 'profiles.borrowConnectionBorrowed' => 'Connection borrowed.',
@@ -6808,13 +6808,13 @@ extension on Translations {
'profiles.pinsDontMatch' => 'PINs don\'t match', 'profiles.pinsDontMatch' => 'PINs don\'t match',
'connections.sectionTitle' => 'Connections', 'connections.sectionTitle' => 'Connections',
'connections.addConnection' => 'Add connection', 'connections.addConnection' => 'Add connection',
'connections.addConnectionSubtitleNoProfile' => 'Sign in with Plex or connect a Jellyfin server', 'connections.addConnectionSubtitleNoProfile' => 'Sign in with Plex or connect a Jellyfin or Emby server',
'connections.addConnectionSubtitleScoped' => ({required Object displayName}) => 'Add to ${displayName}: Plex, Jellyfin, or another profile connection', 'connections.addConnectionSubtitleScoped' => ({required Object displayName}) => 'Add to ${displayName}: Plex, Jellyfin, Emby, or another profile connection',
'connections.sessionExpiredOne' => ({required Object name}) => 'Session expired for ${name}', 'connections.sessionExpiredOne' => ({required Object name}) => 'Session expired for ${name}',
'connections.sessionExpiredMany' => ({required Object count}) => 'Session expired for ${count} servers', 'connections.sessionExpiredMany' => ({required Object count}) => 'Session expired for ${count} servers',
'connections.signInAgain' => 'Sign in again', 'connections.signInAgain' => 'Sign in again',
'connections.editJellyfinTitle' => 'Edit Jellyfin connection', 'connections.editMediaBrowserTitle' => ({required Object product}) => 'Edit ${product} connection',
'connections.editJellyfinIntro' => ({required Object serverName}) => 'Add or remove URLs for ${serverName}. Plezy will use the reachable URL with the lowest latency.', 'connections.editMediaBrowserIntro' => ({required Object serverName}) => 'Add or remove URLs for ${serverName}. Plezy will use the reachable URL with the lowest latency.',
'discover.title' => 'Discover', 'discover.title' => 'Discover',
'discover.noContentAvailable' => 'No content available', 'discover.noContentAvailable' => 'No content available',
'discover.addMediaToLibraries' => 'Add some media to your libraries', 'discover.addMediaToLibraries' => 'Add some media to your libraries',
@@ -6936,7 +6936,7 @@ extension on Translations {
'about.title' => 'About', 'about.title' => 'About',
'about.openSourceLicenses' => 'Open Source Licenses', 'about.openSourceLicenses' => 'Open Source Licenses',
'about.versionLabel' => ({required Object version}) => 'Version ${version}', 'about.versionLabel' => ({required Object version}) => 'Version ${version}',
'about.appDescription' => 'A beautiful Plex and Jellyfin client for Flutter', 'about.appDescription' => 'A beautiful Plex, Jellyfin, and Emby client for Flutter',
'about.viewLicensesDescription' => 'View licenses of third-party libraries', 'about.viewLicensesDescription' => 'View licenses of third-party libraries',
'serverSelection.noServersFoundForAccount' => ({required Object username, required Object email}) => 'No servers found for ${username} (${email})', 'serverSelection.noServersFoundForAccount' => ({required Object username, required Object email}) => 'No servers found for ${username} (${email})',
'serverSelection.failedToLoadServers' => ({required Object error}) => 'Failed to load servers: ${error}', 'serverSelection.failedToLoadServers' => ({required Object error}) => 'Failed to load servers: ${error}',
@@ -7744,12 +7744,12 @@ extension on Translations {
'services.libraryFilter.modeHintWhitelist' => 'Sync only the libraries checked below.', 'services.libraryFilter.modeHintWhitelist' => 'Sync only the libraries checked below.',
'services.libraryFilter.libraries' => 'Libraries', 'services.libraryFilter.libraries' => 'Libraries',
'services.libraryFilter.noLibraries' => 'No libraries available', 'services.libraryFilter.noLibraries' => 'No libraries available',
'addServer.addJellyfinTitle' => 'Add Jellyfin server', 'addServer.addMediaBrowserTitle' => ({required Object product}) => 'Add ${product} server',
'addServer.serverUrls' => 'Server URLs', 'addServer.serverUrls' => 'Server URLs',
'addServer.serverUrlsHelper' => 'Multiple URLs allowed, separated by commas.', 'addServer.serverUrlsHelper' => 'Multiple URLs allowed, separated by commas.',
'addServer.findServer' => 'Find server', 'addServer.findServer' => 'Find server',
'addServer.searchingLocalServers' => 'Looking for local Jellyfin servers...', 'addServer.searchingLocalMediaBrowserServers' => ({required Object product}) => 'Looking for local ${product} servers...',
'addServer.localServers' => 'Local Jellyfin servers', 'addServer.localMediaBrowserServers' => ({required Object product}) => 'Local ${product} servers',
'addServer.username' => 'Username', 'addServer.username' => 'Username',
'addServer.password' => 'Password', 'addServer.password' => 'Password',
'addServer.signIn' => 'Sign in', 'addServer.signIn' => 'Sign in',
@@ -7761,15 +7761,15 @@ extension on Translations {
'addServer.addPlexTitle' => 'Sign in with Plex', 'addServer.addPlexTitle' => 'Sign in with Plex',
'addServer.pinExpired' => 'PIN expired before sign-in. Please try again.', 'addServer.pinExpired' => 'PIN expired before sign-in. Please try again.',
'addServer.failedToRegisterAccount' => ({required Object error}) => 'Failed to register account: ${error}', 'addServer.failedToRegisterAccount' => ({required Object error}) => 'Failed to register account: ${error}',
'addServer.enterJellyfinUrlError' => 'Enter your Jellyfin server URL', 'addServer.enterMediaBrowserUrlError' => ({required Object product}) => 'Enter your ${product} server URL',
'addServer.addConnectionTitle' => 'Add connection', 'addServer.addConnectionTitle' => 'Add connection',
'addServer.addConnectionTitleScoped' => ({required Object name}) => 'Add to ${name}', 'addServer.addConnectionTitleScoped' => ({required Object name}) => 'Add to ${name}',
'addServer.signInWithPlexCard' => 'Sign in with Plex', 'addServer.signInWithPlexCard' => 'Sign in with Plex',
'addServer.signInWithPlexCardSubtitle' => 'Authorize this device. Shared servers are added.', 'addServer.signInWithPlexCardSubtitle' => 'Authorize this device. Shared servers are added.',
'addServer.signInWithPlexCardSubtitleScoped' => 'Authorize a Plex account. Home users become profiles.', 'addServer.signInWithPlexCardSubtitleScoped' => 'Authorize a Plex account. Home users become profiles.',
'addServer.connectToJellyfinCard' => 'Connect to Jellyfin', 'addServer.connectToMediaBrowserCard' => ({required Object product}) => 'Connect to ${product}',
'addServer.connectToJellyfinCardSubtitle' => 'Enter your server URL, username, and password.', 'addServer.connectToMediaBrowserCardSubtitle' => 'Enter your server URL, username, and password.',
'addServer.connectToJellyfinCardSubtitleScoped' => ({required Object name}) => 'Sign in to a Jellyfin server. Binds to ${name}.', 'addServer.connectToMediaBrowserCardSubtitleScoped' => ({required Object product, required Object name}) => 'Sign in to your ${product} server. Binds to ${name}.',
'addServer.borrowFromAnotherProfile' => 'Borrow from another profile', 'addServer.borrowFromAnotherProfile' => 'Borrow from another profile',
'addServer.borrowFromAnotherProfileSubtitle' => 'Reuse another profile\'s connection. PIN-protected profiles require a PIN.', 'addServer.borrowFromAnotherProfileSubtitle' => 'Reuse another profile\'s connection. PIN-protected profiles require a PIN.',
_ => null, _ => null,
+3 -23
View File
@@ -115,7 +115,6 @@ class _Translations$auth$es extends Translations$auth$en {
@override String get waitingForAuth => 'Esperando autenticación...\nInicia sesión desde tu navegador.'; @override String get waitingForAuth => 'Esperando autenticación...\nInicia sesión desde tu navegador.';
@override String get useBrowser => 'Usar navegador'; @override String get useBrowser => 'Usar navegador';
@override String get or => 'o'; @override String get or => 'o';
@override String get connectToJellyfin => 'Conectar a Jellyfin';
@override String get useQuickConnect => 'Usar Quick Connect'; @override String get useQuickConnect => 'Usar Quick Connect';
@override String get quickConnectInstructions => 'Abre Quick Connect en Jellyfin e introduce este código.'; @override String get quickConnectInstructions => 'Abre Quick Connect en Jellyfin e introduce este código.';
@override String get quickConnectWaiting => 'Esperando aprobación…'; @override String get quickConnectWaiting => 'Esperando aprobación…';
@@ -918,8 +917,6 @@ class _Translations$connections$es extends Translations$connections$en {
@override String sessionExpiredOne({required Object name}) => 'Sesión caducada para ${name}'; @override String sessionExpiredOne({required Object name}) => 'Sesión caducada para ${name}';
@override String sessionExpiredMany({required Object count}) => 'Sesión caducada para ${count} servidores'; @override String sessionExpiredMany({required Object count}) => 'Sesión caducada para ${count} servidores';
@override String get signInAgain => 'Iniciar sesión de nuevo'; @override String get signInAgain => 'Iniciar sesión de nuevo';
@override String get editJellyfinTitle => 'Editar conexión de Jellyfin';
@override String editJellyfinIntro({required Object serverName}) => 'Añade o elimina direcciones URL para ${serverName}. Plezy usará la dirección accesible con menor latencia.';
} }
// Path: discover // Path: discover
@@ -1803,12 +1800,9 @@ class _Translations$addServer$es extends Translations$addServer$en {
final TranslationsEs _root; // ignore: unused_field final TranslationsEs _root; // ignore: unused_field
// Translations // Translations
@override String get addJellyfinTitle => 'Añadir servidor Jellyfin';
@override String get serverUrls => 'Direcciones URL del servidor'; @override String get serverUrls => 'Direcciones URL del servidor';
@override String get serverUrlsHelper => 'Se permiten varias URL, separadas por comas.'; @override String get serverUrlsHelper => 'Se permiten varias URL, separadas por comas.';
@override String get findServer => 'Buscar servidor'; @override String get findServer => 'Buscar servidor';
@override String get searchingLocalServers => 'Buscando servidores Jellyfin locales...';
@override String get localServers => 'Servidores Jellyfin locales';
@override String get username => 'Usuario'; @override String get username => 'Usuario';
@override String get password => 'Contraseña'; @override String get password => 'Contraseña';
@override String get signIn => 'Iniciar sesión'; @override String get signIn => 'Iniciar sesión';
@@ -1820,15 +1814,11 @@ class _Translations$addServer$es extends Translations$addServer$en {
@override String get addPlexTitle => 'Iniciar sesión con Plex'; @override String get addPlexTitle => 'Iniciar sesión con Plex';
@override String get pinExpired => 'El PIN caducó antes de iniciar sesión. Inténtalo de nuevo.'; @override String get pinExpired => 'El PIN caducó antes de iniciar sesión. Inténtalo de nuevo.';
@override String failedToRegisterAccount({required Object error}) => 'No se pudo registrar la cuenta: ${error}'; @override String failedToRegisterAccount({required Object error}) => 'No se pudo registrar la cuenta: ${error}';
@override String get enterJellyfinUrlError => 'Introduce la URL de tu servidor Jellyfin';
@override String get addConnectionTitle => 'Añadir conexión'; @override String get addConnectionTitle => 'Añadir conexión';
@override String addConnectionTitleScoped({required Object name}) => 'Añadir a ${name}'; @override String addConnectionTitleScoped({required Object name}) => 'Añadir a ${name}';
@override String get signInWithPlexCard => 'Iniciar sesión con Plex'; @override String get signInWithPlexCard => 'Iniciar sesión con Plex';
@override String get signInWithPlexCardSubtitle => 'Autoriza este dispositivo. Se añaden servidores compartidos.'; @override String get signInWithPlexCardSubtitle => 'Autoriza este dispositivo. Se añaden servidores compartidos.';
@override String get signInWithPlexCardSubtitleScoped => 'Autoriza una cuenta Plex. Los usuarios de Home se convierten en perfiles.'; @override String get signInWithPlexCardSubtitleScoped => 'Autoriza una cuenta Plex. Los usuarios de Home se convierten en perfiles.';
@override String get connectToJellyfinCard => 'Conectar a Jellyfin';
@override String get connectToJellyfinCardSubtitle => 'Introduce la URL del servidor, usuario y contraseña.';
@override String connectToJellyfinCardSubtitleScoped({required Object name}) => 'Inicia sesión en un servidor Jellyfin. Se vincula a ${name}.';
@override String get borrowFromAnotherProfile => 'Tomar prestado de otro perfil'; @override String get borrowFromAnotherProfile => 'Tomar prestado de otro perfil';
@override String get borrowFromAnotherProfileSubtitle => 'Reutiliza la conexión de otro perfil. Los perfiles protegidos con PIN requieren un PIN.'; @override String get borrowFromAnotherProfileSubtitle => 'Reutiliza la conexión de otro perfil. Los perfiles protegidos con PIN requieren un PIN.';
} }
@@ -2208,7 +2198,6 @@ extension on TranslationsEs {
'auth.waitingForAuth' => 'Esperando autenticación...\nInicia sesión desde tu navegador.', 'auth.waitingForAuth' => 'Esperando autenticación...\nInicia sesión desde tu navegador.',
'auth.useBrowser' => 'Usar navegador', 'auth.useBrowser' => 'Usar navegador',
'auth.or' => 'o', 'auth.or' => 'o',
'auth.connectToJellyfin' => 'Conectar a Jellyfin',
'auth.useQuickConnect' => 'Usar Quick Connect', 'auth.useQuickConnect' => 'Usar Quick Connect',
'auth.quickConnectInstructions' => 'Abre Quick Connect en Jellyfin e introduce este código.', 'auth.quickConnectInstructions' => 'Abre Quick Connect en Jellyfin e introduce este código.',
'auth.quickConnectWaiting' => 'Esperando aprobación…', 'auth.quickConnectWaiting' => 'Esperando aprobación…',
@@ -2711,9 +2700,9 @@ extension on TranslationsEs {
'videoControls.searchSubtitles' => 'Buscar subtítulos', 'videoControls.searchSubtitles' => 'Buscar subtítulos',
'videoControls.language' => 'Idioma', 'videoControls.language' => 'Idioma',
'videoControls.noSubtitlesFound' => 'No se encontraron subtítulos', 'videoControls.noSubtitlesFound' => 'No se encontraron subtítulos',
'videoControls.subtitleDownloaded' => 'Subtítulo descargado',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'videoControls.subtitleDownloaded' => 'Subtítulo descargado',
'videoControls.subtitleDownloadedNotApplied' => 'El subtítulo se descargó, pero no se pudo seleccionar', 'videoControls.subtitleDownloadedNotApplied' => 'El subtítulo se descargó, pero no se pudo seleccionar',
'videoControls.subtitleDownloadFailed' => 'Error al descargar subtítulo', 'videoControls.subtitleDownloadFailed' => 'Error al descargar subtítulo',
'videoControls.searchLanguages' => 'Buscar idiomas...', 'videoControls.searchLanguages' => 'Buscar idiomas...',
@@ -2869,8 +2858,6 @@ extension on TranslationsEs {
'connections.sessionExpiredOne' => ({required Object name}) => 'Sesión caducada para ${name}', 'connections.sessionExpiredOne' => ({required Object name}) => 'Sesión caducada para ${name}',
'connections.sessionExpiredMany' => ({required Object count}) => 'Sesión caducada para ${count} servidores', 'connections.sessionExpiredMany' => ({required Object count}) => 'Sesión caducada para ${count} servidores',
'connections.signInAgain' => 'Iniciar sesión de nuevo', 'connections.signInAgain' => 'Iniciar sesión de nuevo',
'connections.editJellyfinTitle' => 'Editar conexión de Jellyfin',
'connections.editJellyfinIntro' => ({required Object serverName}) => 'Añade o elimina direcciones URL para ${serverName}. Plezy usará la dirección accesible con menor latencia.',
'discover.title' => 'Descubrir', 'discover.title' => 'Descubrir',
'discover.noContentAvailable' => 'No hay contenido disponible', 'discover.noContentAvailable' => 'No hay contenido disponible',
'discover.addMediaToLibraries' => 'Añade contenido a tus bibliotecas', 'discover.addMediaToLibraries' => 'Añade contenido a tus bibliotecas',
@@ -3225,11 +3212,11 @@ extension on TranslationsEs {
'watchTogether.failedToCreate' => 'Error al crear la sesión', 'watchTogether.failedToCreate' => 'Error al crear la sesión',
'watchTogether.failedToJoin' => 'Error al unirse a la sesión', 'watchTogether.failedToJoin' => 'Error al unirse a la sesión',
'watchTogether.sessionCodeCopied' => 'Código de sesión copiado al portapapeles', 'watchTogether.sessionCodeCopied' => 'Código de sesión copiado al portapapeles',
_ => null,
} ?? switch (path) {
'watchTogether.relayUnreachable' => 'No se puede acceder al servidor de retransmisión. Es posible que tu proveedor de internet esté bloqueando Ver juntos.', 'watchTogether.relayUnreachable' => 'No se puede acceder al servidor de retransmisión. Es posible que tu proveedor de internet esté bloqueando Ver juntos.',
'watchTogether.reconnectingToHost' => 'Reconectando con el anfitrión...', 'watchTogether.reconnectingToHost' => 'Reconectando con el anfitrión...',
'watchTogether.currentPlayback' => 'Reproducción actual', 'watchTogether.currentPlayback' => 'Reproducción actual',
_ => null,
} ?? switch (path) {
'watchTogether.joinCurrentPlayback' => 'Unirse a la reproducción actual', 'watchTogether.joinCurrentPlayback' => 'Unirse a la reproducción actual',
'watchTogether.joinCurrentPlaybackDescription' => 'Vuelve a lo que el anfitrión está viendo ahora mismo', 'watchTogether.joinCurrentPlaybackDescription' => 'Vuelve a lo que el anfitrión está viendo ahora mismo',
'watchTogether.failedToOpenCurrentPlayback' => 'No se pudo abrir la reproducción actual', 'watchTogether.failedToOpenCurrentPlayback' => 'No se pudo abrir la reproducción actual',
@@ -3656,12 +3643,9 @@ extension on TranslationsEs {
'services.libraryFilter.modeHintWhitelist' => 'Sincronizar solo las bibliotecas seleccionadas abajo.', 'services.libraryFilter.modeHintWhitelist' => 'Sincronizar solo las bibliotecas seleccionadas abajo.',
'services.libraryFilter.libraries' => 'Bibliotecas', 'services.libraryFilter.libraries' => 'Bibliotecas',
'services.libraryFilter.noLibraries' => 'No hay bibliotecas disponibles', 'services.libraryFilter.noLibraries' => 'No hay bibliotecas disponibles',
'addServer.addJellyfinTitle' => 'Añadir servidor Jellyfin',
'addServer.serverUrls' => 'Direcciones URL del servidor', 'addServer.serverUrls' => 'Direcciones URL del servidor',
'addServer.serverUrlsHelper' => 'Se permiten varias URL, separadas por comas.', 'addServer.serverUrlsHelper' => 'Se permiten varias URL, separadas por comas.',
'addServer.findServer' => 'Buscar servidor', 'addServer.findServer' => 'Buscar servidor',
'addServer.searchingLocalServers' => 'Buscando servidores Jellyfin locales...',
'addServer.localServers' => 'Servidores Jellyfin locales',
'addServer.username' => 'Usuario', 'addServer.username' => 'Usuario',
'addServer.password' => 'Contraseña', 'addServer.password' => 'Contraseña',
'addServer.signIn' => 'Iniciar sesión', 'addServer.signIn' => 'Iniciar sesión',
@@ -3673,15 +3657,11 @@ extension on TranslationsEs {
'addServer.addPlexTitle' => 'Iniciar sesión con Plex', 'addServer.addPlexTitle' => 'Iniciar sesión con Plex',
'addServer.pinExpired' => 'El PIN caducó antes de iniciar sesión. Inténtalo de nuevo.', 'addServer.pinExpired' => 'El PIN caducó antes de iniciar sesión. Inténtalo de nuevo.',
'addServer.failedToRegisterAccount' => ({required Object error}) => 'No se pudo registrar la cuenta: ${error}', 'addServer.failedToRegisterAccount' => ({required Object error}) => 'No se pudo registrar la cuenta: ${error}',
'addServer.enterJellyfinUrlError' => 'Introduce la URL de tu servidor Jellyfin',
'addServer.addConnectionTitle' => 'Añadir conexión', 'addServer.addConnectionTitle' => 'Añadir conexión',
'addServer.addConnectionTitleScoped' => ({required Object name}) => 'Añadir a ${name}', 'addServer.addConnectionTitleScoped' => ({required Object name}) => 'Añadir a ${name}',
'addServer.signInWithPlexCard' => 'Iniciar sesión con Plex', 'addServer.signInWithPlexCard' => 'Iniciar sesión con Plex',
'addServer.signInWithPlexCardSubtitle' => 'Autoriza este dispositivo. Se añaden servidores compartidos.', 'addServer.signInWithPlexCardSubtitle' => 'Autoriza este dispositivo. Se añaden servidores compartidos.',
'addServer.signInWithPlexCardSubtitleScoped' => 'Autoriza una cuenta Plex. Los usuarios de Home se convierten en perfiles.', 'addServer.signInWithPlexCardSubtitleScoped' => 'Autoriza una cuenta Plex. Los usuarios de Home se convierten en perfiles.',
'addServer.connectToJellyfinCard' => 'Conectar a Jellyfin',
'addServer.connectToJellyfinCardSubtitle' => 'Introduce la URL del servidor, usuario y contraseña.',
'addServer.connectToJellyfinCardSubtitleScoped' => ({required Object name}) => 'Inicia sesión en un servidor Jellyfin. Se vincula a ${name}.',
'addServer.borrowFromAnotherProfile' => 'Tomar prestado de otro perfil', 'addServer.borrowFromAnotherProfile' => 'Tomar prestado de otro perfil',
'addServer.borrowFromAnotherProfileSubtitle' => 'Reutiliza la conexión de otro perfil. Los perfiles protegidos con PIN requieren un PIN.', 'addServer.borrowFromAnotherProfileSubtitle' => 'Reutiliza la conexión de otro perfil. Los perfiles protegidos con PIN requieren un PIN.',
_ => null, _ => null,
+3 -23
View File
@@ -115,7 +115,6 @@ class _Translations$auth$fr extends Translations$auth$en {
@override String get waitingForAuth => 'En attente d\'authentification...\nConnectez-vous depuis votre navigateur.'; @override String get waitingForAuth => 'En attente d\'authentification...\nConnectez-vous depuis votre navigateur.';
@override String get useBrowser => 'Utiliser le navigateur'; @override String get useBrowser => 'Utiliser le navigateur';
@override String get or => 'ou'; @override String get or => 'ou';
@override String get connectToJellyfin => 'Se connecter à Jellyfin';
@override String get useQuickConnect => 'Utiliser Quick Connect'; @override String get useQuickConnect => 'Utiliser Quick Connect';
@override String get quickConnectInstructions => 'Ouvrez Quick Connect dans Jellyfin et saisissez ce code.'; @override String get quickConnectInstructions => 'Ouvrez Quick Connect dans Jellyfin et saisissez ce code.';
@override String get quickConnectWaiting => 'En attente d\'approbation…'; @override String get quickConnectWaiting => 'En attente d\'approbation…';
@@ -918,8 +917,6 @@ class _Translations$connections$fr extends Translations$connections$en {
@override String sessionExpiredOne({required Object name}) => 'Session expirée pour ${name}'; @override String sessionExpiredOne({required Object name}) => 'Session expirée pour ${name}';
@override String sessionExpiredMany({required Object count}) => 'Session expirée pour ${count} serveurs'; @override String sessionExpiredMany({required Object count}) => 'Session expirée pour ${count} serveurs';
@override String get signInAgain => 'Se reconnecter'; @override String get signInAgain => 'Se reconnecter';
@override String get editJellyfinTitle => 'Modifier la connexion Jellyfin';
@override String editJellyfinIntro({required Object serverName}) => 'Ajoutez ou supprimez des URL pour ${serverName}. Plezy utilisera l\'URL joignable avec la latence la plus faible.';
} }
// Path: discover // Path: discover
@@ -1803,12 +1800,9 @@ class _Translations$addServer$fr extends Translations$addServer$en {
final TranslationsFr _root; // ignore: unused_field final TranslationsFr _root; // ignore: unused_field
// Translations // Translations
@override String get addJellyfinTitle => 'Ajouter un serveur Jellyfin';
@override String get serverUrls => 'URL du serveur'; @override String get serverUrls => 'URL du serveur';
@override String get serverUrlsHelper => 'Plusieurs URL possibles, séparées par des virgules.'; @override String get serverUrlsHelper => 'Plusieurs URL possibles, séparées par des virgules.';
@override String get findServer => 'Rechercher un serveur'; @override String get findServer => 'Rechercher un serveur';
@override String get searchingLocalServers => 'Recherche de serveurs Jellyfin locaux...';
@override String get localServers => 'Serveurs Jellyfin locaux';
@override String get username => 'Nom d\'utilisateur'; @override String get username => 'Nom d\'utilisateur';
@override String get password => 'Mot de passe'; @override String get password => 'Mot de passe';
@override String get signIn => 'Se connecter'; @override String get signIn => 'Se connecter';
@@ -1820,15 +1814,11 @@ class _Translations$addServer$fr extends Translations$addServer$en {
@override String get addPlexTitle => 'Se connecter avec Plex'; @override String get addPlexTitle => 'Se connecter avec Plex';
@override String get pinExpired => 'Le PIN a expiré avant la connexion. Veuillez réessayer.'; @override String get pinExpired => 'Le PIN a expiré avant la connexion. Veuillez réessayer.';
@override String failedToRegisterAccount({required Object error}) => 'Échec de l\'enregistrement du compte : ${error}'; @override String failedToRegisterAccount({required Object error}) => 'Échec de l\'enregistrement du compte : ${error}';
@override String get enterJellyfinUrlError => 'Saisissez l\'URL de votre serveur Jellyfin';
@override String get addConnectionTitle => 'Ajouter une connexion'; @override String get addConnectionTitle => 'Ajouter une connexion';
@override String addConnectionTitleScoped({required Object name}) => 'Ajouter à ${name}'; @override String addConnectionTitleScoped({required Object name}) => 'Ajouter à ${name}';
@override String get signInWithPlexCard => 'Se connecter avec Plex'; @override String get signInWithPlexCard => 'Se connecter avec Plex';
@override String get signInWithPlexCardSubtitle => 'Autorisez cet appareil. Les serveurs partagés sont ajoutés.'; @override String get signInWithPlexCardSubtitle => 'Autorisez cet appareil. Les serveurs partagés sont ajoutés.';
@override String get signInWithPlexCardSubtitleScoped => 'Autorisez un compte Plex. Les utilisateurs Home deviennent des profils.'; @override String get signInWithPlexCardSubtitleScoped => 'Autorisez un compte Plex. Les utilisateurs Home deviennent des profils.';
@override String get connectToJellyfinCard => 'Se connecter à Jellyfin';
@override String get connectToJellyfinCardSubtitle => 'Saisissez l\'URL du serveur, le nom d\'utilisateur et le mot de passe.';
@override String connectToJellyfinCardSubtitleScoped({required Object name}) => 'Connectez-vous à un serveur Jellyfin. Cette connexion sera liée à ${name}.';
@override String get borrowFromAnotherProfile => 'Emprunter à un autre profil'; @override String get borrowFromAnotherProfile => 'Emprunter à un autre profil';
@override String get borrowFromAnotherProfileSubtitle => 'Réutiliser la connexion d\'un autre profil. Les profils protégés par PIN exigent un PIN.'; @override String get borrowFromAnotherProfileSubtitle => 'Réutiliser la connexion d\'un autre profil. Les profils protégés par PIN exigent un PIN.';
} }
@@ -2208,7 +2198,6 @@ extension on TranslationsFr {
'auth.waitingForAuth' => 'En attente d\'authentification...\nConnectez-vous depuis votre navigateur.', 'auth.waitingForAuth' => 'En attente d\'authentification...\nConnectez-vous depuis votre navigateur.',
'auth.useBrowser' => 'Utiliser le navigateur', 'auth.useBrowser' => 'Utiliser le navigateur',
'auth.or' => 'ou', 'auth.or' => 'ou',
'auth.connectToJellyfin' => 'Se connecter à Jellyfin',
'auth.useQuickConnect' => 'Utiliser Quick Connect', 'auth.useQuickConnect' => 'Utiliser Quick Connect',
'auth.quickConnectInstructions' => 'Ouvrez Quick Connect dans Jellyfin et saisissez ce code.', 'auth.quickConnectInstructions' => 'Ouvrez Quick Connect dans Jellyfin et saisissez ce code.',
'auth.quickConnectWaiting' => 'En attente d\'approbation…', 'auth.quickConnectWaiting' => 'En attente d\'approbation…',
@@ -2711,9 +2700,9 @@ extension on TranslationsFr {
'videoControls.searchSubtitles' => 'Rechercher des sous-titres', 'videoControls.searchSubtitles' => 'Rechercher des sous-titres',
'videoControls.language' => 'Langue', 'videoControls.language' => 'Langue',
'videoControls.noSubtitlesFound' => 'Aucun sous-titre trouvé', 'videoControls.noSubtitlesFound' => 'Aucun sous-titre trouvé',
'videoControls.subtitleDownloaded' => 'Sous-titre téléchargé',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'videoControls.subtitleDownloaded' => 'Sous-titre téléchargé',
'videoControls.subtitleDownloadedNotApplied' => 'Le sous-titre a été téléchargé, mais na pas pu être sélectionné', 'videoControls.subtitleDownloadedNotApplied' => 'Le sous-titre a été téléchargé, mais na pas pu être sélectionné',
'videoControls.subtitleDownloadFailed' => 'Échec du téléchargement du sous-titre', 'videoControls.subtitleDownloadFailed' => 'Échec du téléchargement du sous-titre',
'videoControls.searchLanguages' => 'Rechercher des langues...', 'videoControls.searchLanguages' => 'Rechercher des langues...',
@@ -2869,8 +2858,6 @@ extension on TranslationsFr {
'connections.sessionExpiredOne' => ({required Object name}) => 'Session expirée pour ${name}', 'connections.sessionExpiredOne' => ({required Object name}) => 'Session expirée pour ${name}',
'connections.sessionExpiredMany' => ({required Object count}) => 'Session expirée pour ${count} serveurs', 'connections.sessionExpiredMany' => ({required Object count}) => 'Session expirée pour ${count} serveurs',
'connections.signInAgain' => 'Se reconnecter', 'connections.signInAgain' => 'Se reconnecter',
'connections.editJellyfinTitle' => 'Modifier la connexion Jellyfin',
'connections.editJellyfinIntro' => ({required Object serverName}) => 'Ajoutez ou supprimez des URL pour ${serverName}. Plezy utilisera l\'URL joignable avec la latence la plus faible.',
'discover.title' => 'Découvrir', 'discover.title' => 'Découvrir',
'discover.noContentAvailable' => 'Aucun contenu disponible', 'discover.noContentAvailable' => 'Aucun contenu disponible',
'discover.addMediaToLibraries' => 'Ajoutez des médias à vos bibliothèques', 'discover.addMediaToLibraries' => 'Ajoutez des médias à vos bibliothèques',
@@ -3225,11 +3212,11 @@ extension on TranslationsFr {
'watchTogether.failedToCreate' => 'Échec de la création de la session', 'watchTogether.failedToCreate' => 'Échec de la création de la session',
'watchTogether.failedToJoin' => 'Échec de la connexion à la session', 'watchTogether.failedToJoin' => 'Échec de la connexion à la session',
'watchTogether.sessionCodeCopied' => 'Code de session copié dans le presse-papiers', 'watchTogether.sessionCodeCopied' => 'Code de session copié dans le presse-papiers',
_ => null,
} ?? switch (path) {
'watchTogether.relayUnreachable' => 'Serveur relais inaccessible. Un blocage par le fournisseur daccès peut empêcher le fonctionnement de Regarder ensemble.', 'watchTogether.relayUnreachable' => 'Serveur relais inaccessible. Un blocage par le fournisseur daccès peut empêcher le fonctionnement de Regarder ensemble.',
'watchTogether.reconnectingToHost' => 'Reconnexion à l\'hôte...', 'watchTogether.reconnectingToHost' => 'Reconnexion à l\'hôte...',
'watchTogether.currentPlayback' => 'Lecture en cours', 'watchTogether.currentPlayback' => 'Lecture en cours',
_ => null,
} ?? switch (path) {
'watchTogether.joinCurrentPlayback' => 'Rejoindre la lecture en cours', 'watchTogether.joinCurrentPlayback' => 'Rejoindre la lecture en cours',
'watchTogether.joinCurrentPlaybackDescription' => 'Reprendre le contenu que lhôte regarde actuellement', 'watchTogether.joinCurrentPlaybackDescription' => 'Reprendre le contenu que lhôte regarde actuellement',
'watchTogether.failedToOpenCurrentPlayback' => 'Impossible d\'ouvrir la lecture en cours', 'watchTogether.failedToOpenCurrentPlayback' => 'Impossible d\'ouvrir la lecture en cours',
@@ -3656,12 +3643,9 @@ extension on TranslationsFr {
'services.libraryFilter.modeHintWhitelist' => 'Synchroniser uniquement les bibliothèques cochées ci-dessous.', 'services.libraryFilter.modeHintWhitelist' => 'Synchroniser uniquement les bibliothèques cochées ci-dessous.',
'services.libraryFilter.libraries' => 'Bibliothèques', 'services.libraryFilter.libraries' => 'Bibliothèques',
'services.libraryFilter.noLibraries' => 'Aucune bibliothèque disponible', 'services.libraryFilter.noLibraries' => 'Aucune bibliothèque disponible',
'addServer.addJellyfinTitle' => 'Ajouter un serveur Jellyfin',
'addServer.serverUrls' => 'URL du serveur', 'addServer.serverUrls' => 'URL du serveur',
'addServer.serverUrlsHelper' => 'Plusieurs URL possibles, séparées par des virgules.', 'addServer.serverUrlsHelper' => 'Plusieurs URL possibles, séparées par des virgules.',
'addServer.findServer' => 'Rechercher un serveur', 'addServer.findServer' => 'Rechercher un serveur',
'addServer.searchingLocalServers' => 'Recherche de serveurs Jellyfin locaux...',
'addServer.localServers' => 'Serveurs Jellyfin locaux',
'addServer.username' => 'Nom d\'utilisateur', 'addServer.username' => 'Nom d\'utilisateur',
'addServer.password' => 'Mot de passe', 'addServer.password' => 'Mot de passe',
'addServer.signIn' => 'Se connecter', 'addServer.signIn' => 'Se connecter',
@@ -3673,15 +3657,11 @@ extension on TranslationsFr {
'addServer.addPlexTitle' => 'Se connecter avec Plex', 'addServer.addPlexTitle' => 'Se connecter avec Plex',
'addServer.pinExpired' => 'Le PIN a expiré avant la connexion. Veuillez réessayer.', 'addServer.pinExpired' => 'Le PIN a expiré avant la connexion. Veuillez réessayer.',
'addServer.failedToRegisterAccount' => ({required Object error}) => 'Échec de l\'enregistrement du compte : ${error}', 'addServer.failedToRegisterAccount' => ({required Object error}) => 'Échec de l\'enregistrement du compte : ${error}',
'addServer.enterJellyfinUrlError' => 'Saisissez l\'URL de votre serveur Jellyfin',
'addServer.addConnectionTitle' => 'Ajouter une connexion', 'addServer.addConnectionTitle' => 'Ajouter une connexion',
'addServer.addConnectionTitleScoped' => ({required Object name}) => 'Ajouter à ${name}', 'addServer.addConnectionTitleScoped' => ({required Object name}) => 'Ajouter à ${name}',
'addServer.signInWithPlexCard' => 'Se connecter avec Plex', 'addServer.signInWithPlexCard' => 'Se connecter avec Plex',
'addServer.signInWithPlexCardSubtitle' => 'Autorisez cet appareil. Les serveurs partagés sont ajoutés.', 'addServer.signInWithPlexCardSubtitle' => 'Autorisez cet appareil. Les serveurs partagés sont ajoutés.',
'addServer.signInWithPlexCardSubtitleScoped' => 'Autorisez un compte Plex. Les utilisateurs Home deviennent des profils.', 'addServer.signInWithPlexCardSubtitleScoped' => 'Autorisez un compte Plex. Les utilisateurs Home deviennent des profils.',
'addServer.connectToJellyfinCard' => 'Se connecter à Jellyfin',
'addServer.connectToJellyfinCardSubtitle' => 'Saisissez l\'URL du serveur, le nom d\'utilisateur et le mot de passe.',
'addServer.connectToJellyfinCardSubtitleScoped' => ({required Object name}) => 'Connectez-vous à un serveur Jellyfin. Cette connexion sera liée à ${name}.',
'addServer.borrowFromAnotherProfile' => 'Emprunter à un autre profil', 'addServer.borrowFromAnotherProfile' => 'Emprunter à un autre profil',
'addServer.borrowFromAnotherProfileSubtitle' => 'Réutiliser la connexion d\'un autre profil. Les profils protégés par PIN exigent un PIN.', 'addServer.borrowFromAnotherProfileSubtitle' => 'Réutiliser la connexion d\'un autre profil. Les profils protégés par PIN exigent un PIN.',
_ => null, _ => null,
+3 -23
View File
@@ -115,7 +115,6 @@ class _Translations$auth$hu extends Translations$auth$en {
@override String get waitingForAuth => 'Várakozás a hitelesítésre...\nJelentkezz be a böngésződben.'; @override String get waitingForAuth => 'Várakozás a hitelesítésre...\nJelentkezz be a böngésződben.';
@override String get useBrowser => 'Böngésző használata'; @override String get useBrowser => 'Böngésző használata';
@override String get or => 'vagy'; @override String get or => 'vagy';
@override String get connectToJellyfin => 'Csatlakozás Jellyfinhez';
@override String get useQuickConnect => 'Quick Connect használata'; @override String get useQuickConnect => 'Quick Connect használata';
@override String get quickConnectInstructions => 'Nyisd meg a Quick Connect-et a Jellyfinben, és add meg ezt a kódot.'; @override String get quickConnectInstructions => 'Nyisd meg a Quick Connect-et a Jellyfinben, és add meg ezt a kódot.';
@override String get quickConnectWaiting => 'Várakozás a jóváhagyásra…'; @override String get quickConnectWaiting => 'Várakozás a jóváhagyásra…';
@@ -918,8 +917,6 @@ class _Translations$connections$hu extends Translations$connections$en {
@override String sessionExpiredOne({required Object name}) => 'A(z) ${name} munkamenete lejárt'; @override String sessionExpiredOne({required Object name}) => 'A(z) ${name} munkamenete lejárt';
@override String sessionExpiredMany({required Object count}) => '${count} szerver munkamenete lejárt'; @override String sessionExpiredMany({required Object count}) => '${count} szerver munkamenete lejárt';
@override String get signInAgain => 'Bejelentkezés újra'; @override String get signInAgain => 'Bejelentkezés újra';
@override String get editJellyfinTitle => 'Jellyfin kapcsolat szerkesztése';
@override String editJellyfinIntro({required Object serverName}) => 'URL-ek hozzáadása vagy eltávolítása ehhez: ${serverName}. A Plezy a legalacsonyabb késleltetésű, elérhető URL-t fogja használni.';
} }
// Path: discover // Path: discover
@@ -1803,12 +1800,9 @@ class _Translations$addServer$hu extends Translations$addServer$en {
final TranslationsHu _root; // ignore: unused_field final TranslationsHu _root; // ignore: unused_field
// Translations // Translations
@override String get addJellyfinTitle => 'Jellyfin szerver hozzáadása';
@override String get serverUrls => 'Szerver URL-címei'; @override String get serverUrls => 'Szerver URL-címei';
@override String get serverUrlsHelper => 'Több URL is megadható, vesszővel elválasztva.'; @override String get serverUrlsHelper => 'Több URL is megadható, vesszővel elválasztva.';
@override String get findServer => 'Szerver keresése'; @override String get findServer => 'Szerver keresése';
@override String get searchingLocalServers => 'Helyi Jellyfin-szerverek keresése...';
@override String get localServers => 'Helyi Jellyfin-szerverek';
@override String get username => 'Felhasználónév'; @override String get username => 'Felhasználónév';
@override String get password => 'Jelszó'; @override String get password => 'Jelszó';
@override String get signIn => 'Bejelentkezés'; @override String get signIn => 'Bejelentkezés';
@@ -1820,15 +1814,11 @@ class _Translations$addServer$hu extends Translations$addServer$en {
@override String get addPlexTitle => 'Bejelentkezés Plexszel'; @override String get addPlexTitle => 'Bejelentkezés Plexszel';
@override String get pinExpired => 'A PIN-kód a bejelentkezés előtt lejárt. Próbáld újra.'; @override String get pinExpired => 'A PIN-kód a bejelentkezés előtt lejárt. Próbáld újra.';
@override String failedToRegisterAccount({required Object error}) => 'Nem sikerült a fiók regisztrálása: ${error}'; @override String failedToRegisterAccount({required Object error}) => 'Nem sikerült a fiók regisztrálása: ${error}';
@override String get enterJellyfinUrlError => 'Add meg a Jellyfin szervered URL-jét';
@override String get addConnectionTitle => 'Kapcsolat hozzáadása'; @override String get addConnectionTitle => 'Kapcsolat hozzáadása';
@override String addConnectionTitleScoped({required Object name}) => 'Hozzáadás a következőhöz: ${name}'; @override String addConnectionTitleScoped({required Object name}) => 'Hozzáadás a következőhöz: ${name}';
@override String get signInWithPlexCard => 'Bejelentkezés Plexszel'; @override String get signInWithPlexCard => 'Bejelentkezés Plexszel';
@override String get signInWithPlexCardSubtitle => 'Eszköz engedélyezése. A megosztott szerverek hozzáadásra kerülnek.'; @override String get signInWithPlexCardSubtitle => 'Eszköz engedélyezése. A megosztott szerverek hozzáadásra kerülnek.';
@override String get signInWithPlexCardSubtitleScoped => 'Plex-fiók engedélyezése. A Plex Home-felhasználókból profilok lesznek.'; @override String get signInWithPlexCardSubtitleScoped => 'Plex-fiók engedélyezése. A Plex Home-felhasználókból profilok lesznek.';
@override String get connectToJellyfinCard => 'Csatlakozás Jellyfinhez';
@override String get connectToJellyfinCardSubtitle => 'Add meg a szerver URL-jét, felhasználónevedet és jelszavadat.';
@override String connectToJellyfinCardSubtitleScoped({required Object name}) => 'Bejelentkezés egy Jellyfin-szerverre. Hozzárendelés ehhez: ${name}.';
@override String get borrowFromAnotherProfile => 'Kapcsolat használata másik profilból'; @override String get borrowFromAnotherProfile => 'Kapcsolat használata másik profilból';
@override String get borrowFromAnotherProfileSubtitle => 'Egy másik profil kapcsolatának használata. A PIN-kóddal védett profilokhoz PIN-kód szükséges.'; @override String get borrowFromAnotherProfileSubtitle => 'Egy másik profil kapcsolatának használata. A PIN-kóddal védett profilokhoz PIN-kód szükséges.';
} }
@@ -2208,7 +2198,6 @@ extension on TranslationsHu {
'auth.waitingForAuth' => 'Várakozás a hitelesítésre...\nJelentkezz be a böngésződben.', 'auth.waitingForAuth' => 'Várakozás a hitelesítésre...\nJelentkezz be a böngésződben.',
'auth.useBrowser' => 'Böngésző használata', 'auth.useBrowser' => 'Böngésző használata',
'auth.or' => 'vagy', 'auth.or' => 'vagy',
'auth.connectToJellyfin' => 'Csatlakozás Jellyfinhez',
'auth.useQuickConnect' => 'Quick Connect használata', 'auth.useQuickConnect' => 'Quick Connect használata',
'auth.quickConnectInstructions' => 'Nyisd meg a Quick Connect-et a Jellyfinben, és add meg ezt a kódot.', 'auth.quickConnectInstructions' => 'Nyisd meg a Quick Connect-et a Jellyfinben, és add meg ezt a kódot.',
'auth.quickConnectWaiting' => 'Várakozás a jóváhagyásra…', 'auth.quickConnectWaiting' => 'Várakozás a jóváhagyásra…',
@@ -2711,9 +2700,9 @@ extension on TranslationsHu {
'videoControls.searchSubtitles' => 'Feliratok keresése', 'videoControls.searchSubtitles' => 'Feliratok keresése',
'videoControls.language' => 'Nyelv', 'videoControls.language' => 'Nyelv',
'videoControls.noSubtitlesFound' => 'Nem találhatók feliratok', 'videoControls.noSubtitlesFound' => 'Nem találhatók feliratok',
'videoControls.subtitleDownloaded' => 'Felirat letöltve',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'videoControls.subtitleDownloaded' => 'Felirat letöltve',
'videoControls.subtitleDownloadedNotApplied' => 'Felirat letöltve, de nem sikerült kiválasztani', 'videoControls.subtitleDownloadedNotApplied' => 'Felirat letöltve, de nem sikerült kiválasztani',
'videoControls.subtitleDownloadFailed' => 'Nem sikerült a felirat letöltése', 'videoControls.subtitleDownloadFailed' => 'Nem sikerült a felirat letöltése',
'videoControls.searchLanguages' => 'Nyelvek keresése...', 'videoControls.searchLanguages' => 'Nyelvek keresése...',
@@ -2869,8 +2858,6 @@ extension on TranslationsHu {
'connections.sessionExpiredOne' => ({required Object name}) => 'A(z) ${name} munkamenete lejárt', 'connections.sessionExpiredOne' => ({required Object name}) => 'A(z) ${name} munkamenete lejárt',
'connections.sessionExpiredMany' => ({required Object count}) => '${count} szerver munkamenete lejárt', 'connections.sessionExpiredMany' => ({required Object count}) => '${count} szerver munkamenete lejárt',
'connections.signInAgain' => 'Bejelentkezés újra', 'connections.signInAgain' => 'Bejelentkezés újra',
'connections.editJellyfinTitle' => 'Jellyfin kapcsolat szerkesztése',
'connections.editJellyfinIntro' => ({required Object serverName}) => 'URL-ek hozzáadása vagy eltávolítása ehhez: ${serverName}. A Plezy a legalacsonyabb késleltetésű, elérhető URL-t fogja használni.',
'discover.title' => 'Felfedezés', 'discover.title' => 'Felfedezés',
'discover.noContentAvailable' => 'Nincs elérhető tartalom', 'discover.noContentAvailable' => 'Nincs elérhető tartalom',
'discover.addMediaToLibraries' => 'Adj hozzá médiát a könyvtáraidhoz', 'discover.addMediaToLibraries' => 'Adj hozzá médiát a könyvtáraidhoz',
@@ -3225,11 +3212,11 @@ extension on TranslationsHu {
'watchTogether.failedToCreate' => 'Nem sikerült a munkamenet létrehozása', 'watchTogether.failedToCreate' => 'Nem sikerült a munkamenet létrehozása',
'watchTogether.failedToJoin' => 'Nem sikerült csatlakozni a munkamenethez', 'watchTogether.failedToJoin' => 'Nem sikerült csatlakozni a munkamenethez',
'watchTogether.sessionCodeCopied' => 'A munkamenetkód a vágólapra másolva', 'watchTogether.sessionCodeCopied' => 'A munkamenetkód a vágólapra másolva',
_ => null,
} ?? switch (path) {
'watchTogether.relayUnreachable' => 'A relészerver nem érhető el. Az internetszolgáltató blokkolása megakadályozhatja a közös nézést.', 'watchTogether.relayUnreachable' => 'A relészerver nem érhető el. Az internetszolgáltató blokkolása megakadályozhatja a közös nézést.',
'watchTogether.reconnectingToHost' => 'Újracsatlakozás a házigazdához...', 'watchTogether.reconnectingToHost' => 'Újracsatlakozás a házigazdához...',
'watchTogether.currentPlayback' => 'Jelenlegi lejátszás', 'watchTogether.currentPlayback' => 'Jelenlegi lejátszás',
_ => null,
} ?? switch (path) {
'watchTogether.joinCurrentPlayback' => 'Csatlakozás a jelenlegi lejátszáshoz', 'watchTogether.joinCurrentPlayback' => 'Csatlakozás a jelenlegi lejátszáshoz',
'watchTogether.joinCurrentPlaybackDescription' => 'Visszatérés ahhoz, amit a házigazda éppen néz', 'watchTogether.joinCurrentPlaybackDescription' => 'Visszatérés ahhoz, amit a házigazda éppen néz',
'watchTogether.failedToOpenCurrentPlayback' => 'Nem sikerült megnyitni a jelenlegi lejátszást', 'watchTogether.failedToOpenCurrentPlayback' => 'Nem sikerült megnyitni a jelenlegi lejátszást',
@@ -3656,12 +3643,9 @@ extension on TranslationsHu {
'services.libraryFilter.modeHintWhitelist' => 'Csak az alább bejelölt könyvtárak szinkronizálása.', 'services.libraryFilter.modeHintWhitelist' => 'Csak az alább bejelölt könyvtárak szinkronizálása.',
'services.libraryFilter.libraries' => 'Könyvtárak', 'services.libraryFilter.libraries' => 'Könyvtárak',
'services.libraryFilter.noLibraries' => 'Nincsenek elérhető könyvtárak', 'services.libraryFilter.noLibraries' => 'Nincsenek elérhető könyvtárak',
'addServer.addJellyfinTitle' => 'Jellyfin szerver hozzáadása',
'addServer.serverUrls' => 'Szerver URL-címei', 'addServer.serverUrls' => 'Szerver URL-címei',
'addServer.serverUrlsHelper' => 'Több URL is megadható, vesszővel elválasztva.', 'addServer.serverUrlsHelper' => 'Több URL is megadható, vesszővel elválasztva.',
'addServer.findServer' => 'Szerver keresése', 'addServer.findServer' => 'Szerver keresése',
'addServer.searchingLocalServers' => 'Helyi Jellyfin-szerverek keresése...',
'addServer.localServers' => 'Helyi Jellyfin-szerverek',
'addServer.username' => 'Felhasználónév', 'addServer.username' => 'Felhasználónév',
'addServer.password' => 'Jelszó', 'addServer.password' => 'Jelszó',
'addServer.signIn' => 'Bejelentkezés', 'addServer.signIn' => 'Bejelentkezés',
@@ -3673,15 +3657,11 @@ extension on TranslationsHu {
'addServer.addPlexTitle' => 'Bejelentkezés Plexszel', 'addServer.addPlexTitle' => 'Bejelentkezés Plexszel',
'addServer.pinExpired' => 'A PIN-kód a bejelentkezés előtt lejárt. Próbáld újra.', 'addServer.pinExpired' => 'A PIN-kód a bejelentkezés előtt lejárt. Próbáld újra.',
'addServer.failedToRegisterAccount' => ({required Object error}) => 'Nem sikerült a fiók regisztrálása: ${error}', 'addServer.failedToRegisterAccount' => ({required Object error}) => 'Nem sikerült a fiók regisztrálása: ${error}',
'addServer.enterJellyfinUrlError' => 'Add meg a Jellyfin szervered URL-jét',
'addServer.addConnectionTitle' => 'Kapcsolat hozzáadása', 'addServer.addConnectionTitle' => 'Kapcsolat hozzáadása',
'addServer.addConnectionTitleScoped' => ({required Object name}) => 'Hozzáadás a következőhöz: ${name}', 'addServer.addConnectionTitleScoped' => ({required Object name}) => 'Hozzáadás a következőhöz: ${name}',
'addServer.signInWithPlexCard' => 'Bejelentkezés Plexszel', 'addServer.signInWithPlexCard' => 'Bejelentkezés Plexszel',
'addServer.signInWithPlexCardSubtitle' => 'Eszköz engedélyezése. A megosztott szerverek hozzáadásra kerülnek.', 'addServer.signInWithPlexCardSubtitle' => 'Eszköz engedélyezése. A megosztott szerverek hozzáadásra kerülnek.',
'addServer.signInWithPlexCardSubtitleScoped' => 'Plex-fiók engedélyezése. A Plex Home-felhasználókból profilok lesznek.', 'addServer.signInWithPlexCardSubtitleScoped' => 'Plex-fiók engedélyezése. A Plex Home-felhasználókból profilok lesznek.',
'addServer.connectToJellyfinCard' => 'Csatlakozás Jellyfinhez',
'addServer.connectToJellyfinCardSubtitle' => 'Add meg a szerver URL-jét, felhasználónevedet és jelszavadat.',
'addServer.connectToJellyfinCardSubtitleScoped' => ({required Object name}) => 'Bejelentkezés egy Jellyfin-szerverre. Hozzárendelés ehhez: ${name}.',
'addServer.borrowFromAnotherProfile' => 'Kapcsolat használata másik profilból', 'addServer.borrowFromAnotherProfile' => 'Kapcsolat használata másik profilból',
'addServer.borrowFromAnotherProfileSubtitle' => 'Egy másik profil kapcsolatának használata. A PIN-kóddal védett profilokhoz PIN-kód szükséges.', 'addServer.borrowFromAnotherProfileSubtitle' => 'Egy másik profil kapcsolatának használata. A PIN-kóddal védett profilokhoz PIN-kód szükséges.',
_ => null, _ => null,
+3 -23
View File
@@ -115,7 +115,6 @@ class _Translations$auth$it extends Translations$auth$en {
@override String get waitingForAuth => 'In attesa di autenticazione...\nAccedi dal browser.'; @override String get waitingForAuth => 'In attesa di autenticazione...\nAccedi dal browser.';
@override String get useBrowser => 'Usa il browser'; @override String get useBrowser => 'Usa il browser';
@override String get or => 'o'; @override String get or => 'o';
@override String get connectToJellyfin => 'Connettiti a Jellyfin';
@override String get useQuickConnect => 'Usa Quick Connect'; @override String get useQuickConnect => 'Usa Quick Connect';
@override String get quickConnectInstructions => 'Apri Quick Connect in Jellyfin e inserisci questo codice.'; @override String get quickConnectInstructions => 'Apri Quick Connect in Jellyfin e inserisci questo codice.';
@override String get quickConnectWaiting => 'In attesa di approvazione…'; @override String get quickConnectWaiting => 'In attesa di approvazione…';
@@ -918,8 +917,6 @@ class _Translations$connections$it extends Translations$connections$en {
@override String sessionExpiredOne({required Object name}) => 'Sessione scaduta per ${name}'; @override String sessionExpiredOne({required Object name}) => 'Sessione scaduta per ${name}';
@override String sessionExpiredMany({required Object count}) => 'Sessione scaduta per ${count} server'; @override String sessionExpiredMany({required Object count}) => 'Sessione scaduta per ${count} server';
@override String get signInAgain => 'Accedi di nuovo'; @override String get signInAgain => 'Accedi di nuovo';
@override String get editJellyfinTitle => 'Modifica connessione Jellyfin';
@override String editJellyfinIntro({required Object serverName}) => 'Aggiungi o rimuovi URL per ${serverName}. Plezy userà l\'URL raggiungibile con la latenza più bassa.';
} }
// Path: discover // Path: discover
@@ -1803,12 +1800,9 @@ class _Translations$addServer$it extends Translations$addServer$en {
final TranslationsIt _root; // ignore: unused_field final TranslationsIt _root; // ignore: unused_field
// Translations // Translations
@override String get addJellyfinTitle => 'Aggiungi server Jellyfin';
@override String get serverUrls => 'URL del server'; @override String get serverUrls => 'URL del server';
@override String get serverUrlsHelper => 'Sono consentiti più URL, separati da virgole.'; @override String get serverUrlsHelper => 'Sono consentiti più URL, separati da virgole.';
@override String get findServer => 'Trova il server'; @override String get findServer => 'Trova il server';
@override String get searchingLocalServers => 'Ricerca dei server Jellyfin locali...';
@override String get localServers => 'Server Jellyfin locali';
@override String get username => 'Nome utente'; @override String get username => 'Nome utente';
@override String get password => 'Password'; @override String get password => 'Password';
@override String get signIn => 'Accedi'; @override String get signIn => 'Accedi';
@@ -1820,15 +1814,11 @@ class _Translations$addServer$it extends Translations$addServer$en {
@override String get addPlexTitle => 'Accedi con Plex'; @override String get addPlexTitle => 'Accedi con Plex';
@override String get pinExpired => 'PIN scaduto prima dell\'accesso. Riprova.'; @override String get pinExpired => 'PIN scaduto prima dell\'accesso. Riprova.';
@override String failedToRegisterAccount({required Object error}) => 'Registrazione account non riuscita: ${error}'; @override String failedToRegisterAccount({required Object error}) => 'Registrazione account non riuscita: ${error}';
@override String get enterJellyfinUrlError => 'Inserisci l\'URL del tuo server Jellyfin';
@override String get addConnectionTitle => 'Aggiungi connessione'; @override String get addConnectionTitle => 'Aggiungi connessione';
@override String addConnectionTitleScoped({required Object name}) => 'Aggiungi a ${name}'; @override String addConnectionTitleScoped({required Object name}) => 'Aggiungi a ${name}';
@override String get signInWithPlexCard => 'Accedi con Plex'; @override String get signInWithPlexCard => 'Accedi con Plex';
@override String get signInWithPlexCardSubtitle => 'Autorizza questo dispositivo. I server condivisi vengono aggiunti.'; @override String get signInWithPlexCardSubtitle => 'Autorizza questo dispositivo. I server condivisi vengono aggiunti.';
@override String get signInWithPlexCardSubtitleScoped => 'Autorizza un account Plex. Gli utenti Home diventano profili.'; @override String get signInWithPlexCardSubtitleScoped => 'Autorizza un account Plex. Gli utenti Home diventano profili.';
@override String get connectToJellyfinCard => 'Connettiti a Jellyfin';
@override String get connectToJellyfinCardSubtitle => 'Inserisci l\'URL del server, il nome utente e la password.';
@override String connectToJellyfinCardSubtitleScoped({required Object name}) => 'Accedi a un server Jellyfin. Verrà associato a ${name}.';
@override String get borrowFromAnotherProfile => 'Prendi in prestito da un altro profilo'; @override String get borrowFromAnotherProfile => 'Prendi in prestito da un altro profilo';
@override String get borrowFromAnotherProfileSubtitle => 'Riutilizza la connessione di un altro profilo. I profili protetti da PIN richiedono un PIN.'; @override String get borrowFromAnotherProfileSubtitle => 'Riutilizza la connessione di un altro profilo. I profili protetti da PIN richiedono un PIN.';
} }
@@ -2208,7 +2198,6 @@ extension on TranslationsIt {
'auth.waitingForAuth' => 'In attesa di autenticazione...\nAccedi dal browser.', 'auth.waitingForAuth' => 'In attesa di autenticazione...\nAccedi dal browser.',
'auth.useBrowser' => 'Usa il browser', 'auth.useBrowser' => 'Usa il browser',
'auth.or' => 'o', 'auth.or' => 'o',
'auth.connectToJellyfin' => 'Connettiti a Jellyfin',
'auth.useQuickConnect' => 'Usa Quick Connect', 'auth.useQuickConnect' => 'Usa Quick Connect',
'auth.quickConnectInstructions' => 'Apri Quick Connect in Jellyfin e inserisci questo codice.', 'auth.quickConnectInstructions' => 'Apri Quick Connect in Jellyfin e inserisci questo codice.',
'auth.quickConnectWaiting' => 'In attesa di approvazione…', 'auth.quickConnectWaiting' => 'In attesa di approvazione…',
@@ -2711,9 +2700,9 @@ extension on TranslationsIt {
'videoControls.searchSubtitles' => 'Cerca sottotitoli', 'videoControls.searchSubtitles' => 'Cerca sottotitoli',
'videoControls.language' => 'Lingua', 'videoControls.language' => 'Lingua',
'videoControls.noSubtitlesFound' => 'Nessun sottotitolo trovato', 'videoControls.noSubtitlesFound' => 'Nessun sottotitolo trovato',
'videoControls.subtitleDownloaded' => 'Sottotitolo scaricato',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'videoControls.subtitleDownloaded' => 'Sottotitolo scaricato',
'videoControls.subtitleDownloadedNotApplied' => 'Il sottotitolo è stato scaricato, ma non è stato possibile selezionarlo', 'videoControls.subtitleDownloadedNotApplied' => 'Il sottotitolo è stato scaricato, ma non è stato possibile selezionarlo',
'videoControls.subtitleDownloadFailed' => 'Impossibile scaricare il sottotitolo', 'videoControls.subtitleDownloadFailed' => 'Impossibile scaricare il sottotitolo',
'videoControls.searchLanguages' => 'Cerca lingue...', 'videoControls.searchLanguages' => 'Cerca lingue...',
@@ -2869,8 +2858,6 @@ extension on TranslationsIt {
'connections.sessionExpiredOne' => ({required Object name}) => 'Sessione scaduta per ${name}', 'connections.sessionExpiredOne' => ({required Object name}) => 'Sessione scaduta per ${name}',
'connections.sessionExpiredMany' => ({required Object count}) => 'Sessione scaduta per ${count} server', 'connections.sessionExpiredMany' => ({required Object count}) => 'Sessione scaduta per ${count} server',
'connections.signInAgain' => 'Accedi di nuovo', 'connections.signInAgain' => 'Accedi di nuovo',
'connections.editJellyfinTitle' => 'Modifica connessione Jellyfin',
'connections.editJellyfinIntro' => ({required Object serverName}) => 'Aggiungi o rimuovi URL per ${serverName}. Plezy userà l\'URL raggiungibile con la latenza più bassa.',
'discover.title' => 'Esplora', 'discover.title' => 'Esplora',
'discover.noContentAvailable' => 'Nessun contenuto disponibile', 'discover.noContentAvailable' => 'Nessun contenuto disponibile',
'discover.addMediaToLibraries' => 'Aggiungi contenuti multimediali alle tue librerie', 'discover.addMediaToLibraries' => 'Aggiungi contenuti multimediali alle tue librerie',
@@ -3225,11 +3212,11 @@ extension on TranslationsIt {
'watchTogether.failedToCreate' => 'Impossibile creare la sessione', 'watchTogether.failedToCreate' => 'Impossibile creare la sessione',
'watchTogether.failedToJoin' => 'Impossibile unirsi alla sessione', 'watchTogether.failedToJoin' => 'Impossibile unirsi alla sessione',
'watchTogether.sessionCodeCopied' => 'Codice della sessione copiato negli appunti', 'watchTogether.sessionCodeCopied' => 'Codice della sessione copiato negli appunti',
_ => null,
} ?? switch (path) {
'watchTogether.relayUnreachable' => 'Il server relay non è raggiungibile. Eventuali blocchi dell\'ISP potrebbero impedire l\'uso di Guarda insieme.', 'watchTogether.relayUnreachable' => 'Il server relay non è raggiungibile. Eventuali blocchi dell\'ISP potrebbero impedire l\'uso di Guarda insieme.',
'watchTogether.reconnectingToHost' => 'Riconnessione all\'host...', 'watchTogether.reconnectingToHost' => 'Riconnessione all\'host...',
'watchTogether.currentPlayback' => 'Riproduzione corrente', 'watchTogether.currentPlayback' => 'Riproduzione corrente',
_ => null,
} ?? switch (path) {
'watchTogether.joinCurrentPlayback' => 'Unisciti alla riproduzione corrente', 'watchTogether.joinCurrentPlayback' => 'Unisciti alla riproduzione corrente',
'watchTogether.joinCurrentPlaybackDescription' => 'Torna a ciò che l\'host sta guardando in questo momento', 'watchTogether.joinCurrentPlaybackDescription' => 'Torna a ciò che l\'host sta guardando in questo momento',
'watchTogether.failedToOpenCurrentPlayback' => 'Impossibile aprire la riproduzione corrente', 'watchTogether.failedToOpenCurrentPlayback' => 'Impossibile aprire la riproduzione corrente',
@@ -3656,12 +3643,9 @@ extension on TranslationsIt {
'services.libraryFilter.modeHintWhitelist' => 'Sincronizza solo le librerie selezionate qui sotto.', 'services.libraryFilter.modeHintWhitelist' => 'Sincronizza solo le librerie selezionate qui sotto.',
'services.libraryFilter.libraries' => 'Librerie', 'services.libraryFilter.libraries' => 'Librerie',
'services.libraryFilter.noLibraries' => 'Nessuna libreria disponibile', 'services.libraryFilter.noLibraries' => 'Nessuna libreria disponibile',
'addServer.addJellyfinTitle' => 'Aggiungi server Jellyfin',
'addServer.serverUrls' => 'URL del server', 'addServer.serverUrls' => 'URL del server',
'addServer.serverUrlsHelper' => 'Sono consentiti più URL, separati da virgole.', 'addServer.serverUrlsHelper' => 'Sono consentiti più URL, separati da virgole.',
'addServer.findServer' => 'Trova il server', 'addServer.findServer' => 'Trova il server',
'addServer.searchingLocalServers' => 'Ricerca dei server Jellyfin locali...',
'addServer.localServers' => 'Server Jellyfin locali',
'addServer.username' => 'Nome utente', 'addServer.username' => 'Nome utente',
'addServer.password' => 'Password', 'addServer.password' => 'Password',
'addServer.signIn' => 'Accedi', 'addServer.signIn' => 'Accedi',
@@ -3673,15 +3657,11 @@ extension on TranslationsIt {
'addServer.addPlexTitle' => 'Accedi con Plex', 'addServer.addPlexTitle' => 'Accedi con Plex',
'addServer.pinExpired' => 'PIN scaduto prima dell\'accesso. Riprova.', 'addServer.pinExpired' => 'PIN scaduto prima dell\'accesso. Riprova.',
'addServer.failedToRegisterAccount' => ({required Object error}) => 'Registrazione account non riuscita: ${error}', 'addServer.failedToRegisterAccount' => ({required Object error}) => 'Registrazione account non riuscita: ${error}',
'addServer.enterJellyfinUrlError' => 'Inserisci l\'URL del tuo server Jellyfin',
'addServer.addConnectionTitle' => 'Aggiungi connessione', 'addServer.addConnectionTitle' => 'Aggiungi connessione',
'addServer.addConnectionTitleScoped' => ({required Object name}) => 'Aggiungi a ${name}', 'addServer.addConnectionTitleScoped' => ({required Object name}) => 'Aggiungi a ${name}',
'addServer.signInWithPlexCard' => 'Accedi con Plex', 'addServer.signInWithPlexCard' => 'Accedi con Plex',
'addServer.signInWithPlexCardSubtitle' => 'Autorizza questo dispositivo. I server condivisi vengono aggiunti.', 'addServer.signInWithPlexCardSubtitle' => 'Autorizza questo dispositivo. I server condivisi vengono aggiunti.',
'addServer.signInWithPlexCardSubtitleScoped' => 'Autorizza un account Plex. Gli utenti Home diventano profili.', 'addServer.signInWithPlexCardSubtitleScoped' => 'Autorizza un account Plex. Gli utenti Home diventano profili.',
'addServer.connectToJellyfinCard' => 'Connettiti a Jellyfin',
'addServer.connectToJellyfinCardSubtitle' => 'Inserisci l\'URL del server, il nome utente e la password.',
'addServer.connectToJellyfinCardSubtitleScoped' => ({required Object name}) => 'Accedi a un server Jellyfin. Verrà associato a ${name}.',
'addServer.borrowFromAnotherProfile' => 'Prendi in prestito da un altro profilo', 'addServer.borrowFromAnotherProfile' => 'Prendi in prestito da un altro profilo',
'addServer.borrowFromAnotherProfileSubtitle' => 'Riutilizza la connessione di un altro profilo. I profili protetti da PIN richiedono un PIN.', 'addServer.borrowFromAnotherProfileSubtitle' => 'Riutilizza la connessione di un altro profilo. I profili protetti da PIN richiedono un PIN.',
_ => null, _ => null,
+3 -23
View File
@@ -115,7 +115,6 @@ class _Translations$auth$ja extends Translations$auth$en {
@override String get waitingForAuth => '認証を待っています…\nブラウザでサインインしてください。'; @override String get waitingForAuth => '認証を待っています…\nブラウザでサインインしてください。';
@override String get useBrowser => 'ブラウザを使用'; @override String get useBrowser => 'ブラウザを使用';
@override String get or => 'または'; @override String get or => 'または';
@override String get connectToJellyfin => 'Jellyfinに接続';
@override String get useQuickConnect => 'Quick Connect を使う'; @override String get useQuickConnect => 'Quick Connect を使う';
@override String get quickConnectInstructions => 'JellyfinでQuick Connectを開き、このコードを入力してください。'; @override String get quickConnectInstructions => 'JellyfinでQuick Connectを開き、このコードを入力してください。';
@override String get quickConnectWaiting => '承認を待っています…'; @override String get quickConnectWaiting => '承認を待っています…';
@@ -917,8 +916,6 @@ class _Translations$connections$ja extends Translations$connections$en {
@override String sessionExpiredOne({required Object name}) => '${name} のセッションの有効期限が切れました'; @override String sessionExpiredOne({required Object name}) => '${name} のセッションの有効期限が切れました';
@override String sessionExpiredMany({required Object count}) => '${count} 台のサーバーのセッションの有効期限が切れました'; @override String sessionExpiredMany({required Object count}) => '${count} 台のサーバーのセッションの有効期限が切れました';
@override String get signInAgain => '再度サインイン'; @override String get signInAgain => '再度サインイン';
@override String get editJellyfinTitle => 'Jellyfin接続を編集';
@override String editJellyfinIntro({required Object serverName}) => '${serverName}のURLを追加または削除します。Plezyは接続可能なURLのうち遅延が最も少ないものを使用します。';
} }
// Path: discover // Path: discover
@@ -1800,12 +1797,9 @@ class _Translations$addServer$ja extends Translations$addServer$en {
final TranslationsJa _root; // ignore: unused_field final TranslationsJa _root; // ignore: unused_field
// Translations // Translations
@override String get addJellyfinTitle => 'Jellyfinサーバーを追加';
@override String get serverUrls => 'サーバーURL'; @override String get serverUrls => 'サーバーURL';
@override String get serverUrlsHelper => '複数のURLをカンマ区切りで入力できます。'; @override String get serverUrlsHelper => '複数のURLをカンマ区切りで入力できます。';
@override String get findServer => 'サーバーを検索'; @override String get findServer => 'サーバーを検索';
@override String get searchingLocalServers => 'ローカルのJellyfinサーバーを検索中…';
@override String get localServers => 'ローカルのJellyfinサーバー';
@override String get username => 'ユーザー名'; @override String get username => 'ユーザー名';
@override String get password => 'パスワード'; @override String get password => 'パスワード';
@override String get signIn => 'サインイン'; @override String get signIn => 'サインイン';
@@ -1817,15 +1811,11 @@ class _Translations$addServer$ja extends Translations$addServer$en {
@override String get addPlexTitle => 'Plexでサインイン'; @override String get addPlexTitle => 'Plexでサインイン';
@override String get pinExpired => 'サインイン前にPINの有効期限が切れました。もう一度お試しください。'; @override String get pinExpired => 'サインイン前にPINの有効期限が切れました。もう一度お試しください。';
@override String failedToRegisterAccount({required Object error}) => 'アカウントの登録に失敗しました: ${error}'; @override String failedToRegisterAccount({required Object error}) => 'アカウントの登録に失敗しました: ${error}';
@override String get enterJellyfinUrlError => 'JellyfinサーバーのURLを入力してください';
@override String get addConnectionTitle => '接続を追加'; @override String get addConnectionTitle => '接続を追加';
@override String addConnectionTitleScoped({required Object name}) => '${name}に追加'; @override String addConnectionTitleScoped({required Object name}) => '${name}に追加';
@override String get signInWithPlexCard => 'Plexでサインイン'; @override String get signInWithPlexCard => 'Plexでサインイン';
@override String get signInWithPlexCardSubtitle => 'このデバイスを承認します。共有サーバーが追加されます。'; @override String get signInWithPlexCardSubtitle => 'このデバイスを承認します。共有サーバーが追加されます。';
@override String get signInWithPlexCardSubtitleScoped => 'Plexアカウントを承認します。Homeユーザーはプロフィールになります。'; @override String get signInWithPlexCardSubtitleScoped => 'Plexアカウントを承認します。Homeユーザーはプロフィールになります。';
@override String get connectToJellyfinCard => 'Jellyfinに接続';
@override String get connectToJellyfinCardSubtitle => 'サーバーURL、ユーザー名、パスワードを入力してください。';
@override String connectToJellyfinCardSubtitleScoped({required Object name}) => 'Jellyfinサーバーにサインインします。${name}にひも付けられます。';
@override String get borrowFromAnotherProfile => '別のプロフィールの接続を利用'; @override String get borrowFromAnotherProfile => '別のプロフィールの接続を利用';
@override String get borrowFromAnotherProfileSubtitle => '別のプロフィールの接続を再利用します。PINで保護されたプロフィールにはPINが必要です。'; @override String get borrowFromAnotherProfileSubtitle => '別のプロフィールの接続を再利用します。PINで保護されたプロフィールにはPINが必要です。';
} }
@@ -2205,7 +2195,6 @@ extension on TranslationsJa {
'auth.waitingForAuth' => '認証を待っています…\nブラウザでサインインしてください。', 'auth.waitingForAuth' => '認証を待っています…\nブラウザでサインインしてください。',
'auth.useBrowser' => 'ブラウザを使用', 'auth.useBrowser' => 'ブラウザを使用',
'auth.or' => 'または', 'auth.or' => 'または',
'auth.connectToJellyfin' => 'Jellyfinに接続',
'auth.useQuickConnect' => 'Quick Connect を使う', 'auth.useQuickConnect' => 'Quick Connect を使う',
'auth.quickConnectInstructions' => 'JellyfinでQuick Connectを開き、このコードを入力してください。', 'auth.quickConnectInstructions' => 'JellyfinでQuick Connectを開き、このコードを入力してください。',
'auth.quickConnectWaiting' => '承認を待っています…', 'auth.quickConnectWaiting' => '承認を待っています…',
@@ -2708,9 +2697,9 @@ extension on TranslationsJa {
'videoControls.searchSubtitles' => '字幕を検索', 'videoControls.searchSubtitles' => '字幕を検索',
'videoControls.language' => '言語', 'videoControls.language' => '言語',
'videoControls.noSubtitlesFound' => '字幕が見つかりません', 'videoControls.noSubtitlesFound' => '字幕が見つかりません',
'videoControls.subtitleDownloaded' => '字幕をダウンロードしました',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'videoControls.subtitleDownloaded' => '字幕をダウンロードしました',
'videoControls.subtitleDownloadedNotApplied' => '字幕はダウンロードされましたが、選択できませんでした', 'videoControls.subtitleDownloadedNotApplied' => '字幕はダウンロードされましたが、選択できませんでした',
'videoControls.subtitleDownloadFailed' => '字幕のダウンロードに失敗しました', 'videoControls.subtitleDownloadFailed' => '字幕のダウンロードに失敗しました',
'videoControls.searchLanguages' => '言語を検索…', 'videoControls.searchLanguages' => '言語を検索…',
@@ -2866,8 +2855,6 @@ extension on TranslationsJa {
'connections.sessionExpiredOne' => ({required Object name}) => '${name} のセッションの有効期限が切れました', 'connections.sessionExpiredOne' => ({required Object name}) => '${name} のセッションの有効期限が切れました',
'connections.sessionExpiredMany' => ({required Object count}) => '${count} 台のサーバーのセッションの有効期限が切れました', 'connections.sessionExpiredMany' => ({required Object count}) => '${count} 台のサーバーのセッションの有効期限が切れました',
'connections.signInAgain' => '再度サインイン', 'connections.signInAgain' => '再度サインイン',
'connections.editJellyfinTitle' => 'Jellyfin接続を編集',
'connections.editJellyfinIntro' => ({required Object serverName}) => '${serverName}のURLを追加または削除します。Plezyは接続可能なURLのうち遅延が最も少ないものを使用します。',
'discover.title' => '探す', 'discover.title' => '探す',
'discover.noContentAvailable' => 'コンテンツがありません', 'discover.noContentAvailable' => 'コンテンツがありません',
'discover.addMediaToLibraries' => 'ライブラリにメディアを追加してください', 'discover.addMediaToLibraries' => 'ライブラリにメディアを追加してください',
@@ -3222,11 +3209,11 @@ extension on TranslationsJa {
'watchTogether.failedToCreate' => 'セッションの作成に失敗しました', 'watchTogether.failedToCreate' => 'セッションの作成に失敗しました',
'watchTogether.failedToJoin' => 'セッションへの参加に失敗しました', 'watchTogether.failedToJoin' => 'セッションへの参加に失敗しました',
'watchTogether.sessionCodeCopied' => 'セッションコードをクリップボードにコピーしました', 'watchTogether.sessionCodeCopied' => 'セッションコードをクリップボードにコピーしました',
_ => null,
} ?? switch (path) {
'watchTogether.relayUnreachable' => 'リレーサーバーに接続できません。ISPによるブロックのため「一緒に見る」を利用できない可能性があります。', 'watchTogether.relayUnreachable' => 'リレーサーバーに接続できません。ISPによるブロックのため「一緒に見る」を利用できない可能性があります。',
'watchTogether.reconnectingToHost' => 'ホストに再接続中…', 'watchTogether.reconnectingToHost' => 'ホストに再接続中…',
'watchTogether.currentPlayback' => '現在の再生', 'watchTogether.currentPlayback' => '現在の再生',
_ => null,
} ?? switch (path) {
'watchTogether.joinCurrentPlayback' => '現在の再生に参加', 'watchTogether.joinCurrentPlayback' => '現在の再生に参加',
'watchTogether.joinCurrentPlaybackDescription' => 'ホストが現在視聴中のコンテンツに戻る', 'watchTogether.joinCurrentPlaybackDescription' => 'ホストが現在視聴中のコンテンツに戻る',
'watchTogether.failedToOpenCurrentPlayback' => '現在の再生を開けませんでした', 'watchTogether.failedToOpenCurrentPlayback' => '現在の再生を開けませんでした',
@@ -3653,12 +3640,9 @@ extension on TranslationsJa {
'services.libraryFilter.modeHintWhitelist' => '下でチェックしたライブラリのみ同期します。', 'services.libraryFilter.modeHintWhitelist' => '下でチェックしたライブラリのみ同期します。',
'services.libraryFilter.libraries' => 'ライブラリ', 'services.libraryFilter.libraries' => 'ライブラリ',
'services.libraryFilter.noLibraries' => '利用できるライブラリがありません', 'services.libraryFilter.noLibraries' => '利用できるライブラリがありません',
'addServer.addJellyfinTitle' => 'Jellyfinサーバーを追加',
'addServer.serverUrls' => 'サーバーURL', 'addServer.serverUrls' => 'サーバーURL',
'addServer.serverUrlsHelper' => '複数のURLをカンマ区切りで入力できます。', 'addServer.serverUrlsHelper' => '複数のURLをカンマ区切りで入力できます。',
'addServer.findServer' => 'サーバーを検索', 'addServer.findServer' => 'サーバーを検索',
'addServer.searchingLocalServers' => 'ローカルのJellyfinサーバーを検索中…',
'addServer.localServers' => 'ローカルのJellyfinサーバー',
'addServer.username' => 'ユーザー名', 'addServer.username' => 'ユーザー名',
'addServer.password' => 'パスワード', 'addServer.password' => 'パスワード',
'addServer.signIn' => 'サインイン', 'addServer.signIn' => 'サインイン',
@@ -3670,15 +3654,11 @@ extension on TranslationsJa {
'addServer.addPlexTitle' => 'Plexでサインイン', 'addServer.addPlexTitle' => 'Plexでサインイン',
'addServer.pinExpired' => 'サインイン前にPINの有効期限が切れました。もう一度お試しください。', 'addServer.pinExpired' => 'サインイン前にPINの有効期限が切れました。もう一度お試しください。',
'addServer.failedToRegisterAccount' => ({required Object error}) => 'アカウントの登録に失敗しました: ${error}', 'addServer.failedToRegisterAccount' => ({required Object error}) => 'アカウントの登録に失敗しました: ${error}',
'addServer.enterJellyfinUrlError' => 'JellyfinサーバーのURLを入力してください',
'addServer.addConnectionTitle' => '接続を追加', 'addServer.addConnectionTitle' => '接続を追加',
'addServer.addConnectionTitleScoped' => ({required Object name}) => '${name}に追加', 'addServer.addConnectionTitleScoped' => ({required Object name}) => '${name}に追加',
'addServer.signInWithPlexCard' => 'Plexでサインイン', 'addServer.signInWithPlexCard' => 'Plexでサインイン',
'addServer.signInWithPlexCardSubtitle' => 'このデバイスを承認します。共有サーバーが追加されます。', 'addServer.signInWithPlexCardSubtitle' => 'このデバイスを承認します。共有サーバーが追加されます。',
'addServer.signInWithPlexCardSubtitleScoped' => 'Plexアカウントを承認します。Homeユーザーはプロフィールになります。', 'addServer.signInWithPlexCardSubtitleScoped' => 'Plexアカウントを承認します。Homeユーザーはプロフィールになります。',
'addServer.connectToJellyfinCard' => 'Jellyfinに接続',
'addServer.connectToJellyfinCardSubtitle' => 'サーバーURL、ユーザー名、パスワードを入力してください。',
'addServer.connectToJellyfinCardSubtitleScoped' => ({required Object name}) => 'Jellyfinサーバーにサインインします。${name}にひも付けられます。',
'addServer.borrowFromAnotherProfile' => '別のプロフィールの接続を利用', 'addServer.borrowFromAnotherProfile' => '別のプロフィールの接続を利用',
'addServer.borrowFromAnotherProfileSubtitle' => '別のプロフィールの接続を再利用します。PINで保護されたプロフィールにはPINが必要です。', 'addServer.borrowFromAnotherProfileSubtitle' => '別のプロフィールの接続を再利用します。PINで保護されたプロフィールにはPINが必要です。',
_ => null, _ => null,
+3 -23
View File
@@ -115,7 +115,6 @@ class _Translations$auth$kk extends Translations$auth$en {
@override String get waitingForAuth => 'Растау күтілуде...\nБраузеріңізден кіріңіз.'; @override String get waitingForAuth => 'Растау күтілуде...\nБраузеріңізден кіріңіз.';
@override String get useBrowser => 'Браузерді пайдалану'; @override String get useBrowser => 'Браузерді пайдалану';
@override String get or => 'немесе'; @override String get or => 'немесе';
@override String get connectToJellyfin => 'Jellyfin-ге қосылу';
@override String get useQuickConnect => 'Жылдам қосылуды пайдалану'; @override String get useQuickConnect => 'Жылдам қосылуды пайдалану';
@override String get quickConnectInstructions => 'Jellyfin-де Жылдам қосылуды ашып, осы кодты енгізіңіз.'; @override String get quickConnectInstructions => 'Jellyfin-де Жылдам қосылуды ашып, осы кодты енгізіңіз.';
@override String get quickConnectWaiting => 'Растау күтілуде…'; @override String get quickConnectWaiting => 'Растау күтілуде…';
@@ -922,8 +921,6 @@ class _Translations$connections$kk extends Translations$connections$en {
@override String sessionExpiredOne({required Object name}) => '${name} үшін сеанс мерзімі өтті'; @override String sessionExpiredOne({required Object name}) => '${name} үшін сеанс мерзімі өтті';
@override String sessionExpiredMany({required Object count}) => '${count} сервер үшін сеанс мерзімі өтті'; @override String sessionExpiredMany({required Object count}) => '${count} сервер үшін сеанс мерзімі өтті';
@override String get signInAgain => 'Қайтадан кіру'; @override String get signInAgain => 'Қайтадан кіру';
@override String get editJellyfinTitle => 'Jellyfin қосылымын өңдеу';
@override String editJellyfinIntro({required Object serverName}) => '${serverName} үшін URL мекенжайын қосыңыз немесе өшіріңіз.';
} }
// Path: discover // Path: discover
@@ -1814,12 +1811,9 @@ class _Translations$addServer$kk extends Translations$addServer$en {
final TranslationsKk _root; // ignore: unused_field final TranslationsKk _root; // ignore: unused_field
// Translations // Translations
@override String get addJellyfinTitle => 'Jellyfin серверін қосу';
@override String get serverUrls => 'Сервер URL-дері'; @override String get serverUrls => 'Сервер URL-дері';
@override String get serverUrlsHelper => 'Үтірмен бөлінген бірнеше URL мекенжайына рұқсат етіледі.'; @override String get serverUrlsHelper => 'Үтірмен бөлінген бірнеше URL мекенжайына рұқсат етіледі.';
@override String get findServer => 'Серверді табу'; @override String get findServer => 'Серверді табу';
@override String get searchingLocalServers => 'Жергілікті Jellyfin серверлері ізделуде...';
@override String get localServers => 'Жергілікті Jellyfin серверлері';
@override String get username => 'Пайдаланушы аты'; @override String get username => 'Пайдаланушы аты';
@override String get password => 'Құпия сөз'; @override String get password => 'Құпия сөз';
@override String get signIn => 'Кіру'; @override String get signIn => 'Кіру';
@@ -1831,15 +1825,11 @@ class _Translations$addServer$kk extends Translations$addServer$en {
@override String get addPlexTitle => 'Plex арқылы кіру'; @override String get addPlexTitle => 'Plex арқылы кіру';
@override String get pinExpired => 'PIN код мерзімі өтті.'; @override String get pinExpired => 'PIN код мерзімі өтті.';
@override String failedToRegisterAccount({required Object error}) => 'Тіркелгіні тіркеу қатесі: ${error}'; @override String failedToRegisterAccount({required Object error}) => 'Тіркелгіні тіркеу қатесі: ${error}';
@override String get enterJellyfinUrlError => 'Jellyfin сервер URL-ін енгізіңіз';
@override String get addConnectionTitle => 'Қосылым қосу'; @override String get addConnectionTitle => 'Қосылым қосу';
@override String addConnectionTitleScoped({required Object name}) => '${name} профиліне қосу'; @override String addConnectionTitleScoped({required Object name}) => '${name} профиліне қосу';
@override String get signInWithPlexCard => 'Plex арқылы кіру'; @override String get signInWithPlexCard => 'Plex арқылы кіру';
@override String get signInWithPlexCardSubtitle => 'Осы құрылғыны авторизациялау.'; @override String get signInWithPlexCardSubtitle => 'Осы құрылғыны авторизациялау.';
@override String get signInWithPlexCardSubtitleScoped => 'Plex тіркелгісін авторизациялау.'; @override String get signInWithPlexCardSubtitleScoped => 'Plex тіркелгісін авторизациялау.';
@override String get connectToJellyfinCard => 'Jellyfin-ге қосылу';
@override String get connectToJellyfinCardSubtitle => 'Сервер URL-ін, пайдаланушы атын енгізіңіз.';
@override String connectToJellyfinCardSubtitleScoped({required Object name}) => 'Jellyfin серверіне кіру. ${name} профиліне жалғануда.';
@override String get borrowFromAnotherProfile => 'Басқа профильден алу'; @override String get borrowFromAnotherProfile => 'Басқа профильден алу';
@override String get borrowFromAnotherProfileSubtitle => 'Басқа профильдің қосылымын қайта пайдалану.'; @override String get borrowFromAnotherProfileSubtitle => 'Басқа профильдің қосылымын қайта пайдалану.';
} }
@@ -2219,7 +2209,6 @@ extension on TranslationsKk {
'auth.waitingForAuth' => 'Растау күтілуде...\nБраузеріңізден кіріңіз.', 'auth.waitingForAuth' => 'Растау күтілуде...\nБраузеріңізден кіріңіз.',
'auth.useBrowser' => 'Браузерді пайдалану', 'auth.useBrowser' => 'Браузерді пайдалану',
'auth.or' => 'немесе', 'auth.or' => 'немесе',
'auth.connectToJellyfin' => 'Jellyfin-ге қосылу',
'auth.useQuickConnect' => 'Жылдам қосылуды пайдалану', 'auth.useQuickConnect' => 'Жылдам қосылуды пайдалану',
'auth.quickConnectInstructions' => 'Jellyfin-де Жылдам қосылуды ашып, осы кодты енгізіңіз.', 'auth.quickConnectInstructions' => 'Jellyfin-де Жылдам қосылуды ашып, осы кодты енгізіңіз.',
'auth.quickConnectWaiting' => 'Растау күтілуде…', 'auth.quickConnectWaiting' => 'Растау күтілуде…',
@@ -2722,9 +2711,9 @@ extension on TranslationsKk {
'videoControls.noChaptersAvailable' => 'Бөлімдер қолжетімсіз', 'videoControls.noChaptersAvailable' => 'Бөлімдер қолжетімсіз',
'videoControls.queue' => 'Кезек', 'videoControls.queue' => 'Кезек',
'videoControls.noQueueItems' => 'Кезекте элементтер жоқ', 'videoControls.noQueueItems' => 'Кезекте элементтер жоқ',
'videoControls.searchSubtitles' => 'Субтитр іздеу',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'videoControls.searchSubtitles' => 'Субтитр іздеу',
'videoControls.language' => 'Тіл', 'videoControls.language' => 'Тіл',
'videoControls.noSubtitlesFound' => 'Субтитр табылмады', 'videoControls.noSubtitlesFound' => 'Субтитр табылмады',
'videoControls.subtitleDownloaded' => 'Субтитр жүктелді', 'videoControls.subtitleDownloaded' => 'Субтитр жүктелді',
@@ -2884,8 +2873,6 @@ extension on TranslationsKk {
'connections.sessionExpiredOne' => ({required Object name}) => '${name} үшін сеанс мерзімі өтті', 'connections.sessionExpiredOne' => ({required Object name}) => '${name} үшін сеанс мерзімі өтті',
'connections.sessionExpiredMany' => ({required Object count}) => '${count} сервер үшін сеанс мерзімі өтті', 'connections.sessionExpiredMany' => ({required Object count}) => '${count} сервер үшін сеанс мерзімі өтті',
'connections.signInAgain' => 'Қайтадан кіру', 'connections.signInAgain' => 'Қайтадан кіру',
'connections.editJellyfinTitle' => 'Jellyfin қосылымын өңдеу',
'connections.editJellyfinIntro' => ({required Object serverName}) => '${serverName} үшін URL мекенжайын қосыңыз немесе өшіріңіз.',
'discover.title' => 'Шолу', 'discover.title' => 'Шолу',
'discover.noContentAvailable' => 'Мазмұн қолжетімсіз', 'discover.noContentAvailable' => 'Мазмұн қолжетімсіз',
'discover.addMediaToLibraries' => 'Кітапханаларыңызға медиа қосыңыз', 'discover.addMediaToLibraries' => 'Кітапханаларыңызға медиа қосыңыз',
@@ -3236,11 +3223,11 @@ extension on TranslationsKk {
'watchTogether.enterCodeHint' => '5 таңбалы кодты енгізіңіз', 'watchTogether.enterCodeHint' => '5 таңбалы кодты енгізіңіз',
'watchTogether.pasteFromClipboard' => 'Алмасу буферінен қою', 'watchTogether.pasteFromClipboard' => 'Алмасу буферінен қою',
'watchTogether.pleaseEnterCode' => 'Сеанс кодын енгізіңіз', 'watchTogether.pleaseEnterCode' => 'Сеанс кодын енгізіңіз',
_ => null,
} ?? switch (path) {
'watchTogether.codeMustBe5Chars' => 'Сеанс коды 5 таңбадан тұруы керек', 'watchTogether.codeMustBe5Chars' => 'Сеанс коды 5 таңбадан тұруы керек',
'watchTogether.joinInstructions' => 'Ұйымдастырушының сеанс кодын енгізіңіз.', 'watchTogether.joinInstructions' => 'Ұйымдастырушының сеанс кодын енгізіңіз.',
'watchTogether.failedToCreate' => 'Сеансты жасау мүмкін болмады', 'watchTogether.failedToCreate' => 'Сеансты жасау мүмкін болмады',
_ => null,
} ?? switch (path) {
'watchTogether.failedToJoin' => 'Сеансқа қосылу мүмкін болмады', 'watchTogether.failedToJoin' => 'Сеансқа қосылу мүмкін болмады',
'watchTogether.sessionCodeCopied' => 'Сеанс коды көшірілді', 'watchTogether.sessionCodeCopied' => 'Сеанс коды көшірілді',
'watchTogether.relayUnreachable' => 'Реле сервері қолжетімсіз.', 'watchTogether.relayUnreachable' => 'Реле сервері қолжетімсіз.',
@@ -3678,12 +3665,9 @@ extension on TranslationsKk {
'services.libraryFilter.modeHintWhitelist' => 'Тек төменде таңдалған кітапханаларды синхрондау.', 'services.libraryFilter.modeHintWhitelist' => 'Тек төменде таңдалған кітапханаларды синхрондау.',
'services.libraryFilter.libraries' => 'Кітапханалар', 'services.libraryFilter.libraries' => 'Кітапханалар',
'services.libraryFilter.noLibraries' => 'Кітапханалар жоқ', 'services.libraryFilter.noLibraries' => 'Кітапханалар жоқ',
'addServer.addJellyfinTitle' => 'Jellyfin серверін қосу',
'addServer.serverUrls' => 'Сервер URL-дері', 'addServer.serverUrls' => 'Сервер URL-дері',
'addServer.serverUrlsHelper' => 'Үтірмен бөлінген бірнеше URL мекенжайына рұқсат етіледі.', 'addServer.serverUrlsHelper' => 'Үтірмен бөлінген бірнеше URL мекенжайына рұқсат етіледі.',
'addServer.findServer' => 'Серверді табу', 'addServer.findServer' => 'Серверді табу',
'addServer.searchingLocalServers' => 'Жергілікті Jellyfin серверлері ізделуде...',
'addServer.localServers' => 'Жергілікті Jellyfin серверлері',
'addServer.username' => 'Пайдаланушы аты', 'addServer.username' => 'Пайдаланушы аты',
'addServer.password' => 'Құпия сөз', 'addServer.password' => 'Құпия сөз',
'addServer.signIn' => 'Кіру', 'addServer.signIn' => 'Кіру',
@@ -3695,15 +3679,11 @@ extension on TranslationsKk {
'addServer.addPlexTitle' => 'Plex арқылы кіру', 'addServer.addPlexTitle' => 'Plex арқылы кіру',
'addServer.pinExpired' => 'PIN код мерзімі өтті.', 'addServer.pinExpired' => 'PIN код мерзімі өтті.',
'addServer.failedToRegisterAccount' => ({required Object error}) => 'Тіркелгіні тіркеу қатесі: ${error}', 'addServer.failedToRegisterAccount' => ({required Object error}) => 'Тіркелгіні тіркеу қатесі: ${error}',
'addServer.enterJellyfinUrlError' => 'Jellyfin сервер URL-ін енгізіңіз',
'addServer.addConnectionTitle' => 'Қосылым қосу', 'addServer.addConnectionTitle' => 'Қосылым қосу',
'addServer.addConnectionTitleScoped' => ({required Object name}) => '${name} профиліне қосу', 'addServer.addConnectionTitleScoped' => ({required Object name}) => '${name} профиліне қосу',
'addServer.signInWithPlexCard' => 'Plex арқылы кіру', 'addServer.signInWithPlexCard' => 'Plex арқылы кіру',
'addServer.signInWithPlexCardSubtitle' => 'Осы құрылғыны авторизациялау.', 'addServer.signInWithPlexCardSubtitle' => 'Осы құрылғыны авторизациялау.',
'addServer.signInWithPlexCardSubtitleScoped' => 'Plex тіркелгісін авторизациялау.', 'addServer.signInWithPlexCardSubtitleScoped' => 'Plex тіркелгісін авторизациялау.',
'addServer.connectToJellyfinCard' => 'Jellyfin-ге қосылу',
'addServer.connectToJellyfinCardSubtitle' => 'Сервер URL-ін, пайдаланушы атын енгізіңіз.',
'addServer.connectToJellyfinCardSubtitleScoped' => ({required Object name}) => 'Jellyfin серверіне кіру. ${name} профиліне жалғануда.',
'addServer.borrowFromAnotherProfile' => 'Басқа профильден алу', 'addServer.borrowFromAnotherProfile' => 'Басқа профильден алу',
'addServer.borrowFromAnotherProfileSubtitle' => 'Басқа профильдің қосылымын қайта пайдалану.', 'addServer.borrowFromAnotherProfileSubtitle' => 'Басқа профильдің қосылымын қайта пайдалану.',
_ => null, _ => null,
+3 -23
View File
@@ -115,7 +115,6 @@ class _Translations$auth$ko extends Translations$auth$en {
@override String get waitingForAuth => '인증 대기 중...\n브라우저에서 로그인하세요.'; @override String get waitingForAuth => '인증 대기 중...\n브라우저에서 로그인하세요.';
@override String get useBrowser => '브라우저 사용'; @override String get useBrowser => '브라우저 사용';
@override String get or => '또는'; @override String get or => '또는';
@override String get connectToJellyfin => 'Jellyfin에 연결';
@override String get useQuickConnect => 'Quick Connect 사용'; @override String get useQuickConnect => 'Quick Connect 사용';
@override String get quickConnectInstructions => 'Jellyfin에서 Quick Connect를 열고 이 코드를 입력하세요.'; @override String get quickConnectInstructions => 'Jellyfin에서 Quick Connect를 열고 이 코드를 입력하세요.';
@override String get quickConnectWaiting => '승인 대기 중…'; @override String get quickConnectWaiting => '승인 대기 중…';
@@ -917,8 +916,6 @@ class _Translations$connections$ko extends Translations$connections$en {
@override String sessionExpiredOne({required Object name}) => '${name}의 세션이 만료되었습니다'; @override String sessionExpiredOne({required Object name}) => '${name}의 세션이 만료되었습니다';
@override String sessionExpiredMany({required Object count}) => '${count}개 서버의 세션이 만료되었습니다'; @override String sessionExpiredMany({required Object count}) => '${count}개 서버의 세션이 만료되었습니다';
@override String get signInAgain => '다시 로그인'; @override String get signInAgain => '다시 로그인';
@override String get editJellyfinTitle => 'Jellyfin 연결 편집';
@override String editJellyfinIntro({required Object serverName}) => '${serverName}의 URL을 추가하거나 제거하세요. Plezy는 연결 가능한 URL 중 지연 시간이 가장 낮은 URL을 사용합니다.';
} }
// Path: discover // Path: discover
@@ -1800,12 +1797,9 @@ class _Translations$addServer$ko extends Translations$addServer$en {
final TranslationsKo _root; // ignore: unused_field final TranslationsKo _root; // ignore: unused_field
// Translations // Translations
@override String get addJellyfinTitle => 'Jellyfin 서버 추가';
@override String get serverUrls => '서버 URL'; @override String get serverUrls => '서버 URL';
@override String get serverUrlsHelper => '쉼표로 구분하여 여러 URL을 입력할 수 있습니다.'; @override String get serverUrlsHelper => '쉼표로 구분하여 여러 URL을 입력할 수 있습니다.';
@override String get findServer => '서버 찾기'; @override String get findServer => '서버 찾기';
@override String get searchingLocalServers => '로컬 Jellyfin 서버 검색 중...';
@override String get localServers => '로컬 Jellyfin 서버';
@override String get username => '사용자 이름'; @override String get username => '사용자 이름';
@override String get password => '비밀번호'; @override String get password => '비밀번호';
@override String get signIn => '로그인'; @override String get signIn => '로그인';
@@ -1817,15 +1811,11 @@ class _Translations$addServer$ko extends Translations$addServer$en {
@override String get addPlexTitle => 'Plex로 로그인'; @override String get addPlexTitle => 'Plex로 로그인';
@override String get pinExpired => '로그인 전에 PIN이 만료되었습니다. 다시 시도하세요.'; @override String get pinExpired => '로그인 전에 PIN이 만료되었습니다. 다시 시도하세요.';
@override String failedToRegisterAccount({required Object error}) => '계정 등록 실패: ${error}'; @override String failedToRegisterAccount({required Object error}) => '계정 등록 실패: ${error}';
@override String get enterJellyfinUrlError => 'Jellyfin 서버 URL을 입력하세요';
@override String get addConnectionTitle => '연결 추가'; @override String get addConnectionTitle => '연결 추가';
@override String addConnectionTitleScoped({required Object name}) => '${name}에 추가'; @override String addConnectionTitleScoped({required Object name}) => '${name}에 추가';
@override String get signInWithPlexCard => 'Plex로 로그인'; @override String get signInWithPlexCard => 'Plex로 로그인';
@override String get signInWithPlexCardSubtitle => '이 기기를 승인합니다. 공유 서버가 추가됩니다.'; @override String get signInWithPlexCardSubtitle => '이 기기를 승인합니다. 공유 서버가 추가됩니다.';
@override String get signInWithPlexCardSubtitleScoped => 'Plex 계정을 승인합니다. Home 사용자는 프로필이 됩니다.'; @override String get signInWithPlexCardSubtitleScoped => 'Plex 계정을 승인합니다. Home 사용자는 프로필이 됩니다.';
@override String get connectToJellyfinCard => 'Jellyfin에 연결';
@override String get connectToJellyfinCardSubtitle => '서버 URL, 사용자 이름, 비밀번호를 입력하세요.';
@override String connectToJellyfinCardSubtitleScoped({required Object name}) => 'Jellyfin 서버에 로그인합니다. ${name}에 연결됩니다.';
@override String get borrowFromAnotherProfile => '다른 프로필에서 빌리기'; @override String get borrowFromAnotherProfile => '다른 프로필에서 빌리기';
@override String get borrowFromAnotherProfileSubtitle => '다른 프로필의 연결을 재사용합니다. PIN으로 보호된 프로필에는 PIN이 필요합니다.'; @override String get borrowFromAnotherProfileSubtitle => '다른 프로필의 연결을 재사용합니다. PIN으로 보호된 프로필에는 PIN이 필요합니다.';
} }
@@ -2205,7 +2195,6 @@ extension on TranslationsKo {
'auth.waitingForAuth' => '인증 대기 중...\n브라우저에서 로그인하세요.', 'auth.waitingForAuth' => '인증 대기 중...\n브라우저에서 로그인하세요.',
'auth.useBrowser' => '브라우저 사용', 'auth.useBrowser' => '브라우저 사용',
'auth.or' => '또는', 'auth.or' => '또는',
'auth.connectToJellyfin' => 'Jellyfin에 연결',
'auth.useQuickConnect' => 'Quick Connect 사용', 'auth.useQuickConnect' => 'Quick Connect 사용',
'auth.quickConnectInstructions' => 'Jellyfin에서 Quick Connect를 열고 이 코드를 입력하세요.', 'auth.quickConnectInstructions' => 'Jellyfin에서 Quick Connect를 열고 이 코드를 입력하세요.',
'auth.quickConnectWaiting' => '승인 대기 중…', 'auth.quickConnectWaiting' => '승인 대기 중…',
@@ -2708,9 +2697,9 @@ extension on TranslationsKo {
'videoControls.searchSubtitles' => '자막 검색', 'videoControls.searchSubtitles' => '자막 검색',
'videoControls.language' => '언어', 'videoControls.language' => '언어',
'videoControls.noSubtitlesFound' => '자막을 찾을 수 없습니다', 'videoControls.noSubtitlesFound' => '자막을 찾을 수 없습니다',
'videoControls.subtitleDownloaded' => '자막이 다운로드되었습니다',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'videoControls.subtitleDownloaded' => '자막이 다운로드되었습니다',
'videoControls.subtitleDownloadedNotApplied' => '자막을 다운로드했지만 선택할 수 없습니다', 'videoControls.subtitleDownloadedNotApplied' => '자막을 다운로드했지만 선택할 수 없습니다',
'videoControls.subtitleDownloadFailed' => '자막 다운로드에 실패했습니다', 'videoControls.subtitleDownloadFailed' => '자막 다운로드에 실패했습니다',
'videoControls.searchLanguages' => '언어 검색...', 'videoControls.searchLanguages' => '언어 검색...',
@@ -2866,8 +2855,6 @@ extension on TranslationsKo {
'connections.sessionExpiredOne' => ({required Object name}) => '${name}의 세션이 만료되었습니다', 'connections.sessionExpiredOne' => ({required Object name}) => '${name}의 세션이 만료되었습니다',
'connections.sessionExpiredMany' => ({required Object count}) => '${count}개 서버의 세션이 만료되었습니다', 'connections.sessionExpiredMany' => ({required Object count}) => '${count}개 서버의 세션이 만료되었습니다',
'connections.signInAgain' => '다시 로그인', 'connections.signInAgain' => '다시 로그인',
'connections.editJellyfinTitle' => 'Jellyfin 연결 편집',
'connections.editJellyfinIntro' => ({required Object serverName}) => '${serverName}의 URL을 추가하거나 제거하세요. Plezy는 연결 가능한 URL 중 지연 시간이 가장 낮은 URL을 사용합니다.',
'discover.title' => '둘러보기', 'discover.title' => '둘러보기',
'discover.noContentAvailable' => '사용 가능한 콘텐츠가 없습니다', 'discover.noContentAvailable' => '사용 가능한 콘텐츠가 없습니다',
'discover.addMediaToLibraries' => '미디어 라이브러리에 미디어를 추가해 주세요', 'discover.addMediaToLibraries' => '미디어 라이브러리에 미디어를 추가해 주세요',
@@ -3222,11 +3209,11 @@ extension on TranslationsKo {
'watchTogether.failedToCreate' => '세션 생성 실패', 'watchTogether.failedToCreate' => '세션 생성 실패',
'watchTogether.failedToJoin' => '세션 참여 실패', 'watchTogether.failedToJoin' => '세션 참여 실패',
'watchTogether.sessionCodeCopied' => '세션 코드가 클립보드에 복사되었습니다', 'watchTogether.sessionCodeCopied' => '세션 코드가 클립보드에 복사되었습니다',
_ => null,
} ?? switch (path) {
'watchTogether.relayUnreachable' => '릴레이 서버에 연결할 수 없습니다. ISP 차단으로 함께 보기를 사용하지 못할 수 있습니다.', 'watchTogether.relayUnreachable' => '릴레이 서버에 연결할 수 없습니다. ISP 차단으로 함께 보기를 사용하지 못할 수 있습니다.',
'watchTogether.reconnectingToHost' => '호스트에 재연결 중...', 'watchTogether.reconnectingToHost' => '호스트에 재연결 중...',
'watchTogether.currentPlayback' => '현재 재생', 'watchTogether.currentPlayback' => '현재 재생',
_ => null,
} ?? switch (path) {
'watchTogether.joinCurrentPlayback' => '현재 재생 참여', 'watchTogether.joinCurrentPlayback' => '현재 재생 참여',
'watchTogether.joinCurrentPlaybackDescription' => '호스트가 현재 시청 중인 콘텐츠로 이동합니다', 'watchTogether.joinCurrentPlaybackDescription' => '호스트가 현재 시청 중인 콘텐츠로 이동합니다',
'watchTogether.failedToOpenCurrentPlayback' => '현재 재생을 열 수 없습니다', 'watchTogether.failedToOpenCurrentPlayback' => '현재 재생을 열 수 없습니다',
@@ -3653,12 +3640,9 @@ extension on TranslationsKo {
'services.libraryFilter.modeHintWhitelist' => '아래에 선택한 라이브러리만 동기화합니다.', 'services.libraryFilter.modeHintWhitelist' => '아래에 선택한 라이브러리만 동기화합니다.',
'services.libraryFilter.libraries' => '라이브러리', 'services.libraryFilter.libraries' => '라이브러리',
'services.libraryFilter.noLibraries' => '사용 가능한 라이브러리가 없습니다', 'services.libraryFilter.noLibraries' => '사용 가능한 라이브러리가 없습니다',
'addServer.addJellyfinTitle' => 'Jellyfin 서버 추가',
'addServer.serverUrls' => '서버 URL', 'addServer.serverUrls' => '서버 URL',
'addServer.serverUrlsHelper' => '쉼표로 구분하여 여러 URL을 입력할 수 있습니다.', 'addServer.serverUrlsHelper' => '쉼표로 구분하여 여러 URL을 입력할 수 있습니다.',
'addServer.findServer' => '서버 찾기', 'addServer.findServer' => '서버 찾기',
'addServer.searchingLocalServers' => '로컬 Jellyfin 서버 검색 중...',
'addServer.localServers' => '로컬 Jellyfin 서버',
'addServer.username' => '사용자 이름', 'addServer.username' => '사용자 이름',
'addServer.password' => '비밀번호', 'addServer.password' => '비밀번호',
'addServer.signIn' => '로그인', 'addServer.signIn' => '로그인',
@@ -3670,15 +3654,11 @@ extension on TranslationsKo {
'addServer.addPlexTitle' => 'Plex로 로그인', 'addServer.addPlexTitle' => 'Plex로 로그인',
'addServer.pinExpired' => '로그인 전에 PIN이 만료되었습니다. 다시 시도하세요.', 'addServer.pinExpired' => '로그인 전에 PIN이 만료되었습니다. 다시 시도하세요.',
'addServer.failedToRegisterAccount' => ({required Object error}) => '계정 등록 실패: ${error}', 'addServer.failedToRegisterAccount' => ({required Object error}) => '계정 등록 실패: ${error}',
'addServer.enterJellyfinUrlError' => 'Jellyfin 서버 URL을 입력하세요',
'addServer.addConnectionTitle' => '연결 추가', 'addServer.addConnectionTitle' => '연결 추가',
'addServer.addConnectionTitleScoped' => ({required Object name}) => '${name}에 추가', 'addServer.addConnectionTitleScoped' => ({required Object name}) => '${name}에 추가',
'addServer.signInWithPlexCard' => 'Plex로 로그인', 'addServer.signInWithPlexCard' => 'Plex로 로그인',
'addServer.signInWithPlexCardSubtitle' => '이 기기를 승인합니다. 공유 서버가 추가됩니다.', 'addServer.signInWithPlexCardSubtitle' => '이 기기를 승인합니다. 공유 서버가 추가됩니다.',
'addServer.signInWithPlexCardSubtitleScoped' => 'Plex 계정을 승인합니다. Home 사용자는 프로필이 됩니다.', 'addServer.signInWithPlexCardSubtitleScoped' => 'Plex 계정을 승인합니다. Home 사용자는 프로필이 됩니다.',
'addServer.connectToJellyfinCard' => 'Jellyfin에 연결',
'addServer.connectToJellyfinCardSubtitle' => '서버 URL, 사용자 이름, 비밀번호를 입력하세요.',
'addServer.connectToJellyfinCardSubtitleScoped' => ({required Object name}) => 'Jellyfin 서버에 로그인합니다. ${name}에 연결됩니다.',
'addServer.borrowFromAnotherProfile' => '다른 프로필에서 빌리기', 'addServer.borrowFromAnotherProfile' => '다른 프로필에서 빌리기',
'addServer.borrowFromAnotherProfileSubtitle' => '다른 프로필의 연결을 재사용합니다. PIN으로 보호된 프로필에는 PIN이 필요합니다.', 'addServer.borrowFromAnotherProfileSubtitle' => '다른 프로필의 연결을 재사용합니다. PIN으로 보호된 프로필에는 PIN이 필요합니다.',
_ => null, _ => null,
+3 -23
View File
@@ -115,7 +115,6 @@ class _Translations$auth$nb extends Translations$auth$en {
@override String get waitingForAuth => 'Venter på autentisering...\nLogg inn fra nettleseren.'; @override String get waitingForAuth => 'Venter på autentisering...\nLogg inn fra nettleseren.';
@override String get useBrowser => 'Bruk nettleser'; @override String get useBrowser => 'Bruk nettleser';
@override String get or => 'eller'; @override String get or => 'eller';
@override String get connectToJellyfin => 'Koble til Jellyfin';
@override String get useQuickConnect => 'Bruk Quick Connect'; @override String get useQuickConnect => 'Bruk Quick Connect';
@override String get quickConnectInstructions => 'Åpne Quick Connect i Jellyfin og skriv inn denne koden.'; @override String get quickConnectInstructions => 'Åpne Quick Connect i Jellyfin og skriv inn denne koden.';
@override String get quickConnectWaiting => 'Venter på godkjenning…'; @override String get quickConnectWaiting => 'Venter på godkjenning…';
@@ -918,8 +917,6 @@ class _Translations$connections$nb extends Translations$connections$en {
@override String sessionExpiredOne({required Object name}) => 'Økten er utløpt for ${name}'; @override String sessionExpiredOne({required Object name}) => 'Økten er utløpt for ${name}';
@override String sessionExpiredMany({required Object count}) => 'Økten er utløpt for ${count} servere'; @override String sessionExpiredMany({required Object count}) => 'Økten er utløpt for ${count} servere';
@override String get signInAgain => 'Logg inn igjen'; @override String get signInAgain => 'Logg inn igjen';
@override String get editJellyfinTitle => 'Rediger Jellyfin-tilkobling';
@override String editJellyfinIntro({required Object serverName}) => 'Legg til eller fjern URL-er for ${serverName}. Plezy bruker den tilgjengelige URL-en med lavest forsinkelse.';
} }
// Path: discover // Path: discover
@@ -1803,12 +1800,9 @@ class _Translations$addServer$nb extends Translations$addServer$en {
final TranslationsNb _root; // ignore: unused_field final TranslationsNb _root; // ignore: unused_field
// Translations // Translations
@override String get addJellyfinTitle => 'Legg til Jellyfin-server';
@override String get serverUrls => 'Server-URL-er'; @override String get serverUrls => 'Server-URL-er';
@override String get serverUrlsHelper => 'Flere URL-er er tillatt, atskilt med komma.'; @override String get serverUrlsHelper => 'Flere URL-er er tillatt, atskilt med komma.';
@override String get findServer => 'Finn server'; @override String get findServer => 'Finn server';
@override String get searchingLocalServers => 'Søker etter lokale Jellyfin-servere...';
@override String get localServers => 'Lokale Jellyfin-servere';
@override String get username => 'Brukernavn'; @override String get username => 'Brukernavn';
@override String get password => 'Passord'; @override String get password => 'Passord';
@override String get signIn => 'Logg inn'; @override String get signIn => 'Logg inn';
@@ -1820,15 +1814,11 @@ class _Translations$addServer$nb extends Translations$addServer$en {
@override String get addPlexTitle => 'Logg inn med Plex'; @override String get addPlexTitle => 'Logg inn med Plex';
@override String get pinExpired => 'PIN-koden utløp før innloggingen var fullført. Prøv igjen.'; @override String get pinExpired => 'PIN-koden utløp før innloggingen var fullført. Prøv igjen.';
@override String failedToRegisterAccount({required Object error}) => 'Kunne ikke registrere kontoen: ${error}'; @override String failedToRegisterAccount({required Object error}) => 'Kunne ikke registrere kontoen: ${error}';
@override String get enterJellyfinUrlError => 'Oppgi URL-en til Jellyfin-serveren din';
@override String get addConnectionTitle => 'Legg til tilkobling'; @override String get addConnectionTitle => 'Legg til tilkobling';
@override String addConnectionTitleScoped({required Object name}) => 'Legg til for ${name}'; @override String addConnectionTitleScoped({required Object name}) => 'Legg til for ${name}';
@override String get signInWithPlexCard => 'Logg inn med Plex'; @override String get signInWithPlexCard => 'Logg inn med Plex';
@override String get signInWithPlexCardSubtitle => 'Autoriser denne enheten. Delte servere legges til.'; @override String get signInWithPlexCardSubtitle => 'Autoriser denne enheten. Delte servere legges til.';
@override String get signInWithPlexCardSubtitleScoped => 'Autoriser en Plex-konto. Home-brukere blir profiler.'; @override String get signInWithPlexCardSubtitleScoped => 'Autoriser en Plex-konto. Home-brukere blir profiler.';
@override String get connectToJellyfinCard => 'Koble til Jellyfin';
@override String get connectToJellyfinCardSubtitle => 'Skriv inn server-URL, brukernavn og passord.';
@override String connectToJellyfinCardSubtitleScoped({required Object name}) => 'Logg på en Jellyfin-server. Knyttes til ${name}.';
@override String get borrowFromAnotherProfile => 'Lån fra en annen profil'; @override String get borrowFromAnotherProfile => 'Lån fra en annen profil';
@override String get borrowFromAnotherProfileSubtitle => 'Gjenbruk en annen profils tilkobling. PIN-beskyttede profiler krever PIN.'; @override String get borrowFromAnotherProfileSubtitle => 'Gjenbruk en annen profils tilkobling. PIN-beskyttede profiler krever PIN.';
} }
@@ -2208,7 +2198,6 @@ extension on TranslationsNb {
'auth.waitingForAuth' => 'Venter på autentisering...\nLogg inn fra nettleseren.', 'auth.waitingForAuth' => 'Venter på autentisering...\nLogg inn fra nettleseren.',
'auth.useBrowser' => 'Bruk nettleser', 'auth.useBrowser' => 'Bruk nettleser',
'auth.or' => 'eller', 'auth.or' => 'eller',
'auth.connectToJellyfin' => 'Koble til Jellyfin',
'auth.useQuickConnect' => 'Bruk Quick Connect', 'auth.useQuickConnect' => 'Bruk Quick Connect',
'auth.quickConnectInstructions' => 'Åpne Quick Connect i Jellyfin og skriv inn denne koden.', 'auth.quickConnectInstructions' => 'Åpne Quick Connect i Jellyfin og skriv inn denne koden.',
'auth.quickConnectWaiting' => 'Venter på godkjenning…', 'auth.quickConnectWaiting' => 'Venter på godkjenning…',
@@ -2711,9 +2700,9 @@ extension on TranslationsNb {
'videoControls.searchSubtitles' => 'Søk etter undertekster', 'videoControls.searchSubtitles' => 'Søk etter undertekster',
'videoControls.language' => 'Språk', 'videoControls.language' => 'Språk',
'videoControls.noSubtitlesFound' => 'Ingen undertekster funnet', 'videoControls.noSubtitlesFound' => 'Ingen undertekster funnet',
'videoControls.subtitleDownloaded' => 'Undertekst lastet ned',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'videoControls.subtitleDownloaded' => 'Undertekst lastet ned',
'videoControls.subtitleDownloadedNotApplied' => 'Underteksten ble lastet ned, men kunne ikke velges', 'videoControls.subtitleDownloadedNotApplied' => 'Underteksten ble lastet ned, men kunne ikke velges',
'videoControls.subtitleDownloadFailed' => 'Kunne ikke laste ned undertekst', 'videoControls.subtitleDownloadFailed' => 'Kunne ikke laste ned undertekst',
'videoControls.searchLanguages' => 'Søk etter språk...', 'videoControls.searchLanguages' => 'Søk etter språk...',
@@ -2869,8 +2858,6 @@ extension on TranslationsNb {
'connections.sessionExpiredOne' => ({required Object name}) => 'Økten er utløpt for ${name}', 'connections.sessionExpiredOne' => ({required Object name}) => 'Økten er utløpt for ${name}',
'connections.sessionExpiredMany' => ({required Object count}) => 'Økten er utløpt for ${count} servere', 'connections.sessionExpiredMany' => ({required Object count}) => 'Økten er utløpt for ${count} servere',
'connections.signInAgain' => 'Logg inn igjen', 'connections.signInAgain' => 'Logg inn igjen',
'connections.editJellyfinTitle' => 'Rediger Jellyfin-tilkobling',
'connections.editJellyfinIntro' => ({required Object serverName}) => 'Legg til eller fjern URL-er for ${serverName}. Plezy bruker den tilgjengelige URL-en med lavest forsinkelse.',
'discover.title' => 'Oppdag', 'discover.title' => 'Oppdag',
'discover.noContentAvailable' => 'Ikke noe innhold tilgjengelig', 'discover.noContentAvailable' => 'Ikke noe innhold tilgjengelig',
'discover.addMediaToLibraries' => 'Legg til medier i bibliotekene dine', 'discover.addMediaToLibraries' => 'Legg til medier i bibliotekene dine',
@@ -3225,11 +3212,11 @@ extension on TranslationsNb {
'watchTogether.failedToCreate' => 'Kunne ikke opprette økt', 'watchTogether.failedToCreate' => 'Kunne ikke opprette økt',
'watchTogether.failedToJoin' => 'Kunne ikke bli med i økt', 'watchTogether.failedToJoin' => 'Kunne ikke bli med i økt',
'watchTogether.sessionCodeCopied' => 'Øktkode kopiert til utklippstavle', 'watchTogether.sessionCodeCopied' => 'Øktkode kopiert til utklippstavle',
_ => null,
} ?? switch (path) {
'watchTogether.relayUnreachable' => 'Reléserveren kan ikke nås. Blokkering hos internettleverandøren kan hindre Se sammen.', 'watchTogether.relayUnreachable' => 'Reléserveren kan ikke nås. Blokkering hos internettleverandøren kan hindre Se sammen.',
'watchTogether.reconnectingToHost' => 'Kobler til verten på nytt...', 'watchTogether.reconnectingToHost' => 'Kobler til verten på nytt...',
'watchTogether.currentPlayback' => 'Gjeldende avspilling', 'watchTogether.currentPlayback' => 'Gjeldende avspilling',
_ => null,
} ?? switch (path) {
'watchTogether.joinCurrentPlayback' => 'Bli med i gjeldende avspilling', 'watchTogether.joinCurrentPlayback' => 'Bli med i gjeldende avspilling',
'watchTogether.joinCurrentPlaybackDescription' => 'Hopp tilbake til det verten ser på nå', 'watchTogether.joinCurrentPlaybackDescription' => 'Hopp tilbake til det verten ser på nå',
'watchTogether.failedToOpenCurrentPlayback' => 'Kunne ikke åpne gjeldende avspilling', 'watchTogether.failedToOpenCurrentPlayback' => 'Kunne ikke åpne gjeldende avspilling',
@@ -3656,12 +3643,9 @@ extension on TranslationsNb {
'services.libraryFilter.modeHintWhitelist' => 'Synkroniser kun bibliotekene du markerer nedenfor.', 'services.libraryFilter.modeHintWhitelist' => 'Synkroniser kun bibliotekene du markerer nedenfor.',
'services.libraryFilter.libraries' => 'Biblioteker', 'services.libraryFilter.libraries' => 'Biblioteker',
'services.libraryFilter.noLibraries' => 'Ingen biblioteker tilgjengelige', 'services.libraryFilter.noLibraries' => 'Ingen biblioteker tilgjengelige',
'addServer.addJellyfinTitle' => 'Legg til Jellyfin-server',
'addServer.serverUrls' => 'Server-URL-er', 'addServer.serverUrls' => 'Server-URL-er',
'addServer.serverUrlsHelper' => 'Flere URL-er er tillatt, atskilt med komma.', 'addServer.serverUrlsHelper' => 'Flere URL-er er tillatt, atskilt med komma.',
'addServer.findServer' => 'Finn server', 'addServer.findServer' => 'Finn server',
'addServer.searchingLocalServers' => 'Søker etter lokale Jellyfin-servere...',
'addServer.localServers' => 'Lokale Jellyfin-servere',
'addServer.username' => 'Brukernavn', 'addServer.username' => 'Brukernavn',
'addServer.password' => 'Passord', 'addServer.password' => 'Passord',
'addServer.signIn' => 'Logg inn', 'addServer.signIn' => 'Logg inn',
@@ -3673,15 +3657,11 @@ extension on TranslationsNb {
'addServer.addPlexTitle' => 'Logg inn med Plex', 'addServer.addPlexTitle' => 'Logg inn med Plex',
'addServer.pinExpired' => 'PIN-koden utløp før innloggingen var fullført. Prøv igjen.', 'addServer.pinExpired' => 'PIN-koden utløp før innloggingen var fullført. Prøv igjen.',
'addServer.failedToRegisterAccount' => ({required Object error}) => 'Kunne ikke registrere kontoen: ${error}', 'addServer.failedToRegisterAccount' => ({required Object error}) => 'Kunne ikke registrere kontoen: ${error}',
'addServer.enterJellyfinUrlError' => 'Oppgi URL-en til Jellyfin-serveren din',
'addServer.addConnectionTitle' => 'Legg til tilkobling', 'addServer.addConnectionTitle' => 'Legg til tilkobling',
'addServer.addConnectionTitleScoped' => ({required Object name}) => 'Legg til for ${name}', 'addServer.addConnectionTitleScoped' => ({required Object name}) => 'Legg til for ${name}',
'addServer.signInWithPlexCard' => 'Logg inn med Plex', 'addServer.signInWithPlexCard' => 'Logg inn med Plex',
'addServer.signInWithPlexCardSubtitle' => 'Autoriser denne enheten. Delte servere legges til.', 'addServer.signInWithPlexCardSubtitle' => 'Autoriser denne enheten. Delte servere legges til.',
'addServer.signInWithPlexCardSubtitleScoped' => 'Autoriser en Plex-konto. Home-brukere blir profiler.', 'addServer.signInWithPlexCardSubtitleScoped' => 'Autoriser en Plex-konto. Home-brukere blir profiler.',
'addServer.connectToJellyfinCard' => 'Koble til Jellyfin',
'addServer.connectToJellyfinCardSubtitle' => 'Skriv inn server-URL, brukernavn og passord.',
'addServer.connectToJellyfinCardSubtitleScoped' => ({required Object name}) => 'Logg på en Jellyfin-server. Knyttes til ${name}.',
'addServer.borrowFromAnotherProfile' => 'Lån fra en annen profil', 'addServer.borrowFromAnotherProfile' => 'Lån fra en annen profil',
'addServer.borrowFromAnotherProfileSubtitle' => 'Gjenbruk en annen profils tilkobling. PIN-beskyttede profiler krever PIN.', 'addServer.borrowFromAnotherProfileSubtitle' => 'Gjenbruk en annen profils tilkobling. PIN-beskyttede profiler krever PIN.',
_ => null, _ => null,
+3 -23
View File
@@ -115,7 +115,6 @@ class _Translations$auth$nl extends Translations$auth$en {
@override String get waitingForAuth => 'Wachten op authenticatie...\nMeld je aan via je browser.'; @override String get waitingForAuth => 'Wachten op authenticatie...\nMeld je aan via je browser.';
@override String get useBrowser => 'Gebruik browser'; @override String get useBrowser => 'Gebruik browser';
@override String get or => 'of'; @override String get or => 'of';
@override String get connectToJellyfin => 'Verbinden met Jellyfin';
@override String get useQuickConnect => 'Quick Connect gebruiken'; @override String get useQuickConnect => 'Quick Connect gebruiken';
@override String get quickConnectInstructions => 'Open Quick Connect in Jellyfin en voer deze code in.'; @override String get quickConnectInstructions => 'Open Quick Connect in Jellyfin en voer deze code in.';
@override String get quickConnectWaiting => 'Wachten op goedkeuring…'; @override String get quickConnectWaiting => 'Wachten op goedkeuring…';
@@ -918,8 +917,6 @@ class _Translations$connections$nl extends Translations$connections$en {
@override String sessionExpiredOne({required Object name}) => 'Sessie verlopen voor ${name}'; @override String sessionExpiredOne({required Object name}) => 'Sessie verlopen voor ${name}';
@override String sessionExpiredMany({required Object count}) => 'Sessie verlopen voor ${count} servers'; @override String sessionExpiredMany({required Object count}) => 'Sessie verlopen voor ${count} servers';
@override String get signInAgain => 'Opnieuw aanmelden'; @override String get signInAgain => 'Opnieuw aanmelden';
@override String get editJellyfinTitle => 'Jellyfin-verbinding bewerken';
@override String editJellyfinIntro({required Object serverName}) => 'Voeg URL\'s voor ${serverName} toe of verwijder ze. Plezy gebruikt de bereikbare URL met de laagste latentie.';
} }
// Path: discover // Path: discover
@@ -1803,12 +1800,9 @@ class _Translations$addServer$nl extends Translations$addServer$en {
final TranslationsNl _root; // ignore: unused_field final TranslationsNl _root; // ignore: unused_field
// Translations // Translations
@override String get addJellyfinTitle => 'Jellyfin-server toevoegen';
@override String get serverUrls => 'Server-URL\'s'; @override String get serverUrls => 'Server-URL\'s';
@override String get serverUrlsHelper => 'Meerdere URL\'s toegestaan, gescheiden door komma\'s.'; @override String get serverUrlsHelper => 'Meerdere URL\'s toegestaan, gescheiden door komma\'s.';
@override String get findServer => 'Server zoeken'; @override String get findServer => 'Server zoeken';
@override String get searchingLocalServers => 'Lokale Jellyfin-servers zoeken...';
@override String get localServers => 'Lokale Jellyfin-servers';
@override String get username => 'Gebruikersnaam'; @override String get username => 'Gebruikersnaam';
@override String get password => 'Wachtwoord'; @override String get password => 'Wachtwoord';
@override String get signIn => 'Inloggen'; @override String get signIn => 'Inloggen';
@@ -1820,15 +1814,11 @@ class _Translations$addServer$nl extends Translations$addServer$en {
@override String get addPlexTitle => 'Inloggen met Plex'; @override String get addPlexTitle => 'Inloggen met Plex';
@override String get pinExpired => 'De pincode verliep voordat je kon inloggen. Probeer het opnieuw.'; @override String get pinExpired => 'De pincode verliep voordat je kon inloggen. Probeer het opnieuw.';
@override String failedToRegisterAccount({required Object error}) => 'Account registreren mislukt: ${error}'; @override String failedToRegisterAccount({required Object error}) => 'Account registreren mislukt: ${error}';
@override String get enterJellyfinUrlError => 'Voer de URL van je Jellyfin-server in';
@override String get addConnectionTitle => 'Verbinding toevoegen'; @override String get addConnectionTitle => 'Verbinding toevoegen';
@override String addConnectionTitleScoped({required Object name}) => 'Toevoegen aan ${name}'; @override String addConnectionTitleScoped({required Object name}) => 'Toevoegen aan ${name}';
@override String get signInWithPlexCard => 'Inloggen met Plex'; @override String get signInWithPlexCard => 'Inloggen met Plex';
@override String get signInWithPlexCardSubtitle => 'Autoriseer dit apparaat. Gedeelde servers worden toegevoegd.'; @override String get signInWithPlexCardSubtitle => 'Autoriseer dit apparaat. Gedeelde servers worden toegevoegd.';
@override String get signInWithPlexCardSubtitleScoped => 'Autoriseer een Plex-account. Home-gebruikers worden profielen.'; @override String get signInWithPlexCardSubtitleScoped => 'Autoriseer een Plex-account. Home-gebruikers worden profielen.';
@override String get connectToJellyfinCard => 'Verbinden met Jellyfin';
@override String get connectToJellyfinCardSubtitle => 'Voer je server-URL, gebruikersnaam en wachtwoord in.';
@override String connectToJellyfinCardSubtitleScoped({required Object name}) => 'Log in op een Jellyfin-server. Wordt gekoppeld aan ${name}.';
@override String get borrowFromAnotherProfile => 'Van een ander profiel lenen'; @override String get borrowFromAnotherProfile => 'Van een ander profiel lenen';
@override String get borrowFromAnotherProfileSubtitle => 'Hergebruik de verbinding van een ander profiel. Voor profielen met pincodebeveiliging is een pincode vereist.'; @override String get borrowFromAnotherProfileSubtitle => 'Hergebruik de verbinding van een ander profiel. Voor profielen met pincodebeveiliging is een pincode vereist.';
} }
@@ -2208,7 +2198,6 @@ extension on TranslationsNl {
'auth.waitingForAuth' => 'Wachten op authenticatie...\nMeld je aan via je browser.', 'auth.waitingForAuth' => 'Wachten op authenticatie...\nMeld je aan via je browser.',
'auth.useBrowser' => 'Gebruik browser', 'auth.useBrowser' => 'Gebruik browser',
'auth.or' => 'of', 'auth.or' => 'of',
'auth.connectToJellyfin' => 'Verbinden met Jellyfin',
'auth.useQuickConnect' => 'Quick Connect gebruiken', 'auth.useQuickConnect' => 'Quick Connect gebruiken',
'auth.quickConnectInstructions' => 'Open Quick Connect in Jellyfin en voer deze code in.', 'auth.quickConnectInstructions' => 'Open Quick Connect in Jellyfin en voer deze code in.',
'auth.quickConnectWaiting' => 'Wachten op goedkeuring…', 'auth.quickConnectWaiting' => 'Wachten op goedkeuring…',
@@ -2711,9 +2700,9 @@ extension on TranslationsNl {
'videoControls.searchSubtitles' => 'Ondertitels zoeken', 'videoControls.searchSubtitles' => 'Ondertitels zoeken',
'videoControls.language' => 'Taal', 'videoControls.language' => 'Taal',
'videoControls.noSubtitlesFound' => 'Geen ondertitels gevonden', 'videoControls.noSubtitlesFound' => 'Geen ondertitels gevonden',
'videoControls.subtitleDownloaded' => 'Ondertitel gedownload',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'videoControls.subtitleDownloaded' => 'Ondertitel gedownload',
'videoControls.subtitleDownloadedNotApplied' => 'De ondertiteling is gedownload, maar kon niet worden geselecteerd', 'videoControls.subtitleDownloadedNotApplied' => 'De ondertiteling is gedownload, maar kon niet worden geselecteerd',
'videoControls.subtitleDownloadFailed' => 'Ondertitel downloaden mislukt', 'videoControls.subtitleDownloadFailed' => 'Ondertitel downloaden mislukt',
'videoControls.searchLanguages' => 'Talen zoeken...', 'videoControls.searchLanguages' => 'Talen zoeken...',
@@ -2869,8 +2858,6 @@ extension on TranslationsNl {
'connections.sessionExpiredOne' => ({required Object name}) => 'Sessie verlopen voor ${name}', 'connections.sessionExpiredOne' => ({required Object name}) => 'Sessie verlopen voor ${name}',
'connections.sessionExpiredMany' => ({required Object count}) => 'Sessie verlopen voor ${count} servers', 'connections.sessionExpiredMany' => ({required Object count}) => 'Sessie verlopen voor ${count} servers',
'connections.signInAgain' => 'Opnieuw aanmelden', 'connections.signInAgain' => 'Opnieuw aanmelden',
'connections.editJellyfinTitle' => 'Jellyfin-verbinding bewerken',
'connections.editJellyfinIntro' => ({required Object serverName}) => 'Voeg URL\'s voor ${serverName} toe of verwijder ze. Plezy gebruikt de bereikbare URL met de laagste latentie.',
'discover.title' => 'Ontdekken', 'discover.title' => 'Ontdekken',
'discover.noContentAvailable' => 'Geen inhoud beschikbaar', 'discover.noContentAvailable' => 'Geen inhoud beschikbaar',
'discover.addMediaToLibraries' => 'Voeg wat media toe aan je bibliotheken', 'discover.addMediaToLibraries' => 'Voeg wat media toe aan je bibliotheken',
@@ -3225,11 +3212,11 @@ extension on TranslationsNl {
'watchTogether.failedToCreate' => 'Sessie maken mislukt', 'watchTogether.failedToCreate' => 'Sessie maken mislukt',
'watchTogether.failedToJoin' => 'Deelnemen aan sessie mislukt', 'watchTogether.failedToJoin' => 'Deelnemen aan sessie mislukt',
'watchTogether.sessionCodeCopied' => 'Sessiecode naar het klembord gekopieerd', 'watchTogether.sessionCodeCopied' => 'Sessiecode naar het klembord gekopieerd',
_ => null,
} ?? switch (path) {
'watchTogether.relayUnreachable' => 'De relayserver is onbereikbaar. Een blokkering door je internetprovider kan Samen kijken verhinderen.', 'watchTogether.relayUnreachable' => 'De relayserver is onbereikbaar. Een blokkering door je internetprovider kan Samen kijken verhinderen.',
'watchTogether.reconnectingToHost' => 'Opnieuw verbinden met host...', 'watchTogether.reconnectingToHost' => 'Opnieuw verbinden met host...',
'watchTogether.currentPlayback' => 'Wat nu wordt afgespeeld', 'watchTogether.currentPlayback' => 'Wat nu wordt afgespeeld',
_ => null,
} ?? switch (path) {
'watchTogether.joinCurrentPlayback' => 'Deelnemen aan huidige weergave', 'watchTogether.joinCurrentPlayback' => 'Deelnemen aan huidige weergave',
'watchTogether.joinCurrentPlaybackDescription' => 'Ga terug naar wat de host nu kijkt', 'watchTogether.joinCurrentPlaybackDescription' => 'Ga terug naar wat de host nu kijkt',
'watchTogether.failedToOpenCurrentPlayback' => 'Wat nu wordt afgespeeld kon niet worden geopend', 'watchTogether.failedToOpenCurrentPlayback' => 'Wat nu wordt afgespeeld kon niet worden geopend',
@@ -3656,12 +3643,9 @@ extension on TranslationsNl {
'services.libraryFilter.modeHintWhitelist' => 'Synchroniseer alleen de hieronder aangevinkte bibliotheken.', 'services.libraryFilter.modeHintWhitelist' => 'Synchroniseer alleen de hieronder aangevinkte bibliotheken.',
'services.libraryFilter.libraries' => 'Bibliotheken', 'services.libraryFilter.libraries' => 'Bibliotheken',
'services.libraryFilter.noLibraries' => 'Geen bibliotheken beschikbaar', 'services.libraryFilter.noLibraries' => 'Geen bibliotheken beschikbaar',
'addServer.addJellyfinTitle' => 'Jellyfin-server toevoegen',
'addServer.serverUrls' => 'Server-URL\'s', 'addServer.serverUrls' => 'Server-URL\'s',
'addServer.serverUrlsHelper' => 'Meerdere URL\'s toegestaan, gescheiden door komma\'s.', 'addServer.serverUrlsHelper' => 'Meerdere URL\'s toegestaan, gescheiden door komma\'s.',
'addServer.findServer' => 'Server zoeken', 'addServer.findServer' => 'Server zoeken',
'addServer.searchingLocalServers' => 'Lokale Jellyfin-servers zoeken...',
'addServer.localServers' => 'Lokale Jellyfin-servers',
'addServer.username' => 'Gebruikersnaam', 'addServer.username' => 'Gebruikersnaam',
'addServer.password' => 'Wachtwoord', 'addServer.password' => 'Wachtwoord',
'addServer.signIn' => 'Inloggen', 'addServer.signIn' => 'Inloggen',
@@ -3673,15 +3657,11 @@ extension on TranslationsNl {
'addServer.addPlexTitle' => 'Inloggen met Plex', 'addServer.addPlexTitle' => 'Inloggen met Plex',
'addServer.pinExpired' => 'De pincode verliep voordat je kon inloggen. Probeer het opnieuw.', 'addServer.pinExpired' => 'De pincode verliep voordat je kon inloggen. Probeer het opnieuw.',
'addServer.failedToRegisterAccount' => ({required Object error}) => 'Account registreren mislukt: ${error}', 'addServer.failedToRegisterAccount' => ({required Object error}) => 'Account registreren mislukt: ${error}',
'addServer.enterJellyfinUrlError' => 'Voer de URL van je Jellyfin-server in',
'addServer.addConnectionTitle' => 'Verbinding toevoegen', 'addServer.addConnectionTitle' => 'Verbinding toevoegen',
'addServer.addConnectionTitleScoped' => ({required Object name}) => 'Toevoegen aan ${name}', 'addServer.addConnectionTitleScoped' => ({required Object name}) => 'Toevoegen aan ${name}',
'addServer.signInWithPlexCard' => 'Inloggen met Plex', 'addServer.signInWithPlexCard' => 'Inloggen met Plex',
'addServer.signInWithPlexCardSubtitle' => 'Autoriseer dit apparaat. Gedeelde servers worden toegevoegd.', 'addServer.signInWithPlexCardSubtitle' => 'Autoriseer dit apparaat. Gedeelde servers worden toegevoegd.',
'addServer.signInWithPlexCardSubtitleScoped' => 'Autoriseer een Plex-account. Home-gebruikers worden profielen.', 'addServer.signInWithPlexCardSubtitleScoped' => 'Autoriseer een Plex-account. Home-gebruikers worden profielen.',
'addServer.connectToJellyfinCard' => 'Verbinden met Jellyfin',
'addServer.connectToJellyfinCardSubtitle' => 'Voer je server-URL, gebruikersnaam en wachtwoord in.',
'addServer.connectToJellyfinCardSubtitleScoped' => ({required Object name}) => 'Log in op een Jellyfin-server. Wordt gekoppeld aan ${name}.',
'addServer.borrowFromAnotherProfile' => 'Van een ander profiel lenen', 'addServer.borrowFromAnotherProfile' => 'Van een ander profiel lenen',
'addServer.borrowFromAnotherProfileSubtitle' => 'Hergebruik de verbinding van een ander profiel. Voor profielen met pincodebeveiliging is een pincode vereist.', 'addServer.borrowFromAnotherProfileSubtitle' => 'Hergebruik de verbinding van een ander profiel. Voor profielen met pincodebeveiliging is een pincode vereist.',
_ => null, _ => null,
+3 -23
View File
@@ -115,7 +115,6 @@ class _Translations$auth$pl extends Translations$auth$en {
@override String get waitingForAuth => 'Oczekiwanie na uwierzytelnienie...\nZaloguj się w przeglądarce.'; @override String get waitingForAuth => 'Oczekiwanie na uwierzytelnienie...\nZaloguj się w przeglądarce.';
@override String get useBrowser => 'Użyj przeglądarki'; @override String get useBrowser => 'Użyj przeglądarki';
@override String get or => 'lub'; @override String get or => 'lub';
@override String get connectToJellyfin => 'Połącz z Jellyfin';
@override String get useQuickConnect => 'Użyj Quick Connect'; @override String get useQuickConnect => 'Użyj Quick Connect';
@override String get quickConnectInstructions => 'Otwórz Quick Connect w Jellyfin i wpisz ten kod.'; @override String get quickConnectInstructions => 'Otwórz Quick Connect w Jellyfin i wpisz ten kod.';
@override String get quickConnectWaiting => 'Oczekiwanie na zatwierdzenie…'; @override String get quickConnectWaiting => 'Oczekiwanie na zatwierdzenie…';
@@ -920,8 +919,6 @@ class _Translations$connections$pl extends Translations$connections$en {
@override String sessionExpiredOne({required Object name}) => 'Sesja wygasła dla ${name}'; @override String sessionExpiredOne({required Object name}) => 'Sesja wygasła dla ${name}';
@override String sessionExpiredMany({required Object count}) => 'Sesja wygasła dla ${count} serwerów'; @override String sessionExpiredMany({required Object count}) => 'Sesja wygasła dla ${count} serwerów';
@override String get signInAgain => 'Zaloguj się ponownie'; @override String get signInAgain => 'Zaloguj się ponownie';
@override String get editJellyfinTitle => 'Edytuj połączenie Jellyfin';
@override String editJellyfinIntro({required Object serverName}) => 'Dodaj lub usuń adresy URL dla ${serverName}. Plezy użyje osiągalnego URL-a o najniższym opóźnieniu.';
} }
// Path: discover // Path: discover
@@ -1809,12 +1806,9 @@ class _Translations$addServer$pl extends Translations$addServer$en {
final TranslationsPl _root; // ignore: unused_field final TranslationsPl _root; // ignore: unused_field
// Translations // Translations
@override String get addJellyfinTitle => 'Dodaj serwer Jellyfin';
@override String get serverUrls => 'Adresy URL serwera'; @override String get serverUrls => 'Adresy URL serwera';
@override String get serverUrlsHelper => 'Można podać wiele adresów URL rozdzielonych przecinkami.'; @override String get serverUrlsHelper => 'Można podać wiele adresów URL rozdzielonych przecinkami.';
@override String get findServer => 'Znajdź serwer'; @override String get findServer => 'Znajdź serwer';
@override String get searchingLocalServers => 'Szukanie lokalnych serwerów Jellyfin...';
@override String get localServers => 'Lokalne serwery Jellyfin';
@override String get username => 'Nazwa użytkownika'; @override String get username => 'Nazwa użytkownika';
@override String get password => 'Hasło'; @override String get password => 'Hasło';
@override String get signIn => 'Zaloguj się'; @override String get signIn => 'Zaloguj się';
@@ -1826,15 +1820,11 @@ class _Translations$addServer$pl extends Translations$addServer$en {
@override String get addPlexTitle => 'Zaloguj się przez Plex'; @override String get addPlexTitle => 'Zaloguj się przez Plex';
@override String get pinExpired => 'PIN wygasł przed zalogowaniem. Spróbuj ponownie.'; @override String get pinExpired => 'PIN wygasł przed zalogowaniem. Spróbuj ponownie.';
@override String failedToRegisterAccount({required Object error}) => 'Nie udało się zarejestrować konta: ${error}'; @override String failedToRegisterAccount({required Object error}) => 'Nie udało się zarejestrować konta: ${error}';
@override String get enterJellyfinUrlError => 'Podaj URL serwera Jellyfin';
@override String get addConnectionTitle => 'Dodaj połączenie'; @override String get addConnectionTitle => 'Dodaj połączenie';
@override String addConnectionTitleScoped({required Object name}) => 'Dodaj do ${name}'; @override String addConnectionTitleScoped({required Object name}) => 'Dodaj do ${name}';
@override String get signInWithPlexCard => 'Zaloguj się przez Plex'; @override String get signInWithPlexCard => 'Zaloguj się przez Plex';
@override String get signInWithPlexCardSubtitle => 'Autoryzuj to urządzenie. Serwery udostępnione zostaną dodane.'; @override String get signInWithPlexCardSubtitle => 'Autoryzuj to urządzenie. Serwery udostępnione zostaną dodane.';
@override String get signInWithPlexCardSubtitleScoped => 'Autoryzuj konto Plex. Użytkownicy Home staną się profilami.'; @override String get signInWithPlexCardSubtitleScoped => 'Autoryzuj konto Plex. Użytkownicy Home staną się profilami.';
@override String get connectToJellyfinCard => 'Połącz z Jellyfin';
@override String get connectToJellyfinCardSubtitle => 'Wpisz URL serwera, nazwę użytkownika i hasło.';
@override String connectToJellyfinCardSubtitleScoped({required Object name}) => 'Zaloguj się do serwera Jellyfin. Powiązane z ${name}.';
@override String get borrowFromAnotherProfile => 'Pożycz z innego profilu'; @override String get borrowFromAnotherProfile => 'Pożycz z innego profilu';
@override String get borrowFromAnotherProfileSubtitle => 'Użyj połączenia innego profilu. Profile chronione PIN-em wymagają podania PIN-u.'; @override String get borrowFromAnotherProfileSubtitle => 'Użyj połączenia innego profilu. Profile chronione PIN-em wymagają podania PIN-u.';
} }
@@ -2214,7 +2204,6 @@ extension on TranslationsPl {
'auth.waitingForAuth' => 'Oczekiwanie na uwierzytelnienie...\nZaloguj się w przeglądarce.', 'auth.waitingForAuth' => 'Oczekiwanie na uwierzytelnienie...\nZaloguj się w przeglądarce.',
'auth.useBrowser' => 'Użyj przeglądarki', 'auth.useBrowser' => 'Użyj przeglądarki',
'auth.or' => 'lub', 'auth.or' => 'lub',
'auth.connectToJellyfin' => 'Połącz z Jellyfin',
'auth.useQuickConnect' => 'Użyj Quick Connect', 'auth.useQuickConnect' => 'Użyj Quick Connect',
'auth.quickConnectInstructions' => 'Otwórz Quick Connect w Jellyfin i wpisz ten kod.', 'auth.quickConnectInstructions' => 'Otwórz Quick Connect w Jellyfin i wpisz ten kod.',
'auth.quickConnectWaiting' => 'Oczekiwanie na zatwierdzenie…', 'auth.quickConnectWaiting' => 'Oczekiwanie na zatwierdzenie…',
@@ -2717,9 +2706,9 @@ extension on TranslationsPl {
'videoControls.searchSubtitles' => 'Szukaj napisów', 'videoControls.searchSubtitles' => 'Szukaj napisów',
'videoControls.language' => 'Język', 'videoControls.language' => 'Język',
'videoControls.noSubtitlesFound' => 'Nie znaleziono napisów', 'videoControls.noSubtitlesFound' => 'Nie znaleziono napisów',
'videoControls.subtitleDownloaded' => 'Napisy pobrane',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'videoControls.subtitleDownloaded' => 'Napisy pobrane',
'videoControls.subtitleDownloadedNotApplied' => 'Napisy zostały pobrane, ale nie można ich było wybrać', 'videoControls.subtitleDownloadedNotApplied' => 'Napisy zostały pobrane, ale nie można ich było wybrać',
'videoControls.subtitleDownloadFailed' => 'Nie udało się pobrać napisów', 'videoControls.subtitleDownloadFailed' => 'Nie udało się pobrać napisów',
'videoControls.searchLanguages' => 'Szukaj języków...', 'videoControls.searchLanguages' => 'Szukaj języków...',
@@ -2875,8 +2864,6 @@ extension on TranslationsPl {
'connections.sessionExpiredOne' => ({required Object name}) => 'Sesja wygasła dla ${name}', 'connections.sessionExpiredOne' => ({required Object name}) => 'Sesja wygasła dla ${name}',
'connections.sessionExpiredMany' => ({required Object count}) => 'Sesja wygasła dla ${count} serwerów', 'connections.sessionExpiredMany' => ({required Object count}) => 'Sesja wygasła dla ${count} serwerów',
'connections.signInAgain' => 'Zaloguj się ponownie', 'connections.signInAgain' => 'Zaloguj się ponownie',
'connections.editJellyfinTitle' => 'Edytuj połączenie Jellyfin',
'connections.editJellyfinIntro' => ({required Object serverName}) => 'Dodaj lub usuń adresy URL dla ${serverName}. Plezy użyje osiągalnego URL-a o najniższym opóźnieniu.',
'discover.title' => 'Odkryj', 'discover.title' => 'Odkryj',
'discover.noContentAvailable' => 'Brak dostępnych treści', 'discover.noContentAvailable' => 'Brak dostępnych treści',
'discover.addMediaToLibraries' => 'Dodaj multimedia do swoich bibliotek', 'discover.addMediaToLibraries' => 'Dodaj multimedia do swoich bibliotek',
@@ -3231,11 +3218,11 @@ extension on TranslationsPl {
'watchTogether.failedToCreate' => 'Nie udało się utworzyć sesji', 'watchTogether.failedToCreate' => 'Nie udało się utworzyć sesji',
'watchTogether.failedToJoin' => 'Nie udało się dołączyć do sesji', 'watchTogether.failedToJoin' => 'Nie udało się dołączyć do sesji',
'watchTogether.sessionCodeCopied' => 'Kod sesji skopiowany do schowka', 'watchTogether.sessionCodeCopied' => 'Kod sesji skopiowany do schowka',
_ => null,
} ?? switch (path) {
'watchTogether.relayUnreachable' => 'Serwer pośredniczący jest nieosiągalny. Blokada operatora internetowego może uniemożliwiać korzystanie z funkcji „Oglądaj razem”.', 'watchTogether.relayUnreachable' => 'Serwer pośredniczący jest nieosiągalny. Blokada operatora internetowego może uniemożliwiać korzystanie z funkcji „Oglądaj razem”.',
'watchTogether.reconnectingToHost' => 'Ponowne łączenie z gospodarzem...', 'watchTogether.reconnectingToHost' => 'Ponowne łączenie z gospodarzem...',
'watchTogether.currentPlayback' => 'Bieżące odtwarzanie', 'watchTogether.currentPlayback' => 'Bieżące odtwarzanie',
_ => null,
} ?? switch (path) {
'watchTogether.joinCurrentPlayback' => 'Dołącz do bieżącego odtwarzania', 'watchTogether.joinCurrentPlayback' => 'Dołącz do bieżącego odtwarzania',
'watchTogether.joinCurrentPlaybackDescription' => 'Wróć do treści oglądanej obecnie przez gospodarza', 'watchTogether.joinCurrentPlaybackDescription' => 'Wróć do treści oglądanej obecnie przez gospodarza',
'watchTogether.failedToOpenCurrentPlayback' => 'Nie udało się otworzyć bieżącego odtwarzania', 'watchTogether.failedToOpenCurrentPlayback' => 'Nie udało się otworzyć bieżącego odtwarzania',
@@ -3662,12 +3649,9 @@ extension on TranslationsPl {
'services.libraryFilter.modeHintWhitelist' => 'Synchronizuj tylko biblioteki zaznaczone poniżej.', 'services.libraryFilter.modeHintWhitelist' => 'Synchronizuj tylko biblioteki zaznaczone poniżej.',
'services.libraryFilter.libraries' => 'Biblioteki', 'services.libraryFilter.libraries' => 'Biblioteki',
'services.libraryFilter.noLibraries' => 'Brak dostępnych bibliotek', 'services.libraryFilter.noLibraries' => 'Brak dostępnych bibliotek',
'addServer.addJellyfinTitle' => 'Dodaj serwer Jellyfin',
'addServer.serverUrls' => 'Adresy URL serwera', 'addServer.serverUrls' => 'Adresy URL serwera',
'addServer.serverUrlsHelper' => 'Można podać wiele adresów URL rozdzielonych przecinkami.', 'addServer.serverUrlsHelper' => 'Można podać wiele adresów URL rozdzielonych przecinkami.',
'addServer.findServer' => 'Znajdź serwer', 'addServer.findServer' => 'Znajdź serwer',
'addServer.searchingLocalServers' => 'Szukanie lokalnych serwerów Jellyfin...',
'addServer.localServers' => 'Lokalne serwery Jellyfin',
'addServer.username' => 'Nazwa użytkownika', 'addServer.username' => 'Nazwa użytkownika',
'addServer.password' => 'Hasło', 'addServer.password' => 'Hasło',
'addServer.signIn' => 'Zaloguj się', 'addServer.signIn' => 'Zaloguj się',
@@ -3679,15 +3663,11 @@ extension on TranslationsPl {
'addServer.addPlexTitle' => 'Zaloguj się przez Plex', 'addServer.addPlexTitle' => 'Zaloguj się przez Plex',
'addServer.pinExpired' => 'PIN wygasł przed zalogowaniem. Spróbuj ponownie.', 'addServer.pinExpired' => 'PIN wygasł przed zalogowaniem. Spróbuj ponownie.',
'addServer.failedToRegisterAccount' => ({required Object error}) => 'Nie udało się zarejestrować konta: ${error}', 'addServer.failedToRegisterAccount' => ({required Object error}) => 'Nie udało się zarejestrować konta: ${error}',
'addServer.enterJellyfinUrlError' => 'Podaj URL serwera Jellyfin',
'addServer.addConnectionTitle' => 'Dodaj połączenie', 'addServer.addConnectionTitle' => 'Dodaj połączenie',
'addServer.addConnectionTitleScoped' => ({required Object name}) => 'Dodaj do ${name}', 'addServer.addConnectionTitleScoped' => ({required Object name}) => 'Dodaj do ${name}',
'addServer.signInWithPlexCard' => 'Zaloguj się przez Plex', 'addServer.signInWithPlexCard' => 'Zaloguj się przez Plex',
'addServer.signInWithPlexCardSubtitle' => 'Autoryzuj to urządzenie. Serwery udostępnione zostaną dodane.', 'addServer.signInWithPlexCardSubtitle' => 'Autoryzuj to urządzenie. Serwery udostępnione zostaną dodane.',
'addServer.signInWithPlexCardSubtitleScoped' => 'Autoryzuj konto Plex. Użytkownicy Home staną się profilami.', 'addServer.signInWithPlexCardSubtitleScoped' => 'Autoryzuj konto Plex. Użytkownicy Home staną się profilami.',
'addServer.connectToJellyfinCard' => 'Połącz z Jellyfin',
'addServer.connectToJellyfinCardSubtitle' => 'Wpisz URL serwera, nazwę użytkownika i hasło.',
'addServer.connectToJellyfinCardSubtitleScoped' => ({required Object name}) => 'Zaloguj się do serwera Jellyfin. Powiązane z ${name}.',
'addServer.borrowFromAnotherProfile' => 'Pożycz z innego profilu', 'addServer.borrowFromAnotherProfile' => 'Pożycz z innego profilu',
'addServer.borrowFromAnotherProfileSubtitle' => 'Użyj połączenia innego profilu. Profile chronione PIN-em wymagają podania PIN-u.', 'addServer.borrowFromAnotherProfileSubtitle' => 'Użyj połączenia innego profilu. Profile chronione PIN-em wymagają podania PIN-u.',
_ => null, _ => null,
+3 -23
View File
@@ -115,7 +115,6 @@ class _Translations$auth$pt extends Translations$auth$en {
@override String get waitingForAuth => 'Aguardando autenticação...\nEntre pelo navegador.'; @override String get waitingForAuth => 'Aguardando autenticação...\nEntre pelo navegador.';
@override String get useBrowser => 'Usar navegador'; @override String get useBrowser => 'Usar navegador';
@override String get or => 'ou'; @override String get or => 'ou';
@override String get connectToJellyfin => 'Conectar ao Jellyfin';
@override String get useQuickConnect => 'Usar Quick Connect'; @override String get useQuickConnect => 'Usar Quick Connect';
@override String get quickConnectInstructions => 'Abra o Quick Connect no Jellyfin e insira este código.'; @override String get quickConnectInstructions => 'Abra o Quick Connect no Jellyfin e insira este código.';
@override String get quickConnectWaiting => 'Aguardando aprovação…'; @override String get quickConnectWaiting => 'Aguardando aprovação…';
@@ -918,8 +917,6 @@ class _Translations$connections$pt extends Translations$connections$en {
@override String sessionExpiredOne({required Object name}) => 'Sessão de ${name} expirada'; @override String sessionExpiredOne({required Object name}) => 'Sessão de ${name} expirada';
@override String sessionExpiredMany({required Object count}) => 'Sessões expiradas em ${count} servidores'; @override String sessionExpiredMany({required Object count}) => 'Sessões expiradas em ${count} servidores';
@override String get signInAgain => 'Entrar novamente'; @override String get signInAgain => 'Entrar novamente';
@override String get editJellyfinTitle => 'Editar conexão Jellyfin';
@override String editJellyfinIntro({required Object serverName}) => 'Adicione ou remova URLs de ${serverName}. O Plezy usará a URL acessível com a menor latência.';
} }
// Path: discover // Path: discover
@@ -1803,12 +1800,9 @@ class _Translations$addServer$pt extends Translations$addServer$en {
final TranslationsPt _root; // ignore: unused_field final TranslationsPt _root; // ignore: unused_field
// Translations // Translations
@override String get addJellyfinTitle => 'Adicionar servidor Jellyfin';
@override String get serverUrls => 'URLs do servidor'; @override String get serverUrls => 'URLs do servidor';
@override String get serverUrlsHelper => 'Várias URLs são permitidas, separadas por vírgulas.'; @override String get serverUrlsHelper => 'Várias URLs são permitidas, separadas por vírgulas.';
@override String get findServer => 'Encontrar servidor'; @override String get findServer => 'Encontrar servidor';
@override String get searchingLocalServers => 'Procurando servidores Jellyfin locais...';
@override String get localServers => 'Servidores Jellyfin locais';
@override String get username => 'Usuário'; @override String get username => 'Usuário';
@override String get password => 'Senha'; @override String get password => 'Senha';
@override String get signIn => 'Entrar'; @override String get signIn => 'Entrar';
@@ -1820,15 +1814,11 @@ class _Translations$addServer$pt extends Translations$addServer$en {
@override String get addPlexTitle => 'Entrar com Plex'; @override String get addPlexTitle => 'Entrar com Plex';
@override String get pinExpired => 'O PIN expirou antes de entrar. Tente novamente.'; @override String get pinExpired => 'O PIN expirou antes de entrar. Tente novamente.';
@override String failedToRegisterAccount({required Object error}) => 'Falha ao registrar a conta: ${error}'; @override String failedToRegisterAccount({required Object error}) => 'Falha ao registrar a conta: ${error}';
@override String get enterJellyfinUrlError => 'Insira a URL do seu servidor Jellyfin';
@override String get addConnectionTitle => 'Adicionar conexão'; @override String get addConnectionTitle => 'Adicionar conexão';
@override String addConnectionTitleScoped({required Object name}) => 'Adicionar a ${name}'; @override String addConnectionTitleScoped({required Object name}) => 'Adicionar a ${name}';
@override String get signInWithPlexCard => 'Entrar com Plex'; @override String get signInWithPlexCard => 'Entrar com Plex';
@override String get signInWithPlexCardSubtitle => 'Autorize este dispositivo. Servidores compartilhados são adicionados.'; @override String get signInWithPlexCardSubtitle => 'Autorize este dispositivo. Servidores compartilhados são adicionados.';
@override String get signInWithPlexCardSubtitleScoped => 'Autorize uma conta Plex. Os usuários do Plex Home se tornam perfis.'; @override String get signInWithPlexCardSubtitleScoped => 'Autorize uma conta Plex. Os usuários do Plex Home se tornam perfis.';
@override String get connectToJellyfinCard => 'Conectar ao Jellyfin';
@override String get connectToJellyfinCardSubtitle => 'Insira URL do servidor, usuário e senha.';
@override String connectToJellyfinCardSubtitleScoped({required Object name}) => 'Entre em um servidor Jellyfin. A conexão será vinculada a ${name}.';
@override String get borrowFromAnotherProfile => 'Pegar emprestado de outro perfil'; @override String get borrowFromAnotherProfile => 'Pegar emprestado de outro perfil';
@override String get borrowFromAnotherProfileSubtitle => 'Reutilize a conexão de outro perfil. Perfis protegidos por PIN exigem PIN.'; @override String get borrowFromAnotherProfileSubtitle => 'Reutilize a conexão de outro perfil. Perfis protegidos por PIN exigem PIN.';
} }
@@ -2208,7 +2198,6 @@ extension on TranslationsPt {
'auth.waitingForAuth' => 'Aguardando autenticação...\nEntre pelo navegador.', 'auth.waitingForAuth' => 'Aguardando autenticação...\nEntre pelo navegador.',
'auth.useBrowser' => 'Usar navegador', 'auth.useBrowser' => 'Usar navegador',
'auth.or' => 'ou', 'auth.or' => 'ou',
'auth.connectToJellyfin' => 'Conectar ao Jellyfin',
'auth.useQuickConnect' => 'Usar Quick Connect', 'auth.useQuickConnect' => 'Usar Quick Connect',
'auth.quickConnectInstructions' => 'Abra o Quick Connect no Jellyfin e insira este código.', 'auth.quickConnectInstructions' => 'Abra o Quick Connect no Jellyfin e insira este código.',
'auth.quickConnectWaiting' => 'Aguardando aprovação…', 'auth.quickConnectWaiting' => 'Aguardando aprovação…',
@@ -2711,9 +2700,9 @@ extension on TranslationsPt {
'videoControls.searchSubtitles' => 'Pesquisar legendas', 'videoControls.searchSubtitles' => 'Pesquisar legendas',
'videoControls.language' => 'Idioma', 'videoControls.language' => 'Idioma',
'videoControls.noSubtitlesFound' => 'Nenhuma legenda encontrada', 'videoControls.noSubtitlesFound' => 'Nenhuma legenda encontrada',
'videoControls.subtitleDownloaded' => 'Legenda baixada',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'videoControls.subtitleDownloaded' => 'Legenda baixada',
'videoControls.subtitleDownloadedNotApplied' => 'A legenda foi baixada, mas não foi possível selecioná-la', 'videoControls.subtitleDownloadedNotApplied' => 'A legenda foi baixada, mas não foi possível selecioná-la',
'videoControls.subtitleDownloadFailed' => 'Falha ao baixar legenda', 'videoControls.subtitleDownloadFailed' => 'Falha ao baixar legenda',
'videoControls.searchLanguages' => 'Pesquisar idiomas...', 'videoControls.searchLanguages' => 'Pesquisar idiomas...',
@@ -2869,8 +2858,6 @@ extension on TranslationsPt {
'connections.sessionExpiredOne' => ({required Object name}) => 'Sessão de ${name} expirada', 'connections.sessionExpiredOne' => ({required Object name}) => 'Sessão de ${name} expirada',
'connections.sessionExpiredMany' => ({required Object count}) => 'Sessões expiradas em ${count} servidores', 'connections.sessionExpiredMany' => ({required Object count}) => 'Sessões expiradas em ${count} servidores',
'connections.signInAgain' => 'Entrar novamente', 'connections.signInAgain' => 'Entrar novamente',
'connections.editJellyfinTitle' => 'Editar conexão Jellyfin',
'connections.editJellyfinIntro' => ({required Object serverName}) => 'Adicione ou remova URLs de ${serverName}. O Plezy usará a URL acessível com a menor latência.',
'discover.title' => 'Descobrir', 'discover.title' => 'Descobrir',
'discover.noContentAvailable' => 'Nenhum conteúdo disponível', 'discover.noContentAvailable' => 'Nenhum conteúdo disponível',
'discover.addMediaToLibraries' => 'Adicione mídias às suas bibliotecas', 'discover.addMediaToLibraries' => 'Adicione mídias às suas bibliotecas',
@@ -3225,11 +3212,11 @@ extension on TranslationsPt {
'watchTogether.failedToCreate' => 'Falha ao criar sessão', 'watchTogether.failedToCreate' => 'Falha ao criar sessão',
'watchTogether.failedToJoin' => 'Falha ao entrar na sessão', 'watchTogether.failedToJoin' => 'Falha ao entrar na sessão',
'watchTogether.sessionCodeCopied' => 'Código da sessão copiado para a área de transferência', 'watchTogether.sessionCodeCopied' => 'Código da sessão copiado para a área de transferência',
_ => null,
} ?? switch (path) {
'watchTogether.relayUnreachable' => 'Servidor de retransmissão inacessível. O bloqueio pelo provedor de internet pode impedir o uso do Assistir Juntos.', 'watchTogether.relayUnreachable' => 'Servidor de retransmissão inacessível. O bloqueio pelo provedor de internet pode impedir o uso do Assistir Juntos.',
'watchTogether.reconnectingToHost' => 'Reconectando ao anfitrião...', 'watchTogether.reconnectingToHost' => 'Reconectando ao anfitrião...',
'watchTogether.currentPlayback' => 'Reprodução atual', 'watchTogether.currentPlayback' => 'Reprodução atual',
_ => null,
} ?? switch (path) {
'watchTogether.joinCurrentPlayback' => 'Entrar na reprodução atual', 'watchTogether.joinCurrentPlayback' => 'Entrar na reprodução atual',
'watchTogether.joinCurrentPlaybackDescription' => 'Voltar ao conteúdo que o anfitrião está assistindo agora', 'watchTogether.joinCurrentPlaybackDescription' => 'Voltar ao conteúdo que o anfitrião está assistindo agora',
'watchTogether.failedToOpenCurrentPlayback' => 'Falha ao abrir a reprodução atual', 'watchTogether.failedToOpenCurrentPlayback' => 'Falha ao abrir a reprodução atual',
@@ -3656,12 +3643,9 @@ extension on TranslationsPt {
'services.libraryFilter.modeHintWhitelist' => 'Sincronizar apenas as bibliotecas marcadas abaixo.', 'services.libraryFilter.modeHintWhitelist' => 'Sincronizar apenas as bibliotecas marcadas abaixo.',
'services.libraryFilter.libraries' => 'Bibliotecas', 'services.libraryFilter.libraries' => 'Bibliotecas',
'services.libraryFilter.noLibraries' => 'Nenhuma biblioteca disponível', 'services.libraryFilter.noLibraries' => 'Nenhuma biblioteca disponível',
'addServer.addJellyfinTitle' => 'Adicionar servidor Jellyfin',
'addServer.serverUrls' => 'URLs do servidor', 'addServer.serverUrls' => 'URLs do servidor',
'addServer.serverUrlsHelper' => 'Várias URLs são permitidas, separadas por vírgulas.', 'addServer.serverUrlsHelper' => 'Várias URLs são permitidas, separadas por vírgulas.',
'addServer.findServer' => 'Encontrar servidor', 'addServer.findServer' => 'Encontrar servidor',
'addServer.searchingLocalServers' => 'Procurando servidores Jellyfin locais...',
'addServer.localServers' => 'Servidores Jellyfin locais',
'addServer.username' => 'Usuário', 'addServer.username' => 'Usuário',
'addServer.password' => 'Senha', 'addServer.password' => 'Senha',
'addServer.signIn' => 'Entrar', 'addServer.signIn' => 'Entrar',
@@ -3673,15 +3657,11 @@ extension on TranslationsPt {
'addServer.addPlexTitle' => 'Entrar com Plex', 'addServer.addPlexTitle' => 'Entrar com Plex',
'addServer.pinExpired' => 'O PIN expirou antes de entrar. Tente novamente.', 'addServer.pinExpired' => 'O PIN expirou antes de entrar. Tente novamente.',
'addServer.failedToRegisterAccount' => ({required Object error}) => 'Falha ao registrar a conta: ${error}', 'addServer.failedToRegisterAccount' => ({required Object error}) => 'Falha ao registrar a conta: ${error}',
'addServer.enterJellyfinUrlError' => 'Insira a URL do seu servidor Jellyfin',
'addServer.addConnectionTitle' => 'Adicionar conexão', 'addServer.addConnectionTitle' => 'Adicionar conexão',
'addServer.addConnectionTitleScoped' => ({required Object name}) => 'Adicionar a ${name}', 'addServer.addConnectionTitleScoped' => ({required Object name}) => 'Adicionar a ${name}',
'addServer.signInWithPlexCard' => 'Entrar com Plex', 'addServer.signInWithPlexCard' => 'Entrar com Plex',
'addServer.signInWithPlexCardSubtitle' => 'Autorize este dispositivo. Servidores compartilhados são adicionados.', 'addServer.signInWithPlexCardSubtitle' => 'Autorize este dispositivo. Servidores compartilhados são adicionados.',
'addServer.signInWithPlexCardSubtitleScoped' => 'Autorize uma conta Plex. Os usuários do Plex Home se tornam perfis.', 'addServer.signInWithPlexCardSubtitleScoped' => 'Autorize uma conta Plex. Os usuários do Plex Home se tornam perfis.',
'addServer.connectToJellyfinCard' => 'Conectar ao Jellyfin',
'addServer.connectToJellyfinCardSubtitle' => 'Insira URL do servidor, usuário e senha.',
'addServer.connectToJellyfinCardSubtitleScoped' => ({required Object name}) => 'Entre em um servidor Jellyfin. A conexão será vinculada a ${name}.',
'addServer.borrowFromAnotherProfile' => 'Pegar emprestado de outro perfil', 'addServer.borrowFromAnotherProfile' => 'Pegar emprestado de outro perfil',
'addServer.borrowFromAnotherProfileSubtitle' => 'Reutilize a conexão de outro perfil. Perfis protegidos por PIN exigem PIN.', 'addServer.borrowFromAnotherProfileSubtitle' => 'Reutilize a conexão de outro perfil. Perfis protegidos por PIN exigem PIN.',
_ => null, _ => null,
+3 -23
View File
@@ -115,7 +115,6 @@ class _Translations$auth$ru extends Translations$auth$en {
@override String get waitingForAuth => 'Ожидание аутентификации...\nВыполните вход в браузере.'; @override String get waitingForAuth => 'Ожидание аутентификации...\nВыполните вход в браузере.';
@override String get useBrowser => 'Использовать браузер'; @override String get useBrowser => 'Использовать браузер';
@override String get or => 'или'; @override String get or => 'или';
@override String get connectToJellyfin => 'Подключиться к Jellyfin';
@override String get useQuickConnect => 'Использовать Quick Connect'; @override String get useQuickConnect => 'Использовать Quick Connect';
@override String get quickConnectInstructions => 'Откройте Quick Connect в Jellyfin и введите этот код.'; @override String get quickConnectInstructions => 'Откройте Quick Connect в Jellyfin и введите этот код.';
@override String get quickConnectWaiting => 'Ожидание подтверждения…'; @override String get quickConnectWaiting => 'Ожидание подтверждения…';
@@ -920,8 +919,6 @@ class _Translations$connections$ru extends Translations$connections$en {
@override String sessionExpiredOne({required Object name}) => 'Сессия истекла для ${name}'; @override String sessionExpiredOne({required Object name}) => 'Сессия истекла для ${name}';
@override String sessionExpiredMany({required Object count}) => 'Сессия истекла для ${count} серверов'; @override String sessionExpiredMany({required Object count}) => 'Сессия истекла для ${count} серверов';
@override String get signInAgain => 'Войти снова'; @override String get signInAgain => 'Войти снова';
@override String get editJellyfinTitle => 'Изменить подключение Jellyfin';
@override String editJellyfinIntro({required Object serverName}) => 'Добавьте или удалите URL для ${serverName}. Plezy будет использовать доступный URL с минимальной задержкой.';
} }
// Path: discover // Path: discover
@@ -1809,12 +1806,9 @@ class _Translations$addServer$ru extends Translations$addServer$en {
final TranslationsRu _root; // ignore: unused_field final TranslationsRu _root; // ignore: unused_field
// Translations // Translations
@override String get addJellyfinTitle => 'Добавить сервер Jellyfin';
@override String get serverUrls => 'URL-адреса сервера'; @override String get serverUrls => 'URL-адреса сервера';
@override String get serverUrlsHelper => 'Можно указать несколько URL через запятую.'; @override String get serverUrlsHelper => 'Можно указать несколько URL через запятую.';
@override String get findServer => 'Найти сервер'; @override String get findServer => 'Найти сервер';
@override String get searchingLocalServers => 'Поиск локальных серверов Jellyfin...';
@override String get localServers => 'Локальные серверы Jellyfin';
@override String get username => 'Имя пользователя'; @override String get username => 'Имя пользователя';
@override String get password => 'Пароль'; @override String get password => 'Пароль';
@override String get signIn => 'Войти'; @override String get signIn => 'Войти';
@@ -1826,15 +1820,11 @@ class _Translations$addServer$ru extends Translations$addServer$en {
@override String get addPlexTitle => 'Войти через Plex'; @override String get addPlexTitle => 'Войти через Plex';
@override String get pinExpired => 'Срок действия PIN истёк до входа. Попробуйте снова.'; @override String get pinExpired => 'Срок действия PIN истёк до входа. Попробуйте снова.';
@override String failedToRegisterAccount({required Object error}) => 'Не удалось зарегистрировать учётную запись: ${error}'; @override String failedToRegisterAccount({required Object error}) => 'Не удалось зарегистрировать учётную запись: ${error}';
@override String get enterJellyfinUrlError => 'Введите URL вашего сервера Jellyfin';
@override String get addConnectionTitle => 'Добавить подключение'; @override String get addConnectionTitle => 'Добавить подключение';
@override String addConnectionTitleScoped({required Object name}) => 'Добавить в ${name}'; @override String addConnectionTitleScoped({required Object name}) => 'Добавить в ${name}';
@override String get signInWithPlexCard => 'Войти через Plex'; @override String get signInWithPlexCard => 'Войти через Plex';
@override String get signInWithPlexCardSubtitle => 'Авторизуйте это устройство. Общие серверы будут добавлены.'; @override String get signInWithPlexCardSubtitle => 'Авторизуйте это устройство. Общие серверы будут добавлены.';
@override String get signInWithPlexCardSubtitleScoped => 'Авторизуйте аккаунт Plex. Пользователи Home станут профилями.'; @override String get signInWithPlexCardSubtitleScoped => 'Авторизуйте аккаунт Plex. Пользователи Home станут профилями.';
@override String get connectToJellyfinCard => 'Подключиться к Jellyfin';
@override String get connectToJellyfinCardSubtitle => 'Введите URL сервера, имя пользователя и пароль.';
@override String connectToJellyfinCardSubtitleScoped({required Object name}) => 'Войдите на сервер Jellyfin. Привязывается к ${name}.';
@override String get borrowFromAnotherProfile => 'Использовать подключение другого профиля'; @override String get borrowFromAnotherProfile => 'Использовать подключение другого профиля';
@override String get borrowFromAnotherProfileSubtitle => 'Повторно используйте подключение другого профиля. Для защищённых профилей потребуется PIN.'; @override String get borrowFromAnotherProfileSubtitle => 'Повторно используйте подключение другого профиля. Для защищённых профилей потребуется PIN.';
} }
@@ -2214,7 +2204,6 @@ extension on TranslationsRu {
'auth.waitingForAuth' => 'Ожидание аутентификации...\nВыполните вход в браузере.', 'auth.waitingForAuth' => 'Ожидание аутентификации...\nВыполните вход в браузере.',
'auth.useBrowser' => 'Использовать браузер', 'auth.useBrowser' => 'Использовать браузер',
'auth.or' => 'или', 'auth.or' => 'или',
'auth.connectToJellyfin' => 'Подключиться к Jellyfin',
'auth.useQuickConnect' => 'Использовать Quick Connect', 'auth.useQuickConnect' => 'Использовать Quick Connect',
'auth.quickConnectInstructions' => 'Откройте Quick Connect в Jellyfin и введите этот код.', 'auth.quickConnectInstructions' => 'Откройте Quick Connect в Jellyfin и введите этот код.',
'auth.quickConnectWaiting' => 'Ожидание подтверждения…', 'auth.quickConnectWaiting' => 'Ожидание подтверждения…',
@@ -2717,9 +2706,9 @@ extension on TranslationsRu {
'videoControls.searchSubtitles' => 'Поиск субтитров', 'videoControls.searchSubtitles' => 'Поиск субтитров',
'videoControls.language' => 'Язык', 'videoControls.language' => 'Язык',
'videoControls.noSubtitlesFound' => 'Субтитры не найдены', 'videoControls.noSubtitlesFound' => 'Субтитры не найдены',
'videoControls.subtitleDownloaded' => 'Субтитры загружены',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'videoControls.subtitleDownloaded' => 'Субтитры загружены',
'videoControls.subtitleDownloadedNotApplied' => 'Субтитры загружены, но их не удалось выбрать', 'videoControls.subtitleDownloadedNotApplied' => 'Субтитры загружены, но их не удалось выбрать',
'videoControls.subtitleDownloadFailed' => 'Не удалось загрузить субтитры', 'videoControls.subtitleDownloadFailed' => 'Не удалось загрузить субтитры',
'videoControls.searchLanguages' => 'Поиск языков...', 'videoControls.searchLanguages' => 'Поиск языков...',
@@ -2875,8 +2864,6 @@ extension on TranslationsRu {
'connections.sessionExpiredOne' => ({required Object name}) => 'Сессия истекла для ${name}', 'connections.sessionExpiredOne' => ({required Object name}) => 'Сессия истекла для ${name}',
'connections.sessionExpiredMany' => ({required Object count}) => 'Сессия истекла для ${count} серверов', 'connections.sessionExpiredMany' => ({required Object count}) => 'Сессия истекла для ${count} серверов',
'connections.signInAgain' => 'Войти снова', 'connections.signInAgain' => 'Войти снова',
'connections.editJellyfinTitle' => 'Изменить подключение Jellyfin',
'connections.editJellyfinIntro' => ({required Object serverName}) => 'Добавьте или удалите URL для ${serverName}. Plezy будет использовать доступный URL с минимальной задержкой.',
'discover.title' => 'Обзор', 'discover.title' => 'Обзор',
'discover.noContentAvailable' => 'Контент недоступен', 'discover.noContentAvailable' => 'Контент недоступен',
'discover.addMediaToLibraries' => 'Добавьте медиафайлы в ваши библиотеки', 'discover.addMediaToLibraries' => 'Добавьте медиафайлы в ваши библиотеки',
@@ -3231,11 +3218,11 @@ extension on TranslationsRu {
'watchTogether.failedToCreate' => 'Не удалось создать сессию', 'watchTogether.failedToCreate' => 'Не удалось создать сессию',
'watchTogether.failedToJoin' => 'Не удалось присоединиться к сессии', 'watchTogether.failedToJoin' => 'Не удалось присоединиться к сессии',
'watchTogether.sessionCodeCopied' => 'Код сессии скопирован в буфер обмена', 'watchTogether.sessionCodeCopied' => 'Код сессии скопирован в буфер обмена',
_ => null,
} ?? switch (path) {
'watchTogether.relayUnreachable' => 'Сервер ретрансляции недоступен. Блокировка интернет-провайдером может помешать совместному просмотру.', 'watchTogether.relayUnreachable' => 'Сервер ретрансляции недоступен. Блокировка интернет-провайдером может помешать совместному просмотру.',
'watchTogether.reconnectingToHost' => 'Повторное подключение к организатору...', 'watchTogether.reconnectingToHost' => 'Повторное подключение к организатору...',
'watchTogether.currentPlayback' => 'Текущее воспроизведение', 'watchTogether.currentPlayback' => 'Текущее воспроизведение',
_ => null,
} ?? switch (path) {
'watchTogether.joinCurrentPlayback' => 'Присоединиться к текущему воспроизведению', 'watchTogether.joinCurrentPlayback' => 'Присоединиться к текущему воспроизведению',
'watchTogether.joinCurrentPlaybackDescription' => 'Вернуться к материалу, который сейчас смотрит организатор', 'watchTogether.joinCurrentPlaybackDescription' => 'Вернуться к материалу, который сейчас смотрит организатор',
'watchTogether.failedToOpenCurrentPlayback' => 'Не удалось открыть текущее воспроизведение', 'watchTogether.failedToOpenCurrentPlayback' => 'Не удалось открыть текущее воспроизведение',
@@ -3662,12 +3649,9 @@ extension on TranslationsRu {
'services.libraryFilter.modeHintWhitelist' => 'Синхронизировать только библиотеки, отмеченные ниже.', 'services.libraryFilter.modeHintWhitelist' => 'Синхронизировать только библиотеки, отмеченные ниже.',
'services.libraryFilter.libraries' => 'Библиотеки', 'services.libraryFilter.libraries' => 'Библиотеки',
'services.libraryFilter.noLibraries' => 'Библиотеки недоступны', 'services.libraryFilter.noLibraries' => 'Библиотеки недоступны',
'addServer.addJellyfinTitle' => 'Добавить сервер Jellyfin',
'addServer.serverUrls' => 'URL-адреса сервера', 'addServer.serverUrls' => 'URL-адреса сервера',
'addServer.serverUrlsHelper' => 'Можно указать несколько URL через запятую.', 'addServer.serverUrlsHelper' => 'Можно указать несколько URL через запятую.',
'addServer.findServer' => 'Найти сервер', 'addServer.findServer' => 'Найти сервер',
'addServer.searchingLocalServers' => 'Поиск локальных серверов Jellyfin...',
'addServer.localServers' => 'Локальные серверы Jellyfin',
'addServer.username' => 'Имя пользователя', 'addServer.username' => 'Имя пользователя',
'addServer.password' => 'Пароль', 'addServer.password' => 'Пароль',
'addServer.signIn' => 'Войти', 'addServer.signIn' => 'Войти',
@@ -3679,15 +3663,11 @@ extension on TranslationsRu {
'addServer.addPlexTitle' => 'Войти через Plex', 'addServer.addPlexTitle' => 'Войти через Plex',
'addServer.pinExpired' => 'Срок действия PIN истёк до входа. Попробуйте снова.', 'addServer.pinExpired' => 'Срок действия PIN истёк до входа. Попробуйте снова.',
'addServer.failedToRegisterAccount' => ({required Object error}) => 'Не удалось зарегистрировать учётную запись: ${error}', 'addServer.failedToRegisterAccount' => ({required Object error}) => 'Не удалось зарегистрировать учётную запись: ${error}',
'addServer.enterJellyfinUrlError' => 'Введите URL вашего сервера Jellyfin',
'addServer.addConnectionTitle' => 'Добавить подключение', 'addServer.addConnectionTitle' => 'Добавить подключение',
'addServer.addConnectionTitleScoped' => ({required Object name}) => 'Добавить в ${name}', 'addServer.addConnectionTitleScoped' => ({required Object name}) => 'Добавить в ${name}',
'addServer.signInWithPlexCard' => 'Войти через Plex', 'addServer.signInWithPlexCard' => 'Войти через Plex',
'addServer.signInWithPlexCardSubtitle' => 'Авторизуйте это устройство. Общие серверы будут добавлены.', 'addServer.signInWithPlexCardSubtitle' => 'Авторизуйте это устройство. Общие серверы будут добавлены.',
'addServer.signInWithPlexCardSubtitleScoped' => 'Авторизуйте аккаунт Plex. Пользователи Home станут профилями.', 'addServer.signInWithPlexCardSubtitleScoped' => 'Авторизуйте аккаунт Plex. Пользователи Home станут профилями.',
'addServer.connectToJellyfinCard' => 'Подключиться к Jellyfin',
'addServer.connectToJellyfinCardSubtitle' => 'Введите URL сервера, имя пользователя и пароль.',
'addServer.connectToJellyfinCardSubtitleScoped' => ({required Object name}) => 'Войдите на сервер Jellyfin. Привязывается к ${name}.',
'addServer.borrowFromAnotherProfile' => 'Использовать подключение другого профиля', 'addServer.borrowFromAnotherProfile' => 'Использовать подключение другого профиля',
'addServer.borrowFromAnotherProfileSubtitle' => 'Повторно используйте подключение другого профиля. Для защищённых профилей потребуется PIN.', 'addServer.borrowFromAnotherProfileSubtitle' => 'Повторно используйте подключение другого профиля. Для защищённых профилей потребуется PIN.',
_ => null, _ => null,
+3 -23
View File
@@ -115,7 +115,6 @@ class _Translations$auth$sv extends Translations$auth$en {
@override String get waitingForAuth => 'Väntar på autentisering...\nLogga in från din webbläsare.'; @override String get waitingForAuth => 'Väntar på autentisering...\nLogga in från din webbläsare.';
@override String get useBrowser => 'Använd webbläsare'; @override String get useBrowser => 'Använd webbläsare';
@override String get or => 'eller'; @override String get or => 'eller';
@override String get connectToJellyfin => 'Anslut till Jellyfin';
@override String get useQuickConnect => 'Använd Quick Connect'; @override String get useQuickConnect => 'Använd Quick Connect';
@override String get quickConnectInstructions => 'Öppna Quick Connect i Jellyfin och ange den här koden.'; @override String get quickConnectInstructions => 'Öppna Quick Connect i Jellyfin och ange den här koden.';
@override String get quickConnectWaiting => 'Väntar på godkännande…'; @override String get quickConnectWaiting => 'Väntar på godkännande…';
@@ -918,8 +917,6 @@ class _Translations$connections$sv extends Translations$connections$en {
@override String sessionExpiredOne({required Object name}) => 'Sessionen har gått ut för ${name}'; @override String sessionExpiredOne({required Object name}) => 'Sessionen har gått ut för ${name}';
@override String sessionExpiredMany({required Object count}) => 'Sessionen har gått ut för ${count} servrar'; @override String sessionExpiredMany({required Object count}) => 'Sessionen har gått ut för ${count} servrar';
@override String get signInAgain => 'Logga in igen'; @override String get signInAgain => 'Logga in igen';
@override String get editJellyfinTitle => 'Redigera Jellyfin-anslutning';
@override String editJellyfinIntro({required Object serverName}) => 'Lägg till eller ta bort URL:er för ${serverName}. Plezy använder den nåbara URL som har lägst latens.';
} }
// Path: discover // Path: discover
@@ -1803,12 +1800,9 @@ class _Translations$addServer$sv extends Translations$addServer$en {
final TranslationsSv _root; // ignore: unused_field final TranslationsSv _root; // ignore: unused_field
// Translations // Translations
@override String get addJellyfinTitle => 'Lägg till Jellyfin-server';
@override String get serverUrls => 'Server-URL:er'; @override String get serverUrls => 'Server-URL:er';
@override String get serverUrlsHelper => 'Du kan ange flera URL:er avgränsade med kommatecken.'; @override String get serverUrlsHelper => 'Du kan ange flera URL:er avgränsade med kommatecken.';
@override String get findServer => 'Hitta server'; @override String get findServer => 'Hitta server';
@override String get searchingLocalServers => 'Söker efter lokala Jellyfin-servrar...';
@override String get localServers => 'Lokala Jellyfin-servrar';
@override String get username => 'Användarnamn'; @override String get username => 'Användarnamn';
@override String get password => 'Lösenord'; @override String get password => 'Lösenord';
@override String get signIn => 'Logga in'; @override String get signIn => 'Logga in';
@@ -1820,15 +1814,11 @@ class _Translations$addServer$sv extends Translations$addServer$en {
@override String get addPlexTitle => 'Logga in med Plex'; @override String get addPlexTitle => 'Logga in med Plex';
@override String get pinExpired => 'PIN-koden gick ut innan inloggning. Försök igen.'; @override String get pinExpired => 'PIN-koden gick ut innan inloggning. Försök igen.';
@override String failedToRegisterAccount({required Object error}) => 'Kunde inte registrera kontot: ${error}'; @override String failedToRegisterAccount({required Object error}) => 'Kunde inte registrera kontot: ${error}';
@override String get enterJellyfinUrlError => 'Ange URL till din Jellyfin-server';
@override String get addConnectionTitle => 'Lägg till anslutning'; @override String get addConnectionTitle => 'Lägg till anslutning';
@override String addConnectionTitleScoped({required Object name}) => 'Lägg till i ${name}'; @override String addConnectionTitleScoped({required Object name}) => 'Lägg till i ${name}';
@override String get signInWithPlexCard => 'Logga in med Plex'; @override String get signInWithPlexCard => 'Logga in med Plex';
@override String get signInWithPlexCardSubtitle => 'Auktorisera den här enheten. Delade servrar läggs till.'; @override String get signInWithPlexCardSubtitle => 'Auktorisera den här enheten. Delade servrar läggs till.';
@override String get signInWithPlexCardSubtitleScoped => 'Auktorisera ett Plex-konto. Home-användare blir profiler.'; @override String get signInWithPlexCardSubtitleScoped => 'Auktorisera ett Plex-konto. Home-användare blir profiler.';
@override String get connectToJellyfinCard => 'Anslut till Jellyfin';
@override String get connectToJellyfinCardSubtitle => 'Ange server-URL, användarnamn och lösenord.';
@override String connectToJellyfinCardSubtitleScoped({required Object name}) => 'Logga in på en Jellyfin-server. Kopplas till ${name}.';
@override String get borrowFromAnotherProfile => 'Låna från en annan profil'; @override String get borrowFromAnotherProfile => 'Låna från en annan profil';
@override String get borrowFromAnotherProfileSubtitle => 'Återanvänd en annan profils anslutning. PIN-skyddade profiler kräver en PIN.'; @override String get borrowFromAnotherProfileSubtitle => 'Återanvänd en annan profils anslutning. PIN-skyddade profiler kräver en PIN.';
} }
@@ -2208,7 +2198,6 @@ extension on TranslationsSv {
'auth.waitingForAuth' => 'Väntar på autentisering...\nLogga in från din webbläsare.', 'auth.waitingForAuth' => 'Väntar på autentisering...\nLogga in från din webbläsare.',
'auth.useBrowser' => 'Använd webbläsare', 'auth.useBrowser' => 'Använd webbläsare',
'auth.or' => 'eller', 'auth.or' => 'eller',
'auth.connectToJellyfin' => 'Anslut till Jellyfin',
'auth.useQuickConnect' => 'Använd Quick Connect', 'auth.useQuickConnect' => 'Använd Quick Connect',
'auth.quickConnectInstructions' => 'Öppna Quick Connect i Jellyfin och ange den här koden.', 'auth.quickConnectInstructions' => 'Öppna Quick Connect i Jellyfin och ange den här koden.',
'auth.quickConnectWaiting' => 'Väntar på godkännande…', 'auth.quickConnectWaiting' => 'Väntar på godkännande…',
@@ -2711,9 +2700,9 @@ extension on TranslationsSv {
'videoControls.searchSubtitles' => 'Sök undertexter', 'videoControls.searchSubtitles' => 'Sök undertexter',
'videoControls.language' => 'Språk', 'videoControls.language' => 'Språk',
'videoControls.noSubtitlesFound' => 'Inga undertexter hittades', 'videoControls.noSubtitlesFound' => 'Inga undertexter hittades',
'videoControls.subtitleDownloaded' => 'Undertexten har laddats ned',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'videoControls.subtitleDownloaded' => 'Undertexten har laddats ned',
'videoControls.subtitleDownloadedNotApplied' => 'Undertexten laddades ned men kunde inte väljas', 'videoControls.subtitleDownloadedNotApplied' => 'Undertexten laddades ned men kunde inte väljas',
'videoControls.subtitleDownloadFailed' => 'Det gick inte att ladda ned undertexten', 'videoControls.subtitleDownloadFailed' => 'Det gick inte att ladda ned undertexten',
'videoControls.searchLanguages' => 'Sök språk...', 'videoControls.searchLanguages' => 'Sök språk...',
@@ -2869,8 +2858,6 @@ extension on TranslationsSv {
'connections.sessionExpiredOne' => ({required Object name}) => 'Sessionen har gått ut för ${name}', 'connections.sessionExpiredOne' => ({required Object name}) => 'Sessionen har gått ut för ${name}',
'connections.sessionExpiredMany' => ({required Object count}) => 'Sessionen har gått ut för ${count} servrar', 'connections.sessionExpiredMany' => ({required Object count}) => 'Sessionen har gått ut för ${count} servrar',
'connections.signInAgain' => 'Logga in igen', 'connections.signInAgain' => 'Logga in igen',
'connections.editJellyfinTitle' => 'Redigera Jellyfin-anslutning',
'connections.editJellyfinIntro' => ({required Object serverName}) => 'Lägg till eller ta bort URL:er för ${serverName}. Plezy använder den nåbara URL som har lägst latens.',
'discover.title' => 'Upptäck', 'discover.title' => 'Upptäck',
'discover.noContentAvailable' => 'Inget innehåll tillgängligt', 'discover.noContentAvailable' => 'Inget innehåll tillgängligt',
'discover.addMediaToLibraries' => 'Lägg till medieinnehåll i dina bibliotek', 'discover.addMediaToLibraries' => 'Lägg till medieinnehåll i dina bibliotek',
@@ -3225,11 +3212,11 @@ extension on TranslationsSv {
'watchTogether.failedToCreate' => 'Det gick inte att skapa sessionen', 'watchTogether.failedToCreate' => 'Det gick inte att skapa sessionen',
'watchTogether.failedToJoin' => 'Det gick inte att gå med i sessionen', 'watchTogether.failedToJoin' => 'Det gick inte att gå med i sessionen',
'watchTogether.sessionCodeCopied' => 'Sessionskoden har kopierats till urklipp', 'watchTogether.sessionCodeCopied' => 'Sessionskoden har kopierats till urklipp',
_ => null,
} ?? switch (path) {
'watchTogether.relayUnreachable' => 'Reläservern kan inte nås. Din internetleverantör kan blockera Titta tillsammans.', 'watchTogether.relayUnreachable' => 'Reläservern kan inte nås. Din internetleverantör kan blockera Titta tillsammans.',
'watchTogether.reconnectingToHost' => 'Återansluter till värden...', 'watchTogether.reconnectingToHost' => 'Återansluter till värden...',
'watchTogether.currentPlayback' => 'Aktuell uppspelning', 'watchTogether.currentPlayback' => 'Aktuell uppspelning',
_ => null,
} ?? switch (path) {
'watchTogether.joinCurrentPlayback' => 'Gå med i aktuell uppspelning', 'watchTogether.joinCurrentPlayback' => 'Gå med i aktuell uppspelning',
'watchTogether.joinCurrentPlaybackDescription' => 'Hoppa tillbaka till det värden tittar på just nu', 'watchTogether.joinCurrentPlaybackDescription' => 'Hoppa tillbaka till det värden tittar på just nu',
'watchTogether.failedToOpenCurrentPlayback' => 'Kunde inte öppna aktuell uppspelning', 'watchTogether.failedToOpenCurrentPlayback' => 'Kunde inte öppna aktuell uppspelning',
@@ -3656,12 +3643,9 @@ extension on TranslationsSv {
'services.libraryFilter.modeHintWhitelist' => 'Synkronisera endast de bibliotek som markeras nedan.', 'services.libraryFilter.modeHintWhitelist' => 'Synkronisera endast de bibliotek som markeras nedan.',
'services.libraryFilter.libraries' => 'Bibliotek', 'services.libraryFilter.libraries' => 'Bibliotek',
'services.libraryFilter.noLibraries' => 'Inga bibliotek tillgängliga', 'services.libraryFilter.noLibraries' => 'Inga bibliotek tillgängliga',
'addServer.addJellyfinTitle' => 'Lägg till Jellyfin-server',
'addServer.serverUrls' => 'Server-URL:er', 'addServer.serverUrls' => 'Server-URL:er',
'addServer.serverUrlsHelper' => 'Du kan ange flera URL:er avgränsade med kommatecken.', 'addServer.serverUrlsHelper' => 'Du kan ange flera URL:er avgränsade med kommatecken.',
'addServer.findServer' => 'Hitta server', 'addServer.findServer' => 'Hitta server',
'addServer.searchingLocalServers' => 'Söker efter lokala Jellyfin-servrar...',
'addServer.localServers' => 'Lokala Jellyfin-servrar',
'addServer.username' => 'Användarnamn', 'addServer.username' => 'Användarnamn',
'addServer.password' => 'Lösenord', 'addServer.password' => 'Lösenord',
'addServer.signIn' => 'Logga in', 'addServer.signIn' => 'Logga in',
@@ -3673,15 +3657,11 @@ extension on TranslationsSv {
'addServer.addPlexTitle' => 'Logga in med Plex', 'addServer.addPlexTitle' => 'Logga in med Plex',
'addServer.pinExpired' => 'PIN-koden gick ut innan inloggning. Försök igen.', 'addServer.pinExpired' => 'PIN-koden gick ut innan inloggning. Försök igen.',
'addServer.failedToRegisterAccount' => ({required Object error}) => 'Kunde inte registrera kontot: ${error}', 'addServer.failedToRegisterAccount' => ({required Object error}) => 'Kunde inte registrera kontot: ${error}',
'addServer.enterJellyfinUrlError' => 'Ange URL till din Jellyfin-server',
'addServer.addConnectionTitle' => 'Lägg till anslutning', 'addServer.addConnectionTitle' => 'Lägg till anslutning',
'addServer.addConnectionTitleScoped' => ({required Object name}) => 'Lägg till i ${name}', 'addServer.addConnectionTitleScoped' => ({required Object name}) => 'Lägg till i ${name}',
'addServer.signInWithPlexCard' => 'Logga in med Plex', 'addServer.signInWithPlexCard' => 'Logga in med Plex',
'addServer.signInWithPlexCardSubtitle' => 'Auktorisera den här enheten. Delade servrar läggs till.', 'addServer.signInWithPlexCardSubtitle' => 'Auktorisera den här enheten. Delade servrar läggs till.',
'addServer.signInWithPlexCardSubtitleScoped' => 'Auktorisera ett Plex-konto. Home-användare blir profiler.', 'addServer.signInWithPlexCardSubtitleScoped' => 'Auktorisera ett Plex-konto. Home-användare blir profiler.',
'addServer.connectToJellyfinCard' => 'Anslut till Jellyfin',
'addServer.connectToJellyfinCardSubtitle' => 'Ange server-URL, användarnamn och lösenord.',
'addServer.connectToJellyfinCardSubtitleScoped' => ({required Object name}) => 'Logga in på en Jellyfin-server. Kopplas till ${name}.',
'addServer.borrowFromAnotherProfile' => 'Låna från en annan profil', 'addServer.borrowFromAnotherProfile' => 'Låna från en annan profil',
'addServer.borrowFromAnotherProfileSubtitle' => 'Återanvänd en annan profils anslutning. PIN-skyddade profiler kräver en PIN.', 'addServer.borrowFromAnotherProfileSubtitle' => 'Återanvänd en annan profils anslutning. PIN-skyddade profiler kräver en PIN.',
_ => null, _ => null,
+3 -23
View File
@@ -115,7 +115,6 @@ class _Translations$auth$tr extends Translations$auth$en {
@override String get waitingForAuth => 'Doğrulama bekleniyor...\nTarayıcınızdan giriş yapın.'; @override String get waitingForAuth => 'Doğrulama bekleniyor...\nTarayıcınızdan giriş yapın.';
@override String get useBrowser => 'Tarayıcı kullan'; @override String get useBrowser => 'Tarayıcı kullan';
@override String get or => 'veya'; @override String get or => 'veya';
@override String get connectToJellyfin => 'Jellyfin\'e Bağlan';
@override String get useQuickConnect => 'Hızlı Bağlantıyı Kullan'; @override String get useQuickConnect => 'Hızlı Bağlantıyı Kullan';
@override String get quickConnectInstructions => 'Jellyfin\'de Hızlı Bağlantı\'yı açın ve bu kodu girin.'; @override String get quickConnectInstructions => 'Jellyfin\'de Hızlı Bağlantı\'yı açın ve bu kodu girin.';
@override String get quickConnectWaiting => 'Onay bekleniyor…'; @override String get quickConnectWaiting => 'Onay bekleniyor…';
@@ -922,8 +921,6 @@ class _Translations$connections$tr extends Translations$connections$en {
@override String sessionExpiredOne({required Object name}) => '${name} için oturum süresi doldu'; @override String sessionExpiredOne({required Object name}) => '${name} için oturum süresi doldu';
@override String sessionExpiredMany({required Object count}) => '${count} sunucu için oturum süresi doldu'; @override String sessionExpiredMany({required Object count}) => '${count} sunucu için oturum süresi doldu';
@override String get signInAgain => 'Tekrar giriş yap'; @override String get signInAgain => 'Tekrar giriş yap';
@override String get editJellyfinTitle => 'Jellyfin bağlantısını düzenle';
@override String editJellyfinIntro({required Object serverName}) => '${serverName} için URL\'ler ekleyin veya kaldırın. Plezy, en düşük gecikmeye sahip ulaşılabilir URL\'yi kullanacaktır.';
} }
// Path: discover // Path: discover
@@ -1814,12 +1811,9 @@ class _Translations$addServer$tr extends Translations$addServer$en {
final TranslationsTr _root; // ignore: unused_field final TranslationsTr _root; // ignore: unused_field
// Translations // Translations
@override String get addJellyfinTitle => 'Jellyfin sunucusu ekle';
@override String get serverUrls => 'Sunucu URL\'leri'; @override String get serverUrls => 'Sunucu URL\'leri';
@override String get serverUrlsHelper => 'Virgülle ayrılmış birden fazla URL\'ye izin verilir.'; @override String get serverUrlsHelper => 'Virgülle ayrılmış birden fazla URL\'ye izin verilir.';
@override String get findServer => 'Sunucu bul'; @override String get findServer => 'Sunucu bul';
@override String get searchingLocalServers => 'Yerel Jellyfin sunucuları aranıyor...';
@override String get localServers => 'Yerel Jellyfin sunucuları';
@override String get username => 'Kullanıcı adı'; @override String get username => 'Kullanıcı adı';
@override String get password => 'Şifre'; @override String get password => 'Şifre';
@override String get signIn => 'Giriş Yap'; @override String get signIn => 'Giriş Yap';
@@ -1831,15 +1825,11 @@ class _Translations$addServer$tr extends Translations$addServer$en {
@override String get addPlexTitle => 'Plex ile Giriş Yap'; @override String get addPlexTitle => 'Plex ile Giriş Yap';
@override String get pinExpired => 'Giriş yapmadan önce PIN süresi doldu. Lütfen tekrar deneyin.'; @override String get pinExpired => 'Giriş yapmadan önce PIN süresi doldu. Lütfen tekrar deneyin.';
@override String failedToRegisterAccount({required Object error}) => 'Hesap kaydı başarısız oldu: ${error}'; @override String failedToRegisterAccount({required Object error}) => 'Hesap kaydı başarısız oldu: ${error}';
@override String get enterJellyfinUrlError => 'Jellyfin sunucu URL\'nizi girin';
@override String get addConnectionTitle => 'Bağlantı ekle'; @override String get addConnectionTitle => 'Bağlantı ekle';
@override String addConnectionTitleScoped({required Object name}) => '${name} profiline ekle'; @override String addConnectionTitleScoped({required Object name}) => '${name} profiline ekle';
@override String get signInWithPlexCard => 'Plex ile Giriş Yap'; @override String get signInWithPlexCard => 'Plex ile Giriş Yap';
@override String get signInWithPlexCardSubtitle => 'Bu cihazı yetkilendirin. Paylaşılan sunucular eklenir.'; @override String get signInWithPlexCardSubtitle => 'Bu cihazı yetkilendirin. Paylaşılan sunucular eklenir.';
@override String get signInWithPlexCardSubtitleScoped => 'Bir Plex hesabını yetkilendirin. Ev kullanıcıları profile dönüşür.'; @override String get signInWithPlexCardSubtitleScoped => 'Bir Plex hesabını yetkilendirin. Ev kullanıcıları profile dönüşür.';
@override String get connectToJellyfinCard => 'Jellyfin\'e Bağlan';
@override String get connectToJellyfinCardSubtitle => 'Sunucu URL\'nizi, kullanıcı adınızı ve şifrenizi girin.';
@override String connectToJellyfinCardSubtitleScoped({required Object name}) => 'Bir Jellyfin sunucusuna giriş yapın. ${name} profiline bağlanır.';
@override String get borrowFromAnotherProfile => 'Başka bir profilden ödünç al'; @override String get borrowFromAnotherProfile => 'Başka bir profilden ödünç al';
@override String get borrowFromAnotherProfileSubtitle => 'Başka bir profilin bağlantısını yeniden kullanın. PIN korumalı profiller bir PIN gerektirir.'; @override String get borrowFromAnotherProfileSubtitle => 'Başka bir profilin bağlantısını yeniden kullanın. PIN korumalı profiller bir PIN gerektirir.';
} }
@@ -2219,7 +2209,6 @@ extension on TranslationsTr {
'auth.waitingForAuth' => 'Doğrulama bekleniyor...\nTarayıcınızdan giriş yapın.', 'auth.waitingForAuth' => 'Doğrulama bekleniyor...\nTarayıcınızdan giriş yapın.',
'auth.useBrowser' => 'Tarayıcı kullan', 'auth.useBrowser' => 'Tarayıcı kullan',
'auth.or' => 'veya', 'auth.or' => 'veya',
'auth.connectToJellyfin' => 'Jellyfin\'e Bağlan',
'auth.useQuickConnect' => 'Hızlı Bağlantıyı Kullan', 'auth.useQuickConnect' => 'Hızlı Bağlantıyı Kullan',
'auth.quickConnectInstructions' => 'Jellyfin\'de Hızlı Bağlantı\'yı açın ve bu kodu girin.', 'auth.quickConnectInstructions' => 'Jellyfin\'de Hızlı Bağlantı\'yı açın ve bu kodu girin.',
'auth.quickConnectWaiting' => 'Onay bekleniyor…', 'auth.quickConnectWaiting' => 'Onay bekleniyor…',
@@ -2722,9 +2711,9 @@ extension on TranslationsTr {
'videoControls.noChaptersAvailable' => 'Kısım bulunmuyor', 'videoControls.noChaptersAvailable' => 'Kısım bulunmuyor',
'videoControls.queue' => 'Kuyruk', 'videoControls.queue' => 'Kuyruk',
'videoControls.noQueueItems' => 'Kuyrukta öge yok', 'videoControls.noQueueItems' => 'Kuyrukta öge yok',
'videoControls.searchSubtitles' => 'Altyazı Ara',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'videoControls.searchSubtitles' => 'Altyazı Ara',
'videoControls.language' => 'Dil', 'videoControls.language' => 'Dil',
'videoControls.noSubtitlesFound' => 'Altyazı bulunamadı', 'videoControls.noSubtitlesFound' => 'Altyazı bulunamadı',
'videoControls.subtitleDownloaded' => 'Altyazı indirildi', 'videoControls.subtitleDownloaded' => 'Altyazı indirildi',
@@ -2884,8 +2873,6 @@ extension on TranslationsTr {
'connections.sessionExpiredOne' => ({required Object name}) => '${name} için oturum süresi doldu', 'connections.sessionExpiredOne' => ({required Object name}) => '${name} için oturum süresi doldu',
'connections.sessionExpiredMany' => ({required Object count}) => '${count} sunucu için oturum süresi doldu', 'connections.sessionExpiredMany' => ({required Object count}) => '${count} sunucu için oturum süresi doldu',
'connections.signInAgain' => 'Tekrar giriş yap', 'connections.signInAgain' => 'Tekrar giriş yap',
'connections.editJellyfinTitle' => 'Jellyfin bağlantısını düzenle',
'connections.editJellyfinIntro' => ({required Object serverName}) => '${serverName} için URL\'ler ekleyin veya kaldırın. Plezy, en düşük gecikmeye sahip ulaşılabilir URL\'yi kullanacaktır.',
'discover.title' => 'Keşfet', 'discover.title' => 'Keşfet',
'discover.noContentAvailable' => 'İçerik bulunmuyor', 'discover.noContentAvailable' => 'İçerik bulunmuyor',
'discover.addMediaToLibraries' => 'Kitaplıklarınıza biraz medya ekleyin', 'discover.addMediaToLibraries' => 'Kitaplıklarınıza biraz medya ekleyin',
@@ -3236,11 +3223,11 @@ extension on TranslationsTr {
'watchTogether.enterCodeHint' => '5 karakterlik kodu girin', 'watchTogether.enterCodeHint' => '5 karakterlik kodu girin',
'watchTogether.pasteFromClipboard' => 'Panodan yapıştır', 'watchTogether.pasteFromClipboard' => 'Panodan yapıştır',
'watchTogether.pleaseEnterCode' => 'Lütfen bir oturum kodu girin', 'watchTogether.pleaseEnterCode' => 'Lütfen bir oturum kodu girin',
_ => null,
} ?? switch (path) {
'watchTogether.codeMustBe5Chars' => 'Oturum kodu 5 karakter olmalıdır', 'watchTogether.codeMustBe5Chars' => 'Oturum kodu 5 karakter olmalıdır',
'watchTogether.joinInstructions' => 'Katılmak için kurucunun oturum kodunu girin.', 'watchTogether.joinInstructions' => 'Katılmak için kurucunun oturum kodunu girin.',
'watchTogether.failedToCreate' => 'Oturum oluşturulamadı', 'watchTogether.failedToCreate' => 'Oturum oluşturulamadı',
_ => null,
} ?? switch (path) {
'watchTogether.failedToJoin' => 'Oturuma katılınamadı', 'watchTogether.failedToJoin' => 'Oturuma katılınamadı',
'watchTogether.sessionCodeCopied' => 'Oturum kodu panoya kopyalandı', 'watchTogether.sessionCodeCopied' => 'Oturum kodu panoya kopyalandı',
'watchTogether.relayUnreachable' => 'Aktarıcı sunucusuna ulaşılamıyor. İSS engellemesi Birlikte İzle\'yi önleyebilir.', 'watchTogether.relayUnreachable' => 'Aktarıcı sunucusuna ulaşılamıyor. İSS engellemesi Birlikte İzle\'yi önleyebilir.',
@@ -3678,12 +3665,9 @@ extension on TranslationsTr {
'services.libraryFilter.modeHintWhitelist' => 'Yalnızca aşağıda işaretlenen kitaplıkları eşitle.', 'services.libraryFilter.modeHintWhitelist' => 'Yalnızca aşağıda işaretlenen kitaplıkları eşitle.',
'services.libraryFilter.libraries' => 'Kitaplıklar', 'services.libraryFilter.libraries' => 'Kitaplıklar',
'services.libraryFilter.noLibraries' => 'Kitaplık bulunmuyor', 'services.libraryFilter.noLibraries' => 'Kitaplık bulunmuyor',
'addServer.addJellyfinTitle' => 'Jellyfin sunucusu ekle',
'addServer.serverUrls' => 'Sunucu URL\'leri', 'addServer.serverUrls' => 'Sunucu URL\'leri',
'addServer.serverUrlsHelper' => 'Virgülle ayrılmış birden fazla URL\'ye izin verilir.', 'addServer.serverUrlsHelper' => 'Virgülle ayrılmış birden fazla URL\'ye izin verilir.',
'addServer.findServer' => 'Sunucu bul', 'addServer.findServer' => 'Sunucu bul',
'addServer.searchingLocalServers' => 'Yerel Jellyfin sunucuları aranıyor...',
'addServer.localServers' => 'Yerel Jellyfin sunucuları',
'addServer.username' => 'Kullanıcı adı', 'addServer.username' => 'Kullanıcı adı',
'addServer.password' => 'Şifre', 'addServer.password' => 'Şifre',
'addServer.signIn' => 'Giriş Yap', 'addServer.signIn' => 'Giriş Yap',
@@ -3695,15 +3679,11 @@ extension on TranslationsTr {
'addServer.addPlexTitle' => 'Plex ile Giriş Yap', 'addServer.addPlexTitle' => 'Plex ile Giriş Yap',
'addServer.pinExpired' => 'Giriş yapmadan önce PIN süresi doldu. Lütfen tekrar deneyin.', 'addServer.pinExpired' => 'Giriş yapmadan önce PIN süresi doldu. Lütfen tekrar deneyin.',
'addServer.failedToRegisterAccount' => ({required Object error}) => 'Hesap kaydı başarısız oldu: ${error}', 'addServer.failedToRegisterAccount' => ({required Object error}) => 'Hesap kaydı başarısız oldu: ${error}',
'addServer.enterJellyfinUrlError' => 'Jellyfin sunucu URL\'nizi girin',
'addServer.addConnectionTitle' => 'Bağlantı ekle', 'addServer.addConnectionTitle' => 'Bağlantı ekle',
'addServer.addConnectionTitleScoped' => ({required Object name}) => '${name} profiline ekle', 'addServer.addConnectionTitleScoped' => ({required Object name}) => '${name} profiline ekle',
'addServer.signInWithPlexCard' => 'Plex ile Giriş Yap', 'addServer.signInWithPlexCard' => 'Plex ile Giriş Yap',
'addServer.signInWithPlexCardSubtitle' => 'Bu cihazı yetkilendirin. Paylaşılan sunucular eklenir.', 'addServer.signInWithPlexCardSubtitle' => 'Bu cihazı yetkilendirin. Paylaşılan sunucular eklenir.',
'addServer.signInWithPlexCardSubtitleScoped' => 'Bir Plex hesabını yetkilendirin. Ev kullanıcıları profile dönüşür.', 'addServer.signInWithPlexCardSubtitleScoped' => 'Bir Plex hesabını yetkilendirin. Ev kullanıcıları profile dönüşür.',
'addServer.connectToJellyfinCard' => 'Jellyfin\'e Bağlan',
'addServer.connectToJellyfinCardSubtitle' => 'Sunucu URL\'nizi, kullanıcı adınızı ve şifrenizi girin.',
'addServer.connectToJellyfinCardSubtitleScoped' => ({required Object name}) => 'Bir Jellyfin sunucusuna giriş yapın. ${name} profiline bağlanır.',
'addServer.borrowFromAnotherProfile' => 'Başka bir profilden ödünç al', 'addServer.borrowFromAnotherProfile' => 'Başka bir profilden ödünç al',
'addServer.borrowFromAnotherProfileSubtitle' => 'Başka bir profilin bağlantısını yeniden kullanın. PIN korumalı profiller bir PIN gerektirir.', 'addServer.borrowFromAnotherProfileSubtitle' => 'Başka bir profilin bağlantısını yeniden kullanın. PIN korumalı profiller bir PIN gerektirir.',
_ => null, _ => null,
+3 -23
View File
@@ -115,7 +115,6 @@ class _Translations$auth$uz extends Translations$auth$en {
@override String get waitingForAuth => 'Tasdiqlanish kutilmoqda...\nBrauzeringizdan kiring.'; @override String get waitingForAuth => 'Tasdiqlanish kutilmoqda...\nBrauzeringizdan kiring.';
@override String get useBrowser => 'Brauzerdan foydalanish'; @override String get useBrowser => 'Brauzerdan foydalanish';
@override String get or => 'yoki'; @override String get or => 'yoki';
@override String get connectToJellyfin => 'Jellyfin-ga ulanish';
@override String get useQuickConnect => 'Tezkor ulanishdan foydalanish'; @override String get useQuickConnect => 'Tezkor ulanishdan foydalanish';
@override String get quickConnectInstructions => 'Jellyfin-da Tezkor ulanishni oching va ushbu kodni kiriting.'; @override String get quickConnectInstructions => 'Jellyfin-da Tezkor ulanishni oching va ushbu kodni kiriting.';
@override String get quickConnectWaiting => 'Tasdiq kutilmoqda…'; @override String get quickConnectWaiting => 'Tasdiq kutilmoqda…';
@@ -922,8 +921,6 @@ class _Translations$connections$uz extends Translations$connections$en {
@override String sessionExpiredOne({required Object name}) => '${name} uchun seans vaqti tugadi'; @override String sessionExpiredOne({required Object name}) => '${name} uchun seans vaqti tugadi';
@override String sessionExpiredMany({required Object count}) => '${count} server uchun seans vaqti tugadi'; @override String sessionExpiredMany({required Object count}) => '${count} server uchun seans vaqti tugadi';
@override String get signInAgain => 'Qaytadan kirish'; @override String get signInAgain => 'Qaytadan kirish';
@override String get editJellyfinTitle => 'Jellyfin ulanishini tahrirlash';
@override String editJellyfinIntro({required Object serverName}) => '${serverName} uchun URL manzilini qoʻshing yoki oʻchiring.';
} }
// Path: discover // Path: discover
@@ -1814,12 +1811,9 @@ class _Translations$addServer$uz extends Translations$addServer$en {
final TranslationsUz _root; // ignore: unused_field final TranslationsUz _root; // ignore: unused_field
// Translations // Translations
@override String get addJellyfinTitle => 'Jellyfin serverini qoʻshish';
@override String get serverUrls => 'Server URL-lari'; @override String get serverUrls => 'Server URL-lari';
@override String get serverUrlsHelper => 'Vergul bilan ajratilgan bir nechta URL manziliga ruxsat beriladi.'; @override String get serverUrlsHelper => 'Vergul bilan ajratilgan bir nechta URL manziliga ruxsat beriladi.';
@override String get findServer => 'Serverni topish'; @override String get findServer => 'Serverni topish';
@override String get searchingLocalServers => 'Mahalliy Jellyfin serverlari qidirilmoqda...';
@override String get localServers => 'Mahalliy Jellyfin serverlari';
@override String get username => 'Foydalanuvchi nomi'; @override String get username => 'Foydalanuvchi nomi';
@override String get password => 'Parol'; @override String get password => 'Parol';
@override String get signIn => 'Kirish'; @override String get signIn => 'Kirish';
@@ -1831,15 +1825,11 @@ class _Translations$addServer$uz extends Translations$addServer$en {
@override String get addPlexTitle => 'Plex orqali kirish'; @override String get addPlexTitle => 'Plex orqali kirish';
@override String get pinExpired => 'PIN kod vaqti tugadi.'; @override String get pinExpired => 'PIN kod vaqti tugadi.';
@override String failedToRegisterAccount({required Object error}) => 'Hisobni roʻyxatdan oʻtkazishda xatolik: ${error}'; @override String failedToRegisterAccount({required Object error}) => 'Hisobni roʻyxatdan oʻtkazishda xatolik: ${error}';
@override String get enterJellyfinUrlError => 'Jellyfin server URL-ini kiriting';
@override String get addConnectionTitle => 'Ulanish qoʻshish'; @override String get addConnectionTitle => 'Ulanish qoʻshish';
@override String addConnectionTitleScoped({required Object name}) => '${name} profiliga qoʻshish'; @override String addConnectionTitleScoped({required Object name}) => '${name} profiliga qoʻshish';
@override String get signInWithPlexCard => 'Plex orqali kirish'; @override String get signInWithPlexCard => 'Plex orqali kirish';
@override String get signInWithPlexCardSubtitle => 'Ushbu qurilmani avtorizatsiya qiling.'; @override String get signInWithPlexCardSubtitle => 'Ushbu qurilmani avtorizatsiya qiling.';
@override String get signInWithPlexCardSubtitleScoped => 'Plex hisobini avtorizatsiya qiling.'; @override String get signInWithPlexCardSubtitleScoped => 'Plex hisobini avtorizatsiya qiling.';
@override String get connectToJellyfinCard => 'Jellyfin-ga ulanish';
@override String get connectToJellyfinCardSubtitle => 'Server URL, foydalanuvchi nomi va parolingizni kiriting.';
@override String connectToJellyfinCardSubtitleScoped({required Object name}) => 'Jellyfin serveriga kiring. ${name} profiliga ulanmoqda.';
@override String get borrowFromAnotherProfile => 'Boshqa profildan olish'; @override String get borrowFromAnotherProfile => 'Boshqa profildan olish';
@override String get borrowFromAnotherProfileSubtitle => 'Boshqa profilning ulanishidan qayta foydalaning.'; @override String get borrowFromAnotherProfileSubtitle => 'Boshqa profilning ulanishidan qayta foydalaning.';
} }
@@ -2219,7 +2209,6 @@ extension on TranslationsUz {
'auth.waitingForAuth' => 'Tasdiqlanish kutilmoqda...\nBrauzeringizdan kiring.', 'auth.waitingForAuth' => 'Tasdiqlanish kutilmoqda...\nBrauzeringizdan kiring.',
'auth.useBrowser' => 'Brauzerdan foydalanish', 'auth.useBrowser' => 'Brauzerdan foydalanish',
'auth.or' => 'yoki', 'auth.or' => 'yoki',
'auth.connectToJellyfin' => 'Jellyfin-ga ulanish',
'auth.useQuickConnect' => 'Tezkor ulanishdan foydalanish', 'auth.useQuickConnect' => 'Tezkor ulanishdan foydalanish',
'auth.quickConnectInstructions' => 'Jellyfin-da Tezkor ulanishni oching va ushbu kodni kiriting.', 'auth.quickConnectInstructions' => 'Jellyfin-da Tezkor ulanishni oching va ushbu kodni kiriting.',
'auth.quickConnectWaiting' => 'Tasdiq kutilmoqda…', 'auth.quickConnectWaiting' => 'Tasdiq kutilmoqda…',
@@ -2722,9 +2711,9 @@ extension on TranslationsUz {
'videoControls.noChaptersAvailable' => 'Boʻlimlar mavjud emas', 'videoControls.noChaptersAvailable' => 'Boʻlimlar mavjud emas',
'videoControls.queue' => 'Navbat', 'videoControls.queue' => 'Navbat',
'videoControls.noQueueItems' => 'Navbatda elementlar yoʻq', 'videoControls.noQueueItems' => 'Navbatda elementlar yoʻq',
'videoControls.searchSubtitles' => 'Subtitr qidirish',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'videoControls.searchSubtitles' => 'Subtitr qidirish',
'videoControls.language' => 'Til', 'videoControls.language' => 'Til',
'videoControls.noSubtitlesFound' => 'Subtitr topilmadi', 'videoControls.noSubtitlesFound' => 'Subtitr topilmadi',
'videoControls.subtitleDownloaded' => 'Subtitr yuklab olindi', 'videoControls.subtitleDownloaded' => 'Subtitr yuklab olindi',
@@ -2884,8 +2873,6 @@ extension on TranslationsUz {
'connections.sessionExpiredOne' => ({required Object name}) => '${name} uchun seans vaqti tugadi', 'connections.sessionExpiredOne' => ({required Object name}) => '${name} uchun seans vaqti tugadi',
'connections.sessionExpiredMany' => ({required Object count}) => '${count} server uchun seans vaqti tugadi', 'connections.sessionExpiredMany' => ({required Object count}) => '${count} server uchun seans vaqti tugadi',
'connections.signInAgain' => 'Qaytadan kirish', 'connections.signInAgain' => 'Qaytadan kirish',
'connections.editJellyfinTitle' => 'Jellyfin ulanishini tahrirlash',
'connections.editJellyfinIntro' => ({required Object serverName}) => '${serverName} uchun URL manzilini qoʻshing yoki oʻchiring.',
'discover.title' => 'Kashf qilish', 'discover.title' => 'Kashf qilish',
'discover.noContentAvailable' => 'Kontent mavjud emas', 'discover.noContentAvailable' => 'Kontent mavjud emas',
'discover.addMediaToLibraries' => 'Kutubxonalaringizga media qoʻshing', 'discover.addMediaToLibraries' => 'Kutubxonalaringizga media qoʻshing',
@@ -3236,11 +3223,11 @@ extension on TranslationsUz {
'watchTogether.enterCodeHint' => '5 xonali kodni kiriting', 'watchTogether.enterCodeHint' => '5 xonali kodni kiriting',
'watchTogether.pasteFromClipboard' => 'Xotiradan joylash', 'watchTogether.pasteFromClipboard' => 'Xotiradan joylash',
'watchTogether.pleaseEnterCode' => 'Seans kodini kiriting', 'watchTogether.pleaseEnterCode' => 'Seans kodini kiriting',
_ => null,
} ?? switch (path) {
'watchTogether.codeMustBe5Chars' => 'Seans kodi 5 ta belgidan iborat boʻlishi kerak', 'watchTogether.codeMustBe5Chars' => 'Seans kodi 5 ta belgidan iborat boʻlishi kerak',
'watchTogether.joinInstructions' => 'Tashkilotchining seans kodini kiriting.', 'watchTogether.joinInstructions' => 'Tashkilotchining seans kodini kiriting.',
'watchTogether.failedToCreate' => 'Seansni yaratib boʻlmadi', 'watchTogether.failedToCreate' => 'Seansni yaratib boʻlmadi',
_ => null,
} ?? switch (path) {
'watchTogether.failedToJoin' => 'Seansga qoʻshilib boʻlmadi', 'watchTogether.failedToJoin' => 'Seansga qoʻshilib boʻlmadi',
'watchTogether.sessionCodeCopied' => 'Seans kodi nusxalandi', 'watchTogether.sessionCodeCopied' => 'Seans kodi nusxalandi',
'watchTogether.relayUnreachable' => 'Rele serveriga ulanib boʻlmadi.', 'watchTogether.relayUnreachable' => 'Rele serveriga ulanib boʻlmadi.',
@@ -3678,12 +3665,9 @@ extension on TranslationsUz {
'services.libraryFilter.modeHintWhitelist' => 'Faqat quyida tanlangan kutubxonalarni sinxronlash.', 'services.libraryFilter.modeHintWhitelist' => 'Faqat quyida tanlangan kutubxonalarni sinxronlash.',
'services.libraryFilter.libraries' => 'Kutubxonalar', 'services.libraryFilter.libraries' => 'Kutubxonalar',
'services.libraryFilter.noLibraries' => 'Kutubxonalar yoʻq', 'services.libraryFilter.noLibraries' => 'Kutubxonalar yoʻq',
'addServer.addJellyfinTitle' => 'Jellyfin serverini qoʻshish',
'addServer.serverUrls' => 'Server URL-lari', 'addServer.serverUrls' => 'Server URL-lari',
'addServer.serverUrlsHelper' => 'Vergul bilan ajratilgan bir nechta URL manziliga ruxsat beriladi.', 'addServer.serverUrlsHelper' => 'Vergul bilan ajratilgan bir nechta URL manziliga ruxsat beriladi.',
'addServer.findServer' => 'Serverni topish', 'addServer.findServer' => 'Serverni topish',
'addServer.searchingLocalServers' => 'Mahalliy Jellyfin serverlari qidirilmoqda...',
'addServer.localServers' => 'Mahalliy Jellyfin serverlari',
'addServer.username' => 'Foydalanuvchi nomi', 'addServer.username' => 'Foydalanuvchi nomi',
'addServer.password' => 'Parol', 'addServer.password' => 'Parol',
'addServer.signIn' => 'Kirish', 'addServer.signIn' => 'Kirish',
@@ -3695,15 +3679,11 @@ extension on TranslationsUz {
'addServer.addPlexTitle' => 'Plex orqali kirish', 'addServer.addPlexTitle' => 'Plex orqali kirish',
'addServer.pinExpired' => 'PIN kod vaqti tugadi.', 'addServer.pinExpired' => 'PIN kod vaqti tugadi.',
'addServer.failedToRegisterAccount' => ({required Object error}) => 'Hisobni roʻyxatdan oʻtkazishda xatolik: ${error}', 'addServer.failedToRegisterAccount' => ({required Object error}) => 'Hisobni roʻyxatdan oʻtkazishda xatolik: ${error}',
'addServer.enterJellyfinUrlError' => 'Jellyfin server URL-ini kiriting',
'addServer.addConnectionTitle' => 'Ulanish qoʻshish', 'addServer.addConnectionTitle' => 'Ulanish qoʻshish',
'addServer.addConnectionTitleScoped' => ({required Object name}) => '${name} profiliga qoʻshish', 'addServer.addConnectionTitleScoped' => ({required Object name}) => '${name} profiliga qoʻshish',
'addServer.signInWithPlexCard' => 'Plex orqali kirish', 'addServer.signInWithPlexCard' => 'Plex orqali kirish',
'addServer.signInWithPlexCardSubtitle' => 'Ushbu qurilmani avtorizatsiya qiling.', 'addServer.signInWithPlexCardSubtitle' => 'Ushbu qurilmani avtorizatsiya qiling.',
'addServer.signInWithPlexCardSubtitleScoped' => 'Plex hisobini avtorizatsiya qiling.', 'addServer.signInWithPlexCardSubtitleScoped' => 'Plex hisobini avtorizatsiya qiling.',
'addServer.connectToJellyfinCard' => 'Jellyfin-ga ulanish',
'addServer.connectToJellyfinCardSubtitle' => 'Server URL, foydalanuvchi nomi va parolingizni kiriting.',
'addServer.connectToJellyfinCardSubtitleScoped' => ({required Object name}) => 'Jellyfin serveriga kiring. ${name} profiliga ulanmoqda.',
'addServer.borrowFromAnotherProfile' => 'Boshqa profildan olish', 'addServer.borrowFromAnotherProfile' => 'Boshqa profildan olish',
'addServer.borrowFromAnotherProfileSubtitle' => 'Boshqa profilning ulanishidan qayta foydalaning.', 'addServer.borrowFromAnotherProfileSubtitle' => 'Boshqa profilning ulanishidan qayta foydalaning.',
_ => null, _ => null,
+3 -23
View File
@@ -115,7 +115,6 @@ class Translations$auth$zh extends Translations$auth$en {
@override String get waitingForAuth => '正在等待身份验证…\n请在浏览器中登录。'; @override String get waitingForAuth => '正在等待身份验证…\n请在浏览器中登录。';
@override String get useBrowser => '使用浏览器'; @override String get useBrowser => '使用浏览器';
@override String get or => ''; @override String get or => '';
@override String get connectToJellyfin => '连接到 Jellyfin';
@override String get useQuickConnect => '使用 Quick Connect'; @override String get useQuickConnect => '使用 Quick Connect';
@override String get quickConnectInstructions => '在 Jellyfin 中打开 Quick Connect 并输入此代码。'; @override String get quickConnectInstructions => '在 Jellyfin 中打开 Quick Connect 并输入此代码。';
@override String get quickConnectWaiting => '等待批准…'; @override String get quickConnectWaiting => '等待批准…';
@@ -917,8 +916,6 @@ class Translations$connections$zh extends Translations$connections$en {
@override String sessionExpiredOne({required Object name}) => '${name} 的会话已过期'; @override String sessionExpiredOne({required Object name}) => '${name} 的会话已过期';
@override String sessionExpiredMany({required Object count}) => '${count} 个服务器的会话已过期'; @override String sessionExpiredMany({required Object count}) => '${count} 个服务器的会话已过期';
@override String get signInAgain => '重新登录'; @override String get signInAgain => '重新登录';
@override String get editJellyfinTitle => '编辑 Jellyfin 连接';
@override String editJellyfinIntro({required Object serverName}) => '添加或移除 ${serverName} 的 URL。Plezy 会使用可访问且延迟最低的地址。';
} }
// Path: discover // Path: discover
@@ -1800,12 +1797,9 @@ class Translations$addServer$zh extends Translations$addServer$en {
final TranslationsZh _root; // ignore: unused_field final TranslationsZh _root; // ignore: unused_field
// Translations // Translations
@override String get addJellyfinTitle => '添加 Jellyfin 服务器';
@override String get serverUrls => '服务器 URL'; @override String get serverUrls => '服务器 URL';
@override String get serverUrlsHelper => '可输入多个 URL,并用逗号分隔。'; @override String get serverUrlsHelper => '可输入多个 URL,并用逗号分隔。';
@override String get findServer => '查找服务器'; @override String get findServer => '查找服务器';
@override String get searchingLocalServers => '正在查找本地 Jellyfin 服务器…';
@override String get localServers => '本地 Jellyfin 服务器';
@override String get username => '用户名'; @override String get username => '用户名';
@override String get password => '密码'; @override String get password => '密码';
@override String get signIn => '登录'; @override String get signIn => '登录';
@@ -1817,15 +1811,11 @@ class Translations$addServer$zh extends Translations$addServer$en {
@override String get addPlexTitle => '使用 Plex 登录'; @override String get addPlexTitle => '使用 Plex 登录';
@override String get pinExpired => 'PIN 在登录前已过期。请重试。'; @override String get pinExpired => 'PIN 在登录前已过期。请重试。';
@override String failedToRegisterAccount({required Object error}) => '注册账户失败:${error}'; @override String failedToRegisterAccount({required Object error}) => '注册账户失败:${error}';
@override String get enterJellyfinUrlError => '请输入 Jellyfin 服务器 URL';
@override String get addConnectionTitle => '添加连接'; @override String get addConnectionTitle => '添加连接';
@override String addConnectionTitleScoped({required Object name}) => '添加到 ${name}'; @override String addConnectionTitleScoped({required Object name}) => '添加到 ${name}';
@override String get signInWithPlexCard => '使用 Plex 登录'; @override String get signInWithPlexCard => '使用 Plex 登录';
@override String get signInWithPlexCardSubtitle => '授权此设备。共享服务器会被添加。'; @override String get signInWithPlexCardSubtitle => '授权此设备。共享服务器会被添加。';
@override String get signInWithPlexCardSubtitleScoped => '授权一个 Plex 账户。Plex Home 用户将成为 Plezy 用户资料。'; @override String get signInWithPlexCardSubtitleScoped => '授权一个 Plex 账户。Plex Home 用户将成为 Plezy 用户资料。';
@override String get connectToJellyfinCard => '连接到 Jellyfin';
@override String get connectToJellyfinCardSubtitle => '输入服务器 URL、用户名和密码。';
@override String connectToJellyfinCardSubtitleScoped({required Object name}) => '登录到 Jellyfin 服务器。绑定到 ${name}';
@override String get borrowFromAnotherProfile => '使用其他用户资料的连接'; @override String get borrowFromAnotherProfile => '使用其他用户资料的连接';
@override String get borrowFromAnotherProfileSubtitle => '复用另一个用户资料的连接。受 PIN 保护的用户资料需要输入 PIN。'; @override String get borrowFromAnotherProfileSubtitle => '复用另一个用户资料的连接。受 PIN 保护的用户资料需要输入 PIN。';
} }
@@ -2205,7 +2195,6 @@ extension on TranslationsZh {
'auth.waitingForAuth' => '正在等待身份验证…\n请在浏览器中登录。', 'auth.waitingForAuth' => '正在等待身份验证…\n请在浏览器中登录。',
'auth.useBrowser' => '使用浏览器', 'auth.useBrowser' => '使用浏览器',
'auth.or' => '', 'auth.or' => '',
'auth.connectToJellyfin' => '连接到 Jellyfin',
'auth.useQuickConnect' => '使用 Quick Connect', 'auth.useQuickConnect' => '使用 Quick Connect',
'auth.quickConnectInstructions' => '在 Jellyfin 中打开 Quick Connect 并输入此代码。', 'auth.quickConnectInstructions' => '在 Jellyfin 中打开 Quick Connect 并输入此代码。',
'auth.quickConnectWaiting' => '等待批准…', 'auth.quickConnectWaiting' => '等待批准…',
@@ -2708,9 +2697,9 @@ extension on TranslationsZh {
'videoControls.searchSubtitles' => '搜索字幕', 'videoControls.searchSubtitles' => '搜索字幕',
'videoControls.language' => '语言', 'videoControls.language' => '语言',
'videoControls.noSubtitlesFound' => '未找到字幕', 'videoControls.noSubtitlesFound' => '未找到字幕',
'videoControls.subtitleDownloaded' => '字幕已下载',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'videoControls.subtitleDownloaded' => '字幕已下载',
'videoControls.subtitleDownloadedNotApplied' => '字幕已下载,但无法选择', 'videoControls.subtitleDownloadedNotApplied' => '字幕已下载,但无法选择',
'videoControls.subtitleDownloadFailed' => '字幕下载失败', 'videoControls.subtitleDownloadFailed' => '字幕下载失败',
'videoControls.searchLanguages' => '搜索语言…', 'videoControls.searchLanguages' => '搜索语言…',
@@ -2866,8 +2855,6 @@ extension on TranslationsZh {
'connections.sessionExpiredOne' => ({required Object name}) => '${name} 的会话已过期', 'connections.sessionExpiredOne' => ({required Object name}) => '${name} 的会话已过期',
'connections.sessionExpiredMany' => ({required Object count}) => '${count} 个服务器的会话已过期', 'connections.sessionExpiredMany' => ({required Object count}) => '${count} 个服务器的会话已过期',
'connections.signInAgain' => '重新登录', 'connections.signInAgain' => '重新登录',
'connections.editJellyfinTitle' => '编辑 Jellyfin 连接',
'connections.editJellyfinIntro' => ({required Object serverName}) => '添加或移除 ${serverName} 的 URL。Plezy 会使用可访问且延迟最低的地址。',
'discover.title' => '发现', 'discover.title' => '发现',
'discover.noContentAvailable' => '没有可用内容', 'discover.noContentAvailable' => '没有可用内容',
'discover.addMediaToLibraries' => '请向你的媒体库添加一些媒体', 'discover.addMediaToLibraries' => '请向你的媒体库添加一些媒体',
@@ -3222,11 +3209,11 @@ extension on TranslationsZh {
'watchTogether.failedToCreate' => '创建会话失败', 'watchTogether.failedToCreate' => '创建会话失败',
'watchTogether.failedToJoin' => '加入会话失败', 'watchTogether.failedToJoin' => '加入会话失败',
'watchTogether.sessionCodeCopied' => '会话代码已复制到剪贴板', 'watchTogether.sessionCodeCopied' => '会话代码已复制到剪贴板',
_ => null,
} ?? switch (path) {
'watchTogether.relayUnreachable' => '无法访问中继服务器。网络运营商的屏蔽可能导致“一起看”不可用。', 'watchTogether.relayUnreachable' => '无法访问中继服务器。网络运营商的屏蔽可能导致“一起看”不可用。',
'watchTogether.reconnectingToHost' => '正在重新连接到主持人…', 'watchTogether.reconnectingToHost' => '正在重新连接到主持人…',
'watchTogether.currentPlayback' => '当前播放', 'watchTogether.currentPlayback' => '当前播放',
_ => null,
} ?? switch (path) {
'watchTogether.joinCurrentPlayback' => '加入当前播放', 'watchTogether.joinCurrentPlayback' => '加入当前播放',
'watchTogether.joinCurrentPlaybackDescription' => '加入主持人当前正在观看的内容', 'watchTogether.joinCurrentPlaybackDescription' => '加入主持人当前正在观看的内容',
'watchTogether.failedToOpenCurrentPlayback' => '无法打开当前播放', 'watchTogether.failedToOpenCurrentPlayback' => '无法打开当前播放',
@@ -3653,12 +3640,9 @@ extension on TranslationsZh {
'services.libraryFilter.modeHintWhitelist' => '仅同步下方勾选的媒体库。', 'services.libraryFilter.modeHintWhitelist' => '仅同步下方勾选的媒体库。',
'services.libraryFilter.libraries' => '媒体库', 'services.libraryFilter.libraries' => '媒体库',
'services.libraryFilter.noLibraries' => '没有可用的媒体库', 'services.libraryFilter.noLibraries' => '没有可用的媒体库',
'addServer.addJellyfinTitle' => '添加 Jellyfin 服务器',
'addServer.serverUrls' => '服务器 URL', 'addServer.serverUrls' => '服务器 URL',
'addServer.serverUrlsHelper' => '可输入多个 URL,并用逗号分隔。', 'addServer.serverUrlsHelper' => '可输入多个 URL,并用逗号分隔。',
'addServer.findServer' => '查找服务器', 'addServer.findServer' => '查找服务器',
'addServer.searchingLocalServers' => '正在查找本地 Jellyfin 服务器…',
'addServer.localServers' => '本地 Jellyfin 服务器',
'addServer.username' => '用户名', 'addServer.username' => '用户名',
'addServer.password' => '密码', 'addServer.password' => '密码',
'addServer.signIn' => '登录', 'addServer.signIn' => '登录',
@@ -3670,15 +3654,11 @@ extension on TranslationsZh {
'addServer.addPlexTitle' => '使用 Plex 登录', 'addServer.addPlexTitle' => '使用 Plex 登录',
'addServer.pinExpired' => 'PIN 在登录前已过期。请重试。', 'addServer.pinExpired' => 'PIN 在登录前已过期。请重试。',
'addServer.failedToRegisterAccount' => ({required Object error}) => '注册账户失败:${error}', 'addServer.failedToRegisterAccount' => ({required Object error}) => '注册账户失败:${error}',
'addServer.enterJellyfinUrlError' => '请输入 Jellyfin 服务器 URL',
'addServer.addConnectionTitle' => '添加连接', 'addServer.addConnectionTitle' => '添加连接',
'addServer.addConnectionTitleScoped' => ({required Object name}) => '添加到 ${name}', 'addServer.addConnectionTitleScoped' => ({required Object name}) => '添加到 ${name}',
'addServer.signInWithPlexCard' => '使用 Plex 登录', 'addServer.signInWithPlexCard' => '使用 Plex 登录',
'addServer.signInWithPlexCardSubtitle' => '授权此设备。共享服务器会被添加。', 'addServer.signInWithPlexCardSubtitle' => '授权此设备。共享服务器会被添加。',
'addServer.signInWithPlexCardSubtitleScoped' => '授权一个 Plex 账户。Plex Home 用户将成为 Plezy 用户资料。', 'addServer.signInWithPlexCardSubtitleScoped' => '授权一个 Plex 账户。Plex Home 用户将成为 Plezy 用户资料。',
'addServer.connectToJellyfinCard' => '连接到 Jellyfin',
'addServer.connectToJellyfinCardSubtitle' => '输入服务器 URL、用户名和密码。',
'addServer.connectToJellyfinCardSubtitleScoped' => ({required Object name}) => '登录到 Jellyfin 服务器。绑定到 ${name}',
'addServer.borrowFromAnotherProfile' => '使用其他用户资料的连接', 'addServer.borrowFromAnotherProfile' => '使用其他用户资料的连接',
'addServer.borrowFromAnotherProfileSubtitle' => '复用另一个用户资料的连接。受 PIN 保护的用户资料需要输入 PIN。', 'addServer.borrowFromAnotherProfileSubtitle' => '复用另一个用户资料的连接。受 PIN 保护的用户资料需要输入 PIN。',
_ => null, _ => null,
+3 -23
View File
@@ -116,7 +116,6 @@ class _Translations$auth$zh_Hant extends Translations$auth$zh {
@override String get waitingForAuth => '正在等待驗證…\n請在瀏覽器中登入。'; @override String get waitingForAuth => '正在等待驗證…\n請在瀏覽器中登入。';
@override String get useBrowser => '使用瀏覽器'; @override String get useBrowser => '使用瀏覽器';
@override String get or => ''; @override String get or => '';
@override String get connectToJellyfin => '連線至 Jellyfin';
@override String get useQuickConnect => '使用快速連線(Quick Connect'; @override String get useQuickConnect => '使用快速連線(Quick Connect';
@override String get quickConnectInstructions => '在 Jellyfin 中開啟快速連線並輸入此代碼。'; @override String get quickConnectInstructions => '在 Jellyfin 中開啟快速連線並輸入此代碼。';
@override String get quickConnectWaiting => '等待核准…'; @override String get quickConnectWaiting => '等待核准…';
@@ -918,8 +917,6 @@ class _Translations$connections$zh_Hant extends Translations$connections$zh {
@override String sessionExpiredOne({required Object name}) => '${name} 的工作階段已過期'; @override String sessionExpiredOne({required Object name}) => '${name} 的工作階段已過期';
@override String sessionExpiredMany({required Object count}) => '${count} 個伺服器的工作階段已過期'; @override String sessionExpiredMany({required Object count}) => '${count} 個伺服器的工作階段已過期';
@override String get signInAgain => '重新登入'; @override String get signInAgain => '重新登入';
@override String get editJellyfinTitle => '編輯 Jellyfin 連線';
@override String editJellyfinIntro({required Object serverName}) => '新增或移除 ${serverName} 的 URL。Plezy 會自動選擇可連線且延遲最低的網址。';
} }
// Path: discover // Path: discover
@@ -1801,12 +1798,9 @@ class _Translations$addServer$zh_Hant extends Translations$addServer$zh {
final TranslationsZhHant _root; // ignore: unused_field final TranslationsZhHant _root; // ignore: unused_field
// Translations // Translations
@override String get addJellyfinTitle => '新增 Jellyfin 伺服器';
@override String get serverUrls => '伺服器 URL'; @override String get serverUrls => '伺服器 URL';
@override String get serverUrlsHelper => '可輸入多個連線網址,以逗號區隔。'; @override String get serverUrlsHelper => '可輸入多個連線網址,以逗號區隔。';
@override String get findServer => '尋找伺服器'; @override String get findServer => '尋找伺服器';
@override String get searchingLocalServers => '正在尋找本地 Jellyfin 伺服器…';
@override String get localServers => '本地 Jellyfin 伺服器';
@override String get username => '使用者名稱'; @override String get username => '使用者名稱';
@override String get password => '密碼'; @override String get password => '密碼';
@override String get signIn => '登入'; @override String get signIn => '登入';
@@ -1818,15 +1812,11 @@ class _Translations$addServer$zh_Hant extends Translations$addServer$zh {
@override String get addPlexTitle => '使用 Plex 登入'; @override String get addPlexTitle => '使用 Plex 登入';
@override String get pinExpired => 'PIN 碼在登入前已過期。請重試。'; @override String get pinExpired => 'PIN 碼在登入前已過期。請重試。';
@override String failedToRegisterAccount({required Object error}) => '註冊帳戶失敗:${error}'; @override String failedToRegisterAccount({required Object error}) => '註冊帳戶失敗:${error}';
@override String get enterJellyfinUrlError => '請輸入您的 Jellyfin 伺服器 URL';
@override String get addConnectionTitle => '新增連線'; @override String get addConnectionTitle => '新增連線';
@override String addConnectionTitleScoped({required Object name}) => '新增連線至 ${name}'; @override String addConnectionTitleScoped({required Object name}) => '新增連線至 ${name}';
@override String get signInWithPlexCard => '使用 Plex 登入'; @override String get signInWithPlexCard => '使用 Plex 登入';
@override String get signInWithPlexCardSubtitle => '授權此裝置。將會新增共享的伺服器連線。'; @override String get signInWithPlexCardSubtitle => '授權此裝置。將會新增共享的伺服器連線。';
@override String get signInWithPlexCardSubtitleScoped => '授權 Plex 帳戶。Home 使用者會建立為個別的使用者設定檔。'; @override String get signInWithPlexCardSubtitleScoped => '授權 Plex 帳戶。Home 使用者會建立為個別的使用者設定檔。';
@override String get connectToJellyfinCard => '連線至 Jellyfin';
@override String get connectToJellyfinCardSubtitle => '輸入伺服器 URL、使用者名稱與密碼。';
@override String connectToJellyfinCardSubtitleScoped({required Object name}) => '登入 Jellyfin 伺服器,並綁定至 ${name} 使用者設定檔。';
@override String get borrowFromAnotherProfile => '從另一個使用者設定檔共用'; @override String get borrowFromAnotherProfile => '從另一個使用者設定檔共用';
@override String get borrowFromAnotherProfileSubtitle => '重複使用另一個使用者設定檔的連線資訊。受 PIN 碼保護的使用者設定檔需輸入 PIN 碼。'; @override String get borrowFromAnotherProfileSubtitle => '重複使用另一個使用者設定檔的連線資訊。受 PIN 碼保護的使用者設定檔需輸入 PIN 碼。';
} }
@@ -2206,7 +2196,6 @@ extension on TranslationsZhHant {
'auth.waitingForAuth' => '正在等待驗證…\n請在瀏覽器中登入。', 'auth.waitingForAuth' => '正在等待驗證…\n請在瀏覽器中登入。',
'auth.useBrowser' => '使用瀏覽器', 'auth.useBrowser' => '使用瀏覽器',
'auth.or' => '', 'auth.or' => '',
'auth.connectToJellyfin' => '連線至 Jellyfin',
'auth.useQuickConnect' => '使用快速連線(Quick Connect', 'auth.useQuickConnect' => '使用快速連線(Quick Connect',
'auth.quickConnectInstructions' => '在 Jellyfin 中開啟快速連線並輸入此代碼。', 'auth.quickConnectInstructions' => '在 Jellyfin 中開啟快速連線並輸入此代碼。',
'auth.quickConnectWaiting' => '等待核准…', 'auth.quickConnectWaiting' => '等待核准…',
@@ -2709,9 +2698,9 @@ extension on TranslationsZhHant {
'videoControls.searchSubtitles' => '搜尋字幕', 'videoControls.searchSubtitles' => '搜尋字幕',
'videoControls.language' => '語言', 'videoControls.language' => '語言',
'videoControls.noSubtitlesFound' => '找不到字幕', 'videoControls.noSubtitlesFound' => '找不到字幕',
'videoControls.subtitleDownloaded' => '字幕下載成功',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'videoControls.subtitleDownloaded' => '字幕下載成功',
'videoControls.subtitleDownloadedNotApplied' => '字幕已下載,但無法套用', 'videoControls.subtitleDownloadedNotApplied' => '字幕已下載,但無法套用',
'videoControls.subtitleDownloadFailed' => '字幕下載失敗', 'videoControls.subtitleDownloadFailed' => '字幕下載失敗',
'videoControls.searchLanguages' => '搜尋語言…', 'videoControls.searchLanguages' => '搜尋語言…',
@@ -2867,8 +2856,6 @@ extension on TranslationsZhHant {
'connections.sessionExpiredOne' => ({required Object name}) => '${name} 的工作階段已過期', 'connections.sessionExpiredOne' => ({required Object name}) => '${name} 的工作階段已過期',
'connections.sessionExpiredMany' => ({required Object count}) => '${count} 個伺服器的工作階段已過期', 'connections.sessionExpiredMany' => ({required Object count}) => '${count} 個伺服器的工作階段已過期',
'connections.signInAgain' => '重新登入', 'connections.signInAgain' => '重新登入',
'connections.editJellyfinTitle' => '編輯 Jellyfin 連線',
'connections.editJellyfinIntro' => ({required Object serverName}) => '新增或移除 ${serverName} 的 URL。Plezy 會自動選擇可連線且延遲最低的網址。',
'discover.title' => '發現', 'discover.title' => '發現',
'discover.noContentAvailable' => '沒有可用內容', 'discover.noContentAvailable' => '沒有可用內容',
'discover.addMediaToLibraries' => '請向您的媒體庫新增一些媒體內容', 'discover.addMediaToLibraries' => '請向您的媒體庫新增一些媒體內容',
@@ -3223,11 +3210,11 @@ extension on TranslationsZhHant {
'watchTogether.failedToCreate' => '建立工作階段失敗', 'watchTogether.failedToCreate' => '建立工作階段失敗',
'watchTogether.failedToJoin' => '加入工作階段失敗', 'watchTogether.failedToJoin' => '加入工作階段失敗',
'watchTogether.sessionCodeCopied' => '工作階段代碼已複製到剪貼簿', 'watchTogether.sessionCodeCopied' => '工作階段代碼已複製到剪貼簿',
_ => null,
} ?? switch (path) {
'watchTogether.relayUnreachable' => '無法連線至中繼伺服器。ISP 封鎖可能會導致「一起看」無法使用。', 'watchTogether.relayUnreachable' => '無法連線至中繼伺服器。ISP 封鎖可能會導致「一起看」無法使用。',
'watchTogether.reconnectingToHost' => '正在重新連線至主持人…', 'watchTogether.reconnectingToHost' => '正在重新連線至主持人…',
'watchTogether.currentPlayback' => '目前播放內容', 'watchTogether.currentPlayback' => '目前播放內容',
_ => null,
} ?? switch (path) {
'watchTogether.joinCurrentPlayback' => '加入目前播放點', 'watchTogether.joinCurrentPlayback' => '加入目前播放點',
'watchTogether.joinCurrentPlaybackDescription' => '同步至主持人目前的觀看進度', 'watchTogether.joinCurrentPlaybackDescription' => '同步至主持人目前的觀看進度',
'watchTogether.failedToOpenCurrentPlayback' => '無法開啟目前播放點', 'watchTogether.failedToOpenCurrentPlayback' => '無法開啟目前播放點',
@@ -3654,12 +3641,9 @@ extension on TranslationsZhHant {
'services.libraryFilter.modeHintWhitelist' => '僅同步下方已勾選的媒體庫。', 'services.libraryFilter.modeHintWhitelist' => '僅同步下方已勾選的媒體庫。',
'services.libraryFilter.libraries' => '媒體庫', 'services.libraryFilter.libraries' => '媒體庫',
'services.libraryFilter.noLibraries' => '沒有可用的媒體庫', 'services.libraryFilter.noLibraries' => '沒有可用的媒體庫',
'addServer.addJellyfinTitle' => '新增 Jellyfin 伺服器',
'addServer.serverUrls' => '伺服器 URL', 'addServer.serverUrls' => '伺服器 URL',
'addServer.serverUrlsHelper' => '可輸入多個連線網址,以逗號區隔。', 'addServer.serverUrlsHelper' => '可輸入多個連線網址,以逗號區隔。',
'addServer.findServer' => '尋找伺服器', 'addServer.findServer' => '尋找伺服器',
'addServer.searchingLocalServers' => '正在尋找本地 Jellyfin 伺服器…',
'addServer.localServers' => '本地 Jellyfin 伺服器',
'addServer.username' => '使用者名稱', 'addServer.username' => '使用者名稱',
'addServer.password' => '密碼', 'addServer.password' => '密碼',
'addServer.signIn' => '登入', 'addServer.signIn' => '登入',
@@ -3671,15 +3655,11 @@ extension on TranslationsZhHant {
'addServer.addPlexTitle' => '使用 Plex 登入', 'addServer.addPlexTitle' => '使用 Plex 登入',
'addServer.pinExpired' => 'PIN 碼在登入前已過期。請重試。', 'addServer.pinExpired' => 'PIN 碼在登入前已過期。請重試。',
'addServer.failedToRegisterAccount' => ({required Object error}) => '註冊帳戶失敗:${error}', 'addServer.failedToRegisterAccount' => ({required Object error}) => '註冊帳戶失敗:${error}',
'addServer.enterJellyfinUrlError' => '請輸入您的 Jellyfin 伺服器 URL',
'addServer.addConnectionTitle' => '新增連線', 'addServer.addConnectionTitle' => '新增連線',
'addServer.addConnectionTitleScoped' => ({required Object name}) => '新增連線至 ${name}', 'addServer.addConnectionTitleScoped' => ({required Object name}) => '新增連線至 ${name}',
'addServer.signInWithPlexCard' => '使用 Plex 登入', 'addServer.signInWithPlexCard' => '使用 Plex 登入',
'addServer.signInWithPlexCardSubtitle' => '授權此裝置。將會新增共享的伺服器連線。', 'addServer.signInWithPlexCardSubtitle' => '授權此裝置。將會新增共享的伺服器連線。',
'addServer.signInWithPlexCardSubtitleScoped' => '授權 Plex 帳戶。Home 使用者會建立為個別的使用者設定檔。', 'addServer.signInWithPlexCardSubtitleScoped' => '授權 Plex 帳戶。Home 使用者會建立為個別的使用者設定檔。',
'addServer.connectToJellyfinCard' => '連線至 Jellyfin',
'addServer.connectToJellyfinCardSubtitle' => '輸入伺服器 URL、使用者名稱與密碼。',
'addServer.connectToJellyfinCardSubtitleScoped' => ({required Object name}) => '登入 Jellyfin 伺服器,並綁定至 ${name} 使用者設定檔。',
'addServer.borrowFromAnotherProfile' => '從另一個使用者設定檔共用', 'addServer.borrowFromAnotherProfile' => '從另一個使用者設定檔共用',
'addServer.borrowFromAnotherProfileSubtitle' => '重複使用另一個使用者設定檔的連線資訊。受 PIN 碼保護的使用者設定檔需輸入 PIN 碼。', 'addServer.borrowFromAnotherProfileSubtitle' => '重複使用另一個使用者設定檔的連線資訊。受 PIN 碼保護的使用者設定檔需輸入 PIN 碼。',
_ => null, _ => null,
+10 -10
View File
@@ -11,7 +11,7 @@
"waitingForAuth": "Väntar på autentisering...\nLogga in från din webbläsare.", "waitingForAuth": "Väntar på autentisering...\nLogga in från din webbläsare.",
"useBrowser": "Använd webbläsare", "useBrowser": "Använd webbläsare",
"or": "eller", "or": "eller",
"connectToJellyfin": "Anslut till Jellyfin", "connectToMediaBrowser": "",
"useQuickConnect": "Använd Quick Connect", "useQuickConnect": "Använd Quick Connect",
"quickConnectInstructions": "Öppna Quick Connect i Jellyfin och ange den här koden.", "quickConnectInstructions": "Öppna Quick Connect i Jellyfin och ange den här koden.",
"quickConnectWaiting": "Väntar på godkännande…", "quickConnectWaiting": "Väntar på godkännande…",
@@ -827,8 +827,8 @@
"sessionExpiredOne": "Sessionen har gått ut för ${name}", "sessionExpiredOne": "Sessionen har gått ut för ${name}",
"sessionExpiredMany": "Sessionen har gått ut för ${count} servrar", "sessionExpiredMany": "Sessionen har gått ut för ${count} servrar",
"signInAgain": "Logga in igen", "signInAgain": "Logga in igen",
"editJellyfinTitle": "Redigera Jellyfin-anslutning", "editMediaBrowserTitle": "",
"editJellyfinIntro": "Lägg till eller ta bort URL:er för ${serverName}. Plezy använder den nåbara URL som har lägst latens." "editMediaBrowserIntro": ""
}, },
"discover": { "discover": {
"title": "Upptäck", "title": "Upptäck",
@@ -1873,12 +1873,12 @@
} }
}, },
"addServer": { "addServer": {
"addJellyfinTitle": "Lägg till Jellyfin-server", "addMediaBrowserTitle": "",
"serverUrls": "Server-URL:er", "serverUrls": "Server-URL:er",
"serverUrlsHelper": "Du kan ange flera URL:er avgränsade med kommatecken.", "serverUrlsHelper": "Du kan ange flera URL:er avgränsade med kommatecken.",
"findServer": "Hitta server", "findServer": "Hitta server",
"searchingLocalServers": "Söker efter lokala Jellyfin-servrar...", "searchingLocalMediaBrowserServers": "",
"localServers": "Lokala Jellyfin-servrar", "localMediaBrowserServers": "",
"username": "Användarnamn", "username": "Användarnamn",
"password": "Lösenord", "password": "Lösenord",
"signIn": "Logga in", "signIn": "Logga in",
@@ -1890,15 +1890,15 @@
"addPlexTitle": "Logga in med Plex", "addPlexTitle": "Logga in med Plex",
"pinExpired": "PIN-koden gick ut innan inloggning. Försök igen.", "pinExpired": "PIN-koden gick ut innan inloggning. Försök igen.",
"failedToRegisterAccount": "Kunde inte registrera kontot: ${error}", "failedToRegisterAccount": "Kunde inte registrera kontot: ${error}",
"enterJellyfinUrlError": "Ange URL till din Jellyfin-server", "enterMediaBrowserUrlError": "",
"addConnectionTitle": "Lägg till anslutning", "addConnectionTitle": "Lägg till anslutning",
"addConnectionTitleScoped": "Lägg till i ${name}", "addConnectionTitleScoped": "Lägg till i ${name}",
"signInWithPlexCard": "Logga in med Plex", "signInWithPlexCard": "Logga in med Plex",
"signInWithPlexCardSubtitle": "Auktorisera den här enheten. Delade servrar läggs till.", "signInWithPlexCardSubtitle": "Auktorisera den här enheten. Delade servrar läggs till.",
"signInWithPlexCardSubtitleScoped": "Auktorisera ett Plex-konto. Home-användare blir profiler.", "signInWithPlexCardSubtitleScoped": "Auktorisera ett Plex-konto. Home-användare blir profiler.",
"connectToJellyfinCard": "Anslut till Jellyfin", "connectToMediaBrowserCard": "",
"connectToJellyfinCardSubtitle": "Ange server-URL, användarnamn och lösenord.", "connectToMediaBrowserCardSubtitle": "",
"connectToJellyfinCardSubtitleScoped": "Logga in på en Jellyfin-server. Kopplas till ${name}.", "connectToMediaBrowserCardSubtitleScoped": "",
"borrowFromAnotherProfile": "Låna från en annan profil", "borrowFromAnotherProfile": "Låna från en annan profil",
"borrowFromAnotherProfileSubtitle": "Återanvänd en annan profils anslutning. PIN-skyddade profiler kräver en PIN." "borrowFromAnotherProfileSubtitle": "Återanvänd en annan profils anslutning. PIN-skyddade profiler kräver en PIN."
} }
+10 -10
View File
@@ -11,7 +11,7 @@
"waitingForAuth": "Doğrulama bekleniyor...\nTarayıcınızdan giriş yapın.", "waitingForAuth": "Doğrulama bekleniyor...\nTarayıcınızdan giriş yapın.",
"useBrowser": "Tarayıcı kullan", "useBrowser": "Tarayıcı kullan",
"or": "veya", "or": "veya",
"connectToJellyfin": "Jellyfin'e Bağlan", "connectToMediaBrowser": "",
"useQuickConnect": "Hızlı Bağlantıyı Kullan", "useQuickConnect": "Hızlı Bağlantıyı Kullan",
"quickConnectInstructions": "Jellyfin'de Hızlı Bağlantı'yı açın ve bu kodu girin.", "quickConnectInstructions": "Jellyfin'de Hızlı Bağlantı'yı açın ve bu kodu girin.",
"quickConnectWaiting": "Onay bekleniyor…", "quickConnectWaiting": "Onay bekleniyor…",
@@ -827,8 +827,8 @@
"sessionExpiredOne": "${name} için oturum süresi doldu", "sessionExpiredOne": "${name} için oturum süresi doldu",
"sessionExpiredMany": "${count} sunucu için oturum süresi doldu", "sessionExpiredMany": "${count} sunucu için oturum süresi doldu",
"signInAgain": "Tekrar giriş yap", "signInAgain": "Tekrar giriş yap",
"editJellyfinTitle": "Jellyfin bağlantısını düzenle", "editMediaBrowserTitle": "",
"editJellyfinIntro": "${serverName} için URL'ler ekleyin veya kaldırın. Plezy, en düşük gecikmeye sahip ulaşılabilir URL'yi kullanacaktır." "editMediaBrowserIntro": ""
}, },
"discover": { "discover": {
"title": "Keşfet", "title": "Keşfet",
@@ -1873,12 +1873,12 @@
} }
}, },
"addServer": { "addServer": {
"addJellyfinTitle": "Jellyfin sunucusu ekle", "addMediaBrowserTitle": "",
"serverUrls": "Sunucu URL'leri", "serverUrls": "Sunucu URL'leri",
"serverUrlsHelper": "Virgülle ayrılmış birden fazla URL'ye izin verilir.", "serverUrlsHelper": "Virgülle ayrılmış birden fazla URL'ye izin verilir.",
"findServer": "Sunucu bul", "findServer": "Sunucu bul",
"searchingLocalServers": "Yerel Jellyfin sunucuları aranıyor...", "searchingLocalMediaBrowserServers": "",
"localServers": "Yerel Jellyfin sunucuları", "localMediaBrowserServers": "",
"username": "Kullanıcı adı", "username": "Kullanıcı adı",
"password": "Şifre", "password": "Şifre",
"signIn": "Giriş Yap", "signIn": "Giriş Yap",
@@ -1890,15 +1890,15 @@
"addPlexTitle": "Plex ile Giriş Yap", "addPlexTitle": "Plex ile Giriş Yap",
"pinExpired": "Giriş yapmadan önce PIN süresi doldu. Lütfen tekrar deneyin.", "pinExpired": "Giriş yapmadan önce PIN süresi doldu. Lütfen tekrar deneyin.",
"failedToRegisterAccount": "Hesap kaydı başarısız oldu: ${error}", "failedToRegisterAccount": "Hesap kaydı başarısız oldu: ${error}",
"enterJellyfinUrlError": "Jellyfin sunucu URL'nizi girin", "enterMediaBrowserUrlError": "",
"addConnectionTitle": "Bağlantı ekle", "addConnectionTitle": "Bağlantı ekle",
"addConnectionTitleScoped": "${name} profiline ekle", "addConnectionTitleScoped": "${name} profiline ekle",
"signInWithPlexCard": "Plex ile Giriş Yap", "signInWithPlexCard": "Plex ile Giriş Yap",
"signInWithPlexCardSubtitle": "Bu cihazı yetkilendirin. Paylaşılan sunucular eklenir.", "signInWithPlexCardSubtitle": "Bu cihazı yetkilendirin. Paylaşılan sunucular eklenir.",
"signInWithPlexCardSubtitleScoped": "Bir Plex hesabını yetkilendirin. Ev kullanıcıları profile dönüşür.", "signInWithPlexCardSubtitleScoped": "Bir Plex hesabını yetkilendirin. Ev kullanıcıları profile dönüşür.",
"connectToJellyfinCard": "Jellyfin'e Bağlan", "connectToMediaBrowserCard": "",
"connectToJellyfinCardSubtitle": "Sunucu URL'nizi, kullanıcı adınızı ve şifrenizi girin.", "connectToMediaBrowserCardSubtitle": "",
"connectToJellyfinCardSubtitleScoped": "Bir Jellyfin sunucusuna giriş yapın. ${name} profiline bağlanır.", "connectToMediaBrowserCardSubtitleScoped": "",
"borrowFromAnotherProfile": "Başka bir profilden ödünç al", "borrowFromAnotherProfile": "Başka bir profilden ödünç al",
"borrowFromAnotherProfileSubtitle": "Başka bir profilin bağlantısını yeniden kullanın. PIN korumalı profiller bir PIN gerektirir." "borrowFromAnotherProfileSubtitle": "Başka bir profilin bağlantısını yeniden kullanın. PIN korumalı profiller bir PIN gerektirir."
} }
+10 -10
View File
@@ -11,7 +11,7 @@
"waitingForAuth": "Tasdiqlanish kutilmoqda...\nBrauzeringizdan kiring.", "waitingForAuth": "Tasdiqlanish kutilmoqda...\nBrauzeringizdan kiring.",
"useBrowser": "Brauzerdan foydalanish", "useBrowser": "Brauzerdan foydalanish",
"or": "yoki", "or": "yoki",
"connectToJellyfin": "Jellyfin-ga ulanish", "connectToMediaBrowser": "",
"useQuickConnect": "Tezkor ulanishdan foydalanish", "useQuickConnect": "Tezkor ulanishdan foydalanish",
"quickConnectInstructions": "Jellyfin-da Tezkor ulanishni oching va ushbu kodni kiriting.", "quickConnectInstructions": "Jellyfin-da Tezkor ulanishni oching va ushbu kodni kiriting.",
"quickConnectWaiting": "Tasdiq kutilmoqda…", "quickConnectWaiting": "Tasdiq kutilmoqda…",
@@ -827,8 +827,8 @@
"sessionExpiredOne": "${name} uchun seans vaqti tugadi", "sessionExpiredOne": "${name} uchun seans vaqti tugadi",
"sessionExpiredMany": "${count} server uchun seans vaqti tugadi", "sessionExpiredMany": "${count} server uchun seans vaqti tugadi",
"signInAgain": "Qaytadan kirish", "signInAgain": "Qaytadan kirish",
"editJellyfinTitle": "Jellyfin ulanishini tahrirlash", "editMediaBrowserTitle": "",
"editJellyfinIntro": "${serverName} uchun URL manzilini qoʻshing yoki oʻchiring." "editMediaBrowserIntro": ""
}, },
"discover": { "discover": {
"title": "Kashf qilish", "title": "Kashf qilish",
@@ -1873,12 +1873,12 @@
} }
}, },
"addServer": { "addServer": {
"addJellyfinTitle": "Jellyfin serverini qoʻshish", "addMediaBrowserTitle": "",
"serverUrls": "Server URL-lari", "serverUrls": "Server URL-lari",
"serverUrlsHelper": "Vergul bilan ajratilgan bir nechta URL manziliga ruxsat beriladi.", "serverUrlsHelper": "Vergul bilan ajratilgan bir nechta URL manziliga ruxsat beriladi.",
"findServer": "Serverni topish", "findServer": "Serverni topish",
"searchingLocalServers": "Mahalliy Jellyfin serverlari qidirilmoqda...", "searchingLocalMediaBrowserServers": "",
"localServers": "Mahalliy Jellyfin serverlari", "localMediaBrowserServers": "",
"username": "Foydalanuvchi nomi", "username": "Foydalanuvchi nomi",
"password": "Parol", "password": "Parol",
"signIn": "Kirish", "signIn": "Kirish",
@@ -1890,15 +1890,15 @@
"addPlexTitle": "Plex orqali kirish", "addPlexTitle": "Plex orqali kirish",
"pinExpired": "PIN kod vaqti tugadi.", "pinExpired": "PIN kod vaqti tugadi.",
"failedToRegisterAccount": "Hisobni roʻyxatdan oʻtkazishda xatolik: ${error}", "failedToRegisterAccount": "Hisobni roʻyxatdan oʻtkazishda xatolik: ${error}",
"enterJellyfinUrlError": "Jellyfin server URL-ini kiriting", "enterMediaBrowserUrlError": "",
"addConnectionTitle": "Ulanish qoʻshish", "addConnectionTitle": "Ulanish qoʻshish",
"addConnectionTitleScoped": "${name} profiliga qoʻshish", "addConnectionTitleScoped": "${name} profiliga qoʻshish",
"signInWithPlexCard": "Plex orqali kirish", "signInWithPlexCard": "Plex orqali kirish",
"signInWithPlexCardSubtitle": "Ushbu qurilmani avtorizatsiya qiling.", "signInWithPlexCardSubtitle": "Ushbu qurilmani avtorizatsiya qiling.",
"signInWithPlexCardSubtitleScoped": "Plex hisobini avtorizatsiya qiling.", "signInWithPlexCardSubtitleScoped": "Plex hisobini avtorizatsiya qiling.",
"connectToJellyfinCard": "Jellyfin-ga ulanish", "connectToMediaBrowserCard": "",
"connectToJellyfinCardSubtitle": "Server URL, foydalanuvchi nomi va parolingizni kiriting.", "connectToMediaBrowserCardSubtitle": "",
"connectToJellyfinCardSubtitleScoped": "Jellyfin serveriga kiring. ${name} profiliga ulanmoqda.", "connectToMediaBrowserCardSubtitleScoped": "",
"borrowFromAnotherProfile": "Boshqa profildan olish", "borrowFromAnotherProfile": "Boshqa profildan olish",
"borrowFromAnotherProfileSubtitle": "Boshqa profilning ulanishidan qayta foydalaning." "borrowFromAnotherProfileSubtitle": "Boshqa profilning ulanishidan qayta foydalaning."
} }
+10 -10
View File
@@ -11,7 +11,7 @@
"waitingForAuth": "正在等待驗證…\n請在瀏覽器中登入。", "waitingForAuth": "正在等待驗證…\n請在瀏覽器中登入。",
"useBrowser": "使用瀏覽器", "useBrowser": "使用瀏覽器",
"or": "或", "or": "或",
"connectToJellyfin": "連線至 Jellyfin", "connectToMediaBrowser": "",
"useQuickConnect": "使用快速連線(Quick Connect", "useQuickConnect": "使用快速連線(Quick Connect",
"quickConnectInstructions": "在 Jellyfin 中開啟快速連線並輸入此代碼。", "quickConnectInstructions": "在 Jellyfin 中開啟快速連線並輸入此代碼。",
"quickConnectWaiting": "等待核准…", "quickConnectWaiting": "等待核准…",
@@ -826,8 +826,8 @@
"sessionExpiredOne": "${name} 的工作階段已過期", "sessionExpiredOne": "${name} 的工作階段已過期",
"sessionExpiredMany": "${count} 個伺服器的工作階段已過期", "sessionExpiredMany": "${count} 個伺服器的工作階段已過期",
"signInAgain": "重新登入", "signInAgain": "重新登入",
"editJellyfinTitle": "編輯 Jellyfin 連線", "editMediaBrowserTitle": "",
"editJellyfinIntro": "新增或移除 ${serverName} 的 URL。Plezy 會自動選擇可連線且延遲最低的網址。" "editMediaBrowserIntro": ""
}, },
"discover": { "discover": {
"title": "發現", "title": "發現",
@@ -1867,12 +1867,12 @@
} }
}, },
"addServer": { "addServer": {
"addJellyfinTitle": "新增 Jellyfin 伺服器", "addMediaBrowserTitle": "",
"serverUrls": "伺服器 URL", "serverUrls": "伺服器 URL",
"serverUrlsHelper": "可輸入多個連線網址,以逗號區隔。", "serverUrlsHelper": "可輸入多個連線網址,以逗號區隔。",
"findServer": "尋找伺服器", "findServer": "尋找伺服器",
"searchingLocalServers": "正在尋找本地 Jellyfin 伺服器…", "searchingLocalMediaBrowserServers": "",
"localServers": "本地 Jellyfin 伺服器", "localMediaBrowserServers": "",
"username": "使用者名稱", "username": "使用者名稱",
"password": "密碼", "password": "密碼",
"signIn": "登入", "signIn": "登入",
@@ -1884,15 +1884,15 @@
"addPlexTitle": "使用 Plex 登入", "addPlexTitle": "使用 Plex 登入",
"pinExpired": "PIN 碼在登入前已過期。請重試。", "pinExpired": "PIN 碼在登入前已過期。請重試。",
"failedToRegisterAccount": "註冊帳戶失敗:${error}", "failedToRegisterAccount": "註冊帳戶失敗:${error}",
"enterJellyfinUrlError": "請輸入您的 Jellyfin 伺服器 URL", "enterMediaBrowserUrlError": "",
"addConnectionTitle": "新增連線", "addConnectionTitle": "新增連線",
"addConnectionTitleScoped": "新增連線至 ${name}", "addConnectionTitleScoped": "新增連線至 ${name}",
"signInWithPlexCard": "使用 Plex 登入", "signInWithPlexCard": "使用 Plex 登入",
"signInWithPlexCardSubtitle": "授權此裝置。將會新增共享的伺服器連線。", "signInWithPlexCardSubtitle": "授權此裝置。將會新增共享的伺服器連線。",
"signInWithPlexCardSubtitleScoped": "授權 Plex 帳戶。Home 使用者會建立為個別的使用者設定檔。", "signInWithPlexCardSubtitleScoped": "授權 Plex 帳戶。Home 使用者會建立為個別的使用者設定檔。",
"connectToJellyfinCard": "連線至 Jellyfin", "connectToMediaBrowserCard": "",
"connectToJellyfinCardSubtitle": "輸入伺服器 URL、使用者名稱與密碼。", "connectToMediaBrowserCardSubtitle": "",
"connectToJellyfinCardSubtitleScoped": "登入 Jellyfin 伺服器,並綁定至 ${name} 使用者設定檔。", "connectToMediaBrowserCardSubtitleScoped": "",
"borrowFromAnotherProfile": "從另一個使用者設定檔共用", "borrowFromAnotherProfile": "從另一個使用者設定檔共用",
"borrowFromAnotherProfileSubtitle": "重複使用另一個使用者設定檔的連線資訊。受 PIN 碼保護的使用者設定檔需輸入 PIN 碼。" "borrowFromAnotherProfileSubtitle": "重複使用另一個使用者設定檔的連線資訊。受 PIN 碼保護的使用者設定檔需輸入 PIN 碼。"
} }
+10 -10
View File
@@ -11,7 +11,7 @@
"waitingForAuth": "正在等待身份验证…\n请在浏览器中登录。", "waitingForAuth": "正在等待身份验证…\n请在浏览器中登录。",
"useBrowser": "使用浏览器", "useBrowser": "使用浏览器",
"or": "或", "or": "或",
"connectToJellyfin": "连接到 Jellyfin", "connectToMediaBrowser": "",
"useQuickConnect": "使用 Quick Connect", "useQuickConnect": "使用 Quick Connect",
"quickConnectInstructions": "在 Jellyfin 中打开 Quick Connect 并输入此代码。", "quickConnectInstructions": "在 Jellyfin 中打开 Quick Connect 并输入此代码。",
"quickConnectWaiting": "等待批准…", "quickConnectWaiting": "等待批准…",
@@ -826,8 +826,8 @@
"sessionExpiredOne": "${name} 的会话已过期", "sessionExpiredOne": "${name} 的会话已过期",
"sessionExpiredMany": "${count} 个服务器的会话已过期", "sessionExpiredMany": "${count} 个服务器的会话已过期",
"signInAgain": "重新登录", "signInAgain": "重新登录",
"editJellyfinTitle": "编辑 Jellyfin 连接", "editMediaBrowserTitle": "",
"editJellyfinIntro": "添加或移除 ${serverName} 的 URL。Plezy 会使用可访问且延迟最低的地址。" "editMediaBrowserIntro": ""
}, },
"discover": { "discover": {
"title": "发现", "title": "发现",
@@ -1867,12 +1867,12 @@
} }
}, },
"addServer": { "addServer": {
"addJellyfinTitle": "添加 Jellyfin 服务器", "addMediaBrowserTitle": "",
"serverUrls": "服务器 URL", "serverUrls": "服务器 URL",
"serverUrlsHelper": "可输入多个 URL,并用逗号分隔。", "serverUrlsHelper": "可输入多个 URL,并用逗号分隔。",
"findServer": "查找服务器", "findServer": "查找服务器",
"searchingLocalServers": "正在查找本地 Jellyfin 服务器…", "searchingLocalMediaBrowserServers": "",
"localServers": "本地 Jellyfin 服务器", "localMediaBrowserServers": "",
"username": "用户名", "username": "用户名",
"password": "密码", "password": "密码",
"signIn": "登录", "signIn": "登录",
@@ -1884,15 +1884,15 @@
"addPlexTitle": "使用 Plex 登录", "addPlexTitle": "使用 Plex 登录",
"pinExpired": "PIN 在登录前已过期。请重试。", "pinExpired": "PIN 在登录前已过期。请重试。",
"failedToRegisterAccount": "注册账户失败:${error}", "failedToRegisterAccount": "注册账户失败:${error}",
"enterJellyfinUrlError": "请输入 Jellyfin 服务器 URL", "enterMediaBrowserUrlError": "",
"addConnectionTitle": "添加连接", "addConnectionTitle": "添加连接",
"addConnectionTitleScoped": "添加到 ${name}", "addConnectionTitleScoped": "添加到 ${name}",
"signInWithPlexCard": "使用 Plex 登录", "signInWithPlexCard": "使用 Plex 登录",
"signInWithPlexCardSubtitle": "授权此设备。共享服务器会被添加。", "signInWithPlexCardSubtitle": "授权此设备。共享服务器会被添加。",
"signInWithPlexCardSubtitleScoped": "授权一个 Plex 账户。Plex Home 用户将成为 Plezy 用户资料。", "signInWithPlexCardSubtitleScoped": "授权一个 Plex 账户。Plex Home 用户将成为 Plezy 用户资料。",
"connectToJellyfinCard": "连接到 Jellyfin", "connectToMediaBrowserCard": "",
"connectToJellyfinCardSubtitle": "输入服务器 URL、用户名和密码。", "connectToMediaBrowserCardSubtitle": "",
"connectToJellyfinCardSubtitleScoped": "登录到 Jellyfin 服务器。绑定到 ${name}。", "connectToMediaBrowserCardSubtitleScoped": "",
"borrowFromAnotherProfile": "使用其他用户资料的连接", "borrowFromAnotherProfile": "使用其他用户资料的连接",
"borrowFromAnotherProfileSubtitle": "复用另一个用户资料的连接。受 PIN 保护的用户资料需要输入 PIN。" "borrowFromAnotherProfileSubtitle": "复用另一个用户资料的连接。受 PIN 保护的用户资料需要输入 PIN。"
} }
+5 -5
View File
@@ -1857,7 +1857,7 @@ class _SetupScreenState extends State<SetupScreen> with MountedSetStateMixin {
serverManager: serverManager, serverManager: serverManager,
).pruneUnreferencedJellyfinConnections(); ).pruneUnreferencedJellyfinConnections();
if (pruned > 0) { if (pruned > 0) {
appLogger.i('Setup: pruned $pruned unreferenced Jellyfin connection${pruned == 1 ? '' : 's'}'); appLogger.i('Setup: pruned $pruned unreferenced MediaBrowser connection${pruned == 1 ? '' : 's'}');
} }
// Provider initialization starts before this screen runs the legacy // Provider initialization starts before this screen runs the legacy
// migration. Reload after bootstrap so copied Plex Home users and the // migration. Reload after bootstrap so copied Plex Home users and the
@@ -1943,11 +1943,11 @@ class _SetupScreenState extends State<SetupScreen> with MountedSetStateMixin {
} }
final plexCount = allConnections.whereType<PlexAccountConnection>().fold<int>(0, (n, c) => n + c.servers.length); final plexCount = allConnections.whereType<PlexAccountConnection>().fold<int>(0, (n, c) => n + c.servers.length);
final jellyfinCount = allConnections.whereType<JellyfinConnection>().length; final mediaBrowserCount = allConnections.whereType<JellyfinConnection>().length;
unawaited( unawaited(
Sentry.addBreadcrumb( Sentry.addBreadcrumb(
Breadcrumb( Breadcrumb(
message: 'Handing off to MainScreen with $plexCount Plex server(s) + $jellyfinCount Jellyfin', message: 'Handing off to MainScreen with $plexCount Plex + $mediaBrowserCount MediaBrowser server(s)',
category: 'setup', category: 'setup',
), ),
), ),
@@ -2003,7 +2003,7 @@ class _SetupScreenState extends State<SetupScreen> with MountedSetStateMixin {
if (!mounted) return; if (!mounted) return;
bindingSucceeded = activeProfile.active != null && activeProfile.lastBindingSucceeded; bindingSucceeded = activeProfile.active != null && activeProfile.lastBindingSucceeded;
} else { } else {
// Now wait for the binder to settle. This is the Plex/Jellyfin server // Now wait for the binder to settle. This is the media-server client
// race: per-server status flips on the splash list as each client comes // race: per-server status flips on the splash list as each client comes
// online, and we don't push MainScreen until they're all done (success // online, and we don't push MainScreen until they're all done (success
// or fail). Eliminates the "Failed to load discover content: No servers // or fail). Eliminates the "Failed to load discover content: No servers
@@ -2023,7 +2023,7 @@ class _SetupScreenState extends State<SetupScreen> with MountedSetStateMixin {
} }
// Repopulate metadata for downloaded items now that per-backend caches // Repopulate metadata for downloaded items now that per-backend caches
// are resolvable (the Connections row + live JellyfinClient are in // are resolvable (the Connections row + live MediaBrowser client are in
// place). Without this the downloads list and sync-rule titles render // place). Without this the downloads list and sync-rule titles render
// empty until something forces a later refresh. // empty until something forces a later refresh.
await downloadProvider.refreshMetadataFromCache(); await downloadProvider.refreshMetadataFromCache();
+20 -2
View File
@@ -1,4 +1,5 @@
import '../utils/app_logger.dart'; import '../utils/app_logger.dart';
import 'media_browser_dialect.dart';
/// Backend identifier for a media item, library, or server. /// Backend identifier for a media item, library, or server.
/// ///
@@ -7,16 +8,19 @@ import '../utils/app_logger.dart';
/// in v1) and so persisted records can round-trip the source of an item. /// in v1) and so persisted records can round-trip the source of an item.
enum MediaBackend { enum MediaBackend {
plex, plex,
jellyfin; jellyfin,
emby;
String get id => switch (this) { String get id => switch (this) {
MediaBackend.plex => 'plex', MediaBackend.plex => 'plex',
MediaBackend.jellyfin => 'jellyfin', MediaBackend.jellyfin => 'jellyfin',
MediaBackend.emby => 'emby',
}; };
static MediaBackend fromId(String id) => switch (id) { static MediaBackend fromId(String id) => switch (id) {
'plex' => MediaBackend.plex, 'plex' => MediaBackend.plex,
'jellyfin' => MediaBackend.jellyfin, 'jellyfin' => MediaBackend.jellyfin,
'emby' => MediaBackend.emby,
_ => throw ArgumentError('Unknown MediaBackend id: $id'), _ => throw ArgumentError('Unknown MediaBackend id: $id'),
}; };
@@ -27,12 +31,26 @@ enum MediaBackend {
/// surfaces corrupted cache rows or schema drift instead of silently /// surfaces corrupted cache rows or schema drift instead of silently
/// misclassifying Jellyfin items as Plex. /// misclassifying Jellyfin items as Plex.
static MediaBackend fromString(String? id) { static MediaBackend fromString(String? id) {
if (id != null && id != 'plex' && id != 'jellyfin') { if (id != null && id != 'plex' && id != 'jellyfin' && id != 'emby') {
appLogger.w('Unknown MediaBackend id "$id"; defaulting to plex'); appLogger.w('Unknown MediaBackend id "$id"; defaulting to plex');
} }
return switch (id) { return switch (id) {
'jellyfin' => MediaBackend.jellyfin, 'jellyfin' => MediaBackend.jellyfin,
'emby' => MediaBackend.emby,
_ => MediaBackend.plex, _ => MediaBackend.plex,
}; };
} }
/// True for backends served by the MediaBrowser HTTP API — Jellyfin and its
/// Emby ancestor. They share one client stack, one query grammar and one set
/// of DTO shapes, so behaviour keyed to "not Plex" should test this instead
/// of comparing against [MediaBackend.jellyfin].
bool get usesMediaBrowserApi => dialect != null;
/// The MediaBrowser dialect this backend speaks, or `null` for Plex.
MediaBrowserDialect? get dialect => switch (this) {
MediaBackend.plex => null,
MediaBackend.jellyfin => MediaBrowserDialect.jellyfin,
MediaBackend.emby => MediaBrowserDialect.emby,
};
} }
+189
View File
@@ -0,0 +1,189 @@
import 'media_backend.dart';
/// Which flavour of the MediaBrowser HTTP API a server speaks.
///
/// Jellyfin forked from Emby 3.5.2, so the two still share almost their entire
/// wire contract: identical `BaseItemDto` shapes, the same `/Items` query
/// grammar, the `MediaBrowser` Authorization scheme, the `X-Emby-Token` header
/// and `api_key=` query fallback. Plezy therefore drives both through one
/// client stack ([JellyfinClient]) and keeps every delta in this one type.
///
/// Verified against Jellyfin 10.10.7/10.11 and Emby 4.9.5:
/// - Jellyfin 10.9 renamed a batch of user-scoped write routes to unprefixed
/// forms and added `/Users/Me`. Emby only has the original user-scoped
/// spellings — see [MediaBrowserPaths].
/// - Trickplay, `/MediaSegments`, `/Audio/{id}/Lyrics`, `/Items/Filters` and
/// Quick Connect do not exist on Emby. `/Audio/{id}/Lyrics` is actively
/// harmful there: Emby parses `Lyrics` as a container name and starts an
/// ffmpeg transcode.
/// - Emby tolerates unknown `Fields`/`SortBy` values, so the shared field sets
/// need no per-dialect pruning.
enum MediaBrowserDialect {
jellyfin,
emby;
/// Stable wire/persistence id. Matches the [MediaBackend] and
/// `ConnectionKind` ids for the same server kind.
String get id => switch (this) {
MediaBrowserDialect.jellyfin => 'jellyfin',
MediaBrowserDialect.emby => 'emby',
};
static MediaBrowserDialect fromId(String id) => switch (id) {
'jellyfin' => MediaBrowserDialect.jellyfin,
'emby' => MediaBrowserDialect.emby,
_ => throw ArgumentError('Unknown MediaBrowserDialect id: $id'),
};
/// Like [fromId] but tolerates legacy/missing values by defaulting to
/// Jellyfin. Persisted connection rows written before Emby support carry no
/// `dialect` key at all, and those are Jellyfin by construction.
static MediaBrowserDialect fromIdOrJellyfin(Object? id) => switch (id) {
'emby' => MediaBrowserDialect.emby,
_ => MediaBrowserDialect.jellyfin,
};
MediaBackend get backend => switch (this) {
MediaBrowserDialect.jellyfin => MediaBackend.jellyfin,
MediaBrowserDialect.emby => MediaBackend.emby,
};
/// Product name, used verbatim in UI that names the backend. These are
/// trademarks, so they are not localized.
String get productName => switch (this) {
MediaBrowserDialect.jellyfin => 'Jellyfin',
MediaBrowserDialect.emby => 'Emby',
};
/// Placeholder host shown in the "server URL" field.
String get exampleBaseUrl => switch (this) {
MediaBrowserDialect.jellyfin => 'https://jellyfin.example.com',
MediaBrowserDialect.emby => 'https://emby.example.com',
};
/// UDP payload the server answers on port 7359. Emby ignores Jellyfin's
/// string and vice versa, which makes the datagram itself a reliable
/// dialect discriminator during LAN discovery.
String get lanDiscoveryMessage => switch (this) {
MediaBrowserDialect.jellyfin => 'who is JellyfinServer?',
MediaBrowserDialect.emby => 'who is EmbyServer?',
};
/// Ports appended when the user types a bare host, most-likely first.
/// Both ship 8096 for HTTP; Emby's default HTTPS port is 8920.
List<int> get httpsPortGuesses => switch (this) {
MediaBrowserDialect.jellyfin => const [8096],
MediaBrowserDialect.emby => const [8920, 8096],
};
/// `/QuickConnect/*` plus `POST /Users/AuthenticateWithQuickConnect`.
bool get supportsQuickConnect => this == MediaBrowserDialect.jellyfin;
/// `/Videos/{id}/Trickplay/{width}/{n}.jpg` sprite sheets and the
/// `Trickplay` item field (Jellyfin 10.9+). Emby 404s on the route and never
/// fills the field; its own preview transports are unwired — see
/// [ServerCapabilities.emby].
bool get supportsTrickplay => this == MediaBrowserDialect.jellyfin;
/// `/MediaSegments/{itemId}` intro/outro/credit markers (Jellyfin 10.10+).
/// Emby 404s; chapter-name fallback still applies.
bool get supportsMediaSegments => this == MediaBrowserDialect.jellyfin;
/// `GET /Audio/{id}/Lyrics` (Jellyfin 10.9+). Never call this on Emby: the
/// route resolves to audio streaming with `Lyrics` as the container and
/// spawns an ffmpeg process that fails with a 500.
bool get supportsLyrics => this == MediaBrowserDialect.jellyfin;
/// `GET /Items/Filters`, the single call that returns a library's distinct
/// genres, official ratings, tags and years. Emby has no aggregate route; the
/// client reassembles the same payload from `/Genres`, `/OfficialRatings`,
/// `/Tags` and `/Years`.
bool get supportsAggregateItemFilters => this == MediaBrowserDialect.jellyfin;
/// `POST /Users/{uid}/Items/{id}/HideFromResume` hides an item from Continue
/// Watching without clearing its resume position.
///
/// Emby-only, and the one capability where Emby is ahead of Jellyfin:
/// measured 200 on Emby 4.9.5 (the row leaves `/Users/{uid}/Items/Resume`
/// while `UserData.PlaybackPositionTicks` survives), and 404 on Jellyfin
/// 10.11 for both that spelling and `/UserItems/{id}/HideFromResume`.
bool get supportsContinueWatchingRemoval => this == MediaBrowserDialect.emby;
/// `POST /Items/{id}` persists genre and tag edits from the `GenreItems` /
/// `TagItems` name-pair arrays rather than the plain `Genres` / `Tags` string
/// lists.
///
/// Measured on Emby 4.9.5: sending `Genres: ['Action']` alone round-trips as
/// an empty list and the `/Genres` facet stays empty, while
/// `GenreItems: [{'Name': 'Action'}]` sticks and is immediately indexed. The
/// sibling fields (`Studios`, `People`, `ProductionLocations`, `Taglines`,
/// `Overview`, `OriginalTitle`) all persist from their ordinary shapes on
/// both dialects.
bool get metadataWritesUseNamePairLists => this == MediaBrowserDialect.emby;
/// `GET /Shows/NextUp` answers an unscoped, library-wide query.
///
/// Jellyfin-only. Measured on Emby 4.9.5 with one played episode: the
/// unscoped query returns `TotalRecordCount: 0` under every parameter
/// combination tried (`ParentId` on the view or the series, `SeriesId=`,
/// `Recursive`, `GroupItems`, `EnableResumable`, `SortBy`), while the same
/// query with `SeriesId=<series>` returns the series' 23 remaining episodes.
/// Emby therefore only computes Next Up per series, and the library-wide
/// shelf has to be reconstructed client-side from recently played episodes.
bool get supportsGlobalNextUp => this == MediaBrowserDialect.jellyfin;
/// The resume route returns only items with a saved playback position.
///
/// Jellyfin-only. Measured with one in-progress movie and 30 started series:
/// Jellyfin's `/UserItems/Resume` returned exactly the movie, while Emby's
/// `/Users/{uid}/Items/Resume` returned 30 rows — the movie plus 29
/// zero-position *next* episodes — and `Filters=IsResumable` did not remove
/// them. Emby's own UI merges both into one shelf; Plezy models Continue
/// Watching and Next Up as separate rows, so the Emby resume leg is filtered
/// to genuine progress and the next-up rows come from the Next Up path
/// instead. Without the filter a started series occupies both rows and can
/// push the real in-progress item out of a limited one.
bool get resumeReturnsOnlyStartedItems => this == MediaBrowserDialect.jellyfin;
/// `/Sessions/Playing` and `/Sessions/Playing/Progress` reject a body with no
/// `PlaySessionId`.
///
/// Measured on Emby 4.9.5: both return HTTP 400 `Value cannot be null.
/// (Parameter 'key')` when the field is absent, while
/// `/Sessions/Playing/Stopped` tolerates it. Jellyfin accepts all three
/// without one. Callers that never negotiated a PlaybackInfo session — the
/// offline watch-progress sync is the live example — therefore need a
/// synthesized id on Emby or their progress is silently dropped.
bool get requiresPlaySessionId => this == MediaBrowserDialect.emby;
/// `/Items?IncludeItemTypes=Playlist` honours a `MediaTypes` filter.
///
/// Measured on Emby 4.9.5: passing *any* `MediaTypes` value makes the server
/// discard `IncludeItemTypes` and return the whole index — 14554 rows of
/// `Genre`/`Person`/`Studio`/`Movie` instead of the one playlist. Emby also
/// never populates `MediaType` on a playlist DTO, not even for a playlist
/// created with `MediaType=Audio`, so there is nothing to filter on either
/// side and every playlist is returned regardless of the requested type.
bool get playlistsFilterByMediaType => this == MediaBrowserDialect.jellyfin;
/// True when the dialect only accepts the pre-10.9 `/Users/{userId}/…`
/// spelling of the user-scoped item routes.
bool get requiresUserScopedItemRoutes => this == MediaBrowserDialect.emby;
/// Best-effort dialect detection from a `/System/Info/Public` body.
///
/// Jellyfin reports `ProductName: "Jellyfin Server"`. Emby 4.9 omits
/// `ProductName` entirely but is the only one of the two that returns the
/// `RemoteAddresses` array. Returns `null` when neither signal is present so
/// callers keep whichever dialect the user picked.
static MediaBrowserDialect? detectFromPublicSystemInfo(Map<String, Object?> json) {
final productName = json['ProductName'];
if (productName is String && productName.isNotEmpty) {
final normalized = productName.toLowerCase();
if (normalized.contains('jellyfin')) return MediaBrowserDialect.jellyfin;
if (normalized.contains('emby')) return MediaBrowserDialect.emby;
}
if (json.containsKey('RemoteAddresses')) return MediaBrowserDialect.emby;
return null;
}
}
+21 -4
View File
@@ -7,6 +7,7 @@ import '../services/settings_service.dart' show EpisodePosterMode;
import '../utils/global_key_utils.dart'; import '../utils/global_key_utils.dart';
import '../utils/json_utils.dart'; import '../utils/json_utils.dart';
import 'media_backend.dart'; import 'media_backend.dart';
import 'media_browser_dialect.dart';
import 'media_kind.dart'; import 'media_kind.dart';
import 'media_role.dart'; import 'media_role.dart';
import 'media_version.dart'; import 'media_version.dart';
@@ -23,6 +24,10 @@ const double _squareHeroAspectRatio = 1.39;
/// Backend-neutral media item shape used by UI, providers, persistence, and /// Backend-neutral media item shape used by UI, providers, persistence, and
/// playback. Concrete variants retain backend-only fields without forcing the /// playback. Concrete variants retain backend-only fields without forcing the
/// rest of the app to traffic in Plex/Jellyfin DTOs. /// rest of the app to traffic in Plex/Jellyfin DTOs.
///
/// [JellyfinMediaItem] backs both MediaBrowser-family backends: Jellyfin and
/// Emby return field-identical `BaseItemDto`s, so they share one variant and
/// carry [JellyfinMediaItem.dialect] to tell them apart.
@Freezed(unionKey: 'backend', unionValueCase: FreezedUnionCase.none, equal: false, makeCollectionsUnmodifiable: false) @Freezed(unionKey: 'backend', unionValueCase: FreezedUnionCase.none, equal: false, makeCollectionsUnmodifiable: false)
sealed class MediaItem with _$MediaItem { sealed class MediaItem with _$MediaItem {
const MediaItem._(); const MediaItem._();
@@ -154,7 +159,8 @@ sealed class MediaItem with _$MediaItem {
backendFolderKey: backendFolderKey, backendFolderKey: backendFolderKey,
raw: raw, raw: raw,
), ),
MediaBackend.jellyfin => JellyfinMediaItem( MediaBackend.jellyfin || MediaBackend.emby => JellyfinMediaItem(
dialect: backend.dialect!,
id: id, id: id,
kind: kind, kind: kind,
guid: guid, guid: guid,
@@ -300,10 +306,20 @@ sealed class MediaItem with _$MediaItem {
@JsonKey(fromJson: _mediaItemRawFromJson) Map<String, Object?>? raw, @JsonKey(fromJson: _mediaItemRawFromJson) Map<String, Object?>? raw,
}) = PlexMediaItem; }) = PlexMediaItem;
/// Backend-tagged concrete subclass for items sourced from a Jellyfin server. /// Backend-tagged concrete subclass for items sourced from a MediaBrowser
/// server — Jellyfin or Emby, discriminated by [JellyfinMediaItem.dialect].
@FreezedUnionValue('jellyfin') @FreezedUnionValue('jellyfin')
@JsonSerializable(includeIfNull: false, explicitToJson: true) @JsonSerializable(includeIfNull: false, explicitToJson: true)
const factory MediaItem.jellyfin({ const factory MediaItem.jellyfin({
/// Which MediaBrowser dialect produced this item.
///
/// Not serialized on its own: [MediaItem.toJson] already writes the
/// resolved [backend] id under the union key, and [MediaItem.fromJson]
/// restores the dialect from it. Keeping one discriminator on the wire
/// avoids the two disagreeing on a stale cache row.
@JsonKey(includeToJson: false, includeFromJson: false)
@Default(MediaBrowserDialect.jellyfin)
MediaBrowserDialect dialect,
@JsonKey(readValue: readStringField, defaultValue: '') required String id, @JsonKey(readValue: readStringField, defaultValue: '') required String id,
@JsonKey(fromJson: _mediaKindFromJson, toJson: _mediaKindToJson) required MediaKind kind, @JsonKey(fromJson: _mediaKindFromJson, toJson: _mediaKindToJson) required MediaKind kind,
String? guid, String? guid,
@@ -375,7 +391,7 @@ sealed class MediaItem with _$MediaItem {
MediaBackend get backend => switch (this) { MediaBackend get backend => switch (this) {
PlexMediaItem() => MediaBackend.plex, PlexMediaItem() => MediaBackend.plex,
JellyfinMediaItem() => MediaBackend.jellyfin, JellyfinMediaItem(:final dialect) => dialect.backend,
}; };
/// Restore a [MediaItem] from a [toJson] payload. Missing/unknown backend /// Restore a [MediaItem] from a [toJson] payload. Missing/unknown backend
@@ -385,13 +401,14 @@ sealed class MediaItem with _$MediaItem {
return switch (MediaBackend.fromString(json['backend'] as String?)) { return switch (MediaBackend.fromString(json['backend'] as String?)) {
MediaBackend.plex => _$PlexMediaItemFromJson(json), MediaBackend.plex => _$PlexMediaItemFromJson(json),
MediaBackend.jellyfin => _$JellyfinMediaItemFromJson(json), MediaBackend.jellyfin => _$JellyfinMediaItemFromJson(json),
MediaBackend.emby => _$JellyfinMediaItemFromJson(json).copyWith(dialect: MediaBrowserDialect.emby),
}; };
} }
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
return switch (this) { return switch (this) {
final PlexMediaItem item => {'backend': MediaBackend.plex.id, ..._$PlexMediaItemToJson(item)}, final PlexMediaItem item => {'backend': MediaBackend.plex.id, ..._$PlexMediaItemToJson(item)},
final JellyfinMediaItem item => {'backend': MediaBackend.jellyfin.id, ..._$JellyfinMediaItemToJson(item)}, final JellyfinMediaItem item => {'backend': item.dialect.backend.id, ..._$JellyfinMediaItemToJson(item)},
}; };
} }
File diff suppressed because one or more lines are too long
+37
View File
@@ -115,6 +115,43 @@ class ServerCapabilities {
instantMix: true, instantMix: true,
); );
/// Defaults for an Emby server.
///
/// `continueWatchingRemoval` is the one flag where Emby is ahead of Jellyfin:
/// `POST /Users/{uid}/Items/{id}/HideFromResume` drops an item from Continue
/// Watching while keeping its resume position, and Jellyfin 10.11 has no
/// equivalent route.
///
/// Otherwise identical to [jellyfin] except [scrubThumbnails]: seek-bar
/// previews come
/// from Jellyfin's `/Videos/{id}/Trickplay` sprite sheets, which Emby (the
/// pre-fork ancestor) never gained — it 404s and never populates the
/// `Trickplay` item field. With the flag off the player never attempts the
/// load; see [MediaBrowserDialect.supportsTrickplay].
///
/// Emby does expose two *other* preview transports, `/Videos/{id}/index.bif`
/// (the same BIF format Plex uses, so [BifThumbnailService] could parse it)
/// and `/Items/{id}/ThumbnailSet`. Neither is wired: on Emby 4.9.5 both
/// answer 200 with an empty payload — a 72-byte header-only BIF and
/// `{"Thumbnails": []}` — even after a full metadata+image refresh, because
/// Emby only fills them once its own extraction task has run. Wiring them
/// needs a server that has actually generated the frames, so the flag stays
/// `false` rather than shipping a path that cannot be verified.
static const ServerCapabilities emby = ServerCapabilities(
liveTv: true,
liveTvDvr: false,
videoTranscoding: true,
richHubs: false,
numericUserRating: false,
userFavorites: true,
continueWatchingRemoval: true,
externalSubtitleSearch: false,
richMetadataEdit: true,
scrubThumbnails: false,
folderGrouping: true,
instantMix: true,
);
/// Every flag here is fixed per backend *kind* except [videoTranscoding], /// Every flag here is fixed per backend *kind* except [videoTranscoding],
/// which Plex probes per server (`PlexClient.capabilities`) — so that is the /// which Plex probes per server (`PlexClient.capabilities`) — so that is the
/// only override this type needs. Widen the parameter list if another flag /// only override this type needs. Widen the parameter list if another flag
@@ -13,7 +13,7 @@ class JellyfinMetadataEditAdapter extends MetadataEditAdapter {
JellyfinMetadataEditAdapter(this.client); JellyfinMetadataEditAdapter(this.client);
@override @override
MediaBackend get backend => MediaBackend.jellyfin; MediaBackend get backend => client.backend;
@override @override
MediaServerClient get mediaClient => client; MediaServerClient get mediaClient => client;
@@ -26,7 +26,7 @@ class JellyfinMetadataEditAdapter extends MetadataEditAdapter {
Future<MetadataEditDraft> load(MediaItem item) async { Future<MetadataEditDraft> load(MediaItem item) async {
final raw = await client.fetchEditableMetadataItem(item.id); final raw = await client.fetchEditableMetadataItem(item.id);
if (raw == null) { if (raw == null) {
throw StateError('Editable Jellyfin metadata item is unavailable'); throw StateError('Editable MediaBrowser metadata item is unavailable');
} }
final values = <String, Object?>{}; final values = <String, Object?>{};
_writeCommonValues(values, raw, item); _writeCommonValues(values, raw, item);
@@ -56,8 +56,8 @@ class JellyfinMetadataEditAdapter extends MetadataEditAdapter {
final dto = Map<String, dynamic>.from(raw); final dto = Map<String, dynamic>.from(raw);
dto['ProviderIds'] = _stringMap(dto['ProviderIds']); dto['ProviderIds'] = _stringMap(dto['ProviderIds']);
dto['Tags'] = metadataStringList(dto['Tags']); dto['Tags'] = _namedStringList(dto, 'Tags', 'TagItems');
dto['Genres'] = metadataStringList(dto['Genres']); dto['Genres'] = _namedStringList(dto, 'Genres', 'GenreItems');
dto['People'] = _mapList(dto['People']); dto['People'] = _mapList(dto['People']);
dto['Studios'] = _mapList(dto['Studios']); dto['Studios'] = _mapList(dto['Studios']);
dto['LockedFields'] = metadataStringList(dto['LockedFields']); dto['LockedFields'] = metadataStringList(dto['LockedFields']);
@@ -102,6 +102,17 @@ class JellyfinMetadataEditAdapter extends MetadataEditAdapter {
} }
if (peopleChanged) dto['People'] = people; if (peopleChanged) dto['People'] = people;
// Emby ignores the plain `Genres`/`Tags` string lists on `POST /Items/{id}`
// and reads the `GenreItems`/`TagItems` name-pair arrays instead. The
// fetched DTO carries the *old* pairs, so mirroring unconditionally (not
// only when the field changed) is what keeps a save from silently
// reinstating the previous genres. See
// [MediaBrowserDialect.metadataWritesUseNamePairLists].
if (client.dialect.metadataWritesUseNamePairLists) {
dto['GenreItems'] = _toNamePairs(dto['Genres']);
dto['TagItems'] = _toNamePairs(dto['Tags']);
}
final success = await client.updateMetadataItem(draft.sourceItem.id, dto); final success = await client.updateMetadataItem(draft.sourceItem.id, dto);
if (success) { if (success) {
draft.extras['raw'] = dto; draft.extras['raw'] = dto;
@@ -178,12 +189,12 @@ class JellyfinMetadataEditAdapter extends MetadataEditAdapter {
? metadataFirstString(raw['Taglines']) ? metadataFirstString(raw['Taglines'])
: item.tagline ?? ''; : item.tagline ?? '';
values['summary'] = raw['Overview'] as String? ?? item.summary ?? ''; values['summary'] = raw['Overview'] as String? ?? item.summary ?? '';
values['genre'] = metadataStringList(raw['Genres']); values['genre'] = _namedStringList(raw, 'Genres', 'GenreItems');
values['director'] = _peopleByType(raw['People'], 'Director'); values['director'] = _peopleByType(raw['People'], 'Director');
values['writer'] = _peopleByType(raw['People'], 'Writer'); values['writer'] = _peopleByType(raw['People'], 'Writer');
values['producer'] = _peopleByType(raw['People'], 'Producer'); values['producer'] = _peopleByType(raw['People'], 'Producer');
values['country'] = metadataStringList(raw['ProductionLocations']); values['country'] = metadataStringList(raw['ProductionLocations']);
values['label'] = metadataStringList(raw['Tags']); values['label'] = _namedStringList(raw, 'Tags', 'TagItems');
} }
void _writeArtworkValues(Map<String, Object?> values, MediaItem item) { void _writeArtworkValues(Map<String, Object?> values, MediaItem item) {
@@ -242,6 +253,18 @@ List<String> _nameList(Object? value) {
.toList(); .toList();
} }
/// Effective value of a `Genres`/`Tags` style field, preferring the plain string
/// array and falling back to its `…Items` name-pair sibling.
///
/// Emby never returns the plain `Tags` array at all — only `TagItems`, whatever
/// `Fields` the request asks for (measured on Emby 4.9.5). Reading the plain key
/// alone would show an empty tag editor for an item that has tags and, worse,
/// write that emptiness back on the next save.
List<String> _namedStringList(Map<String, dynamic> dto, String key, String pairKey) {
final plain = metadataStringList(dto[key]);
return plain.isNotEmpty ? plain : _nameList(dto[pairKey]);
}
List<String> _peopleByType(Object? value, String type) { List<String> _peopleByType(Object? value, String type) {
return _mapList(value) return _mapList(value)
.where((person) => (person['Type'] as String?)?.toLowerCase() == type.toLowerCase()) .where((person) => (person['Type'] as String?)?.toLowerCase() == type.toLowerCase())
@@ -267,6 +290,13 @@ List<Map<String, dynamic>> _replaceNamePairs(List<Map<String, dynamic>> existing
return names.map((name) => _preserveNamedMap(existing, used, name)).toList(); return names.map((name) => _preserveNamedMap(existing, used, name)).toList();
} }
/// Project a `Genres`/`Tags` string list into the `[{'Name': …}]` shape Emby
/// requires on write. The server assigns the `Id` for a new entry, so omitting
/// it is correct — it resolves an existing tag by name and creates one when
/// there is no match.
List<Map<String, dynamic>> _toNamePairs(Object? names) =>
metadataStringList(names).map((name) => <String, dynamic>{'Name': name}).toList();
Map<String, dynamic> _preserveNamedMap( Map<String, dynamic> _preserveNamedMap(
List<Map<String, dynamic>> existing, List<Map<String, dynamic>> existing,
Set<int> used, Set<int> used,
+5 -5
View File
@@ -304,7 +304,7 @@ class ActiveProfileBinder {
// Bind the implicit Plex Home parent and borrowed/extra join rows in // Bind the implicit Plex Home parent and borrowed/extra join rows in
// parallel. A slow/offline Plex parent should not add its timeout budget // parallel. A slow/offline Plex parent should not add its timeout budget
// on top of an otherwise reachable Jellyfin or borrowed-server bind. // on top of an otherwise reachable MediaBrowser or borrowed-server bind.
final results = await Future.wait([ final results = await Future.wait([
if (profile.isPlexHome) if (profile.isPlexHome)
_bindPlexHome( _bindPlexHome(
@@ -315,8 +315,8 @@ class ActiveProfileBinder {
generation: generation, generation: generation,
), ),
// Both kinds also bind borrowed/extra connections via the join table. // Both kinds also bind borrowed/extra connections via the join table.
// For plex_home this handles a Jellyfin server (or extra Plex account) // For plex_home this handles a MediaBrowser server (or extra Plex
// that was attached to the profile via the borrow flow — the parent // account) that was attached to the profile via the borrow flow — the
// account is bound by `_bindPlexHome` above and isn't represented in // account is bound by `_bindPlexHome` above and isn't represented in
// the join table. // the join table.
_bindJoinRows( _bindJoinRows(
@@ -548,7 +548,7 @@ class ActiveProfileBinder {
); );
case JellyfinConnection(): case JellyfinConnection():
expected.add(conn.serverMachineId); expected.add(conn.serverMachineId);
futures.add(_bindJellyfin(conn, profileId: profile.id, generation: generation)); futures.add(_bindMediaBrowser(conn, profileId: profile.id, generation: generation));
} }
} }
final results = await Future.wait(futures); final results = await Future.wait(futures);
@@ -1001,7 +1001,7 @@ class ActiveProfileBinder {
); );
} }
Future<_ProfileBindResult> _bindJellyfin( Future<_ProfileBindResult> _bindMediaBrowser(
JellyfinConnection conn, { JellyfinConnection conn, {
required String profileId, required String profileId,
required int generation, required int generation,
+3
View File
@@ -80,6 +80,9 @@ bool _isEarlier(Connection candidate, Connection incumbent) {
/// borrows a *specific* Home user (`userIdentifier`), and that user's live /// borrows a *specific* Home user (`userIdentifier`), and that user's live
/// [PlexHomeUser.thumb] is the picture Plex shows for it. Reading the live /// [PlexHomeUser.thumb] is the picture Plex shows for it. Reading the live
/// cache also means the avatar tracks Plex's hourly refresh for free. /// cache also means the avatar tracks Plex's hourly refresh for free.
///
/// Jellyfin and Emby use the same `/Users/{uid}/Images/Primary` route, so the
/// shared [JellyfinConnection] arm is dialect-agnostic.
String? connectionAvatarUrl({ String? connectionAvatarUrl({
required Connection connection, required Connection connection,
required ProfileConnection link, required ProfileConnection link,
+3 -2
View File
@@ -11,8 +11,9 @@ part 'profile_connection.freezed.dart';
/// `ActiveProfileBinder` performs the switch on first activation and /// `ActiveProfileBinder` performs the switch on first activation and
/// caches the resulting token back into this row. /// caches the resulting token back into this row.
/// ///
/// For Jellyfin: [userToken] mirrors the Connection's accessToken (one /// For the MediaBrowser backends (Jellyfin and Emby): [userToken] mirrors the
/// user per connection); [userIdentifier] is the Jellyfin user id. /// Connection's accessToken (one user per connection); [userIdentifier] is the
/// server-side user id.
@freezed @freezed
sealed class ProfileConnection with _$ProfileConnection { sealed class ProfileConnection with _$ProfileConnection {
const ProfileConnection._(); const ProfileConnection._();
+6 -4
View File
@@ -89,7 +89,7 @@ class ProfileConnectionCleanup {
/// Sign out of a Plex account: remove the account [Connection], every join /// Sign out of a Plex account: remove the account [Connection], every join
/// row referencing it, and everything owned by its virtual Plex Home /// row referencing it, and everything owned by its virtual Plex Home
/// profiles — including borrowed Jellyfin connections left unreferenced, /// profiles — including borrowed MediaBrowser connections left unreferenced,
/// which previously survived as orphans and wedged the session (#1423). /// which previously survived as orphans and wedged the session (#1423).
/// ///
/// Pass a read-only [plannedRemoval] from /// Pass a read-only [plannedRemoval] from
@@ -131,9 +131,9 @@ class ProfileConnectionCleanup {
/// In-session mirror of the boot guard (`main.dart`: "stored connections /// In-session mirror of the boot guard (`main.dart`: "stored connections
/// exist but no profiles resolved — returning to auth"): prune orphaned /// exist but no profiles resolved — returning to auth"): prune orphaned
/// Jellyfin connections, then decide whether any selectable profile remains. /// MediaBrowser connections, then decide whether any selectable profile
/// [plexHomeUsers] is [PlexHomeService.current]; stale entries for removed /// remains. [plexHomeUsers] is [PlexHomeService.current]; stale entries for
/// accounts are harmless because the connection map is re-read here. /// removed accounts are harmless because the connection map is re-read here.
Future<({PostRemovalRoute route, List<Profile> profiles})> resolvePostRemovalState({ Future<({PostRemovalRoute route, List<Profile> profiles})> resolvePostRemovalState({
required ProfileRegistry profileRegistry, required ProfileRegistry profileRegistry,
required Map<String, List<PlexHomeUser>> plexHomeUsers, required Map<String, List<PlexHomeUser>> plexHomeUsers,
@@ -152,6 +152,8 @@ class ProfileConnectionCleanup {
return (route: PostRemovalRoute.staySignedIn, profiles: merged); return (route: PostRemovalRoute.staySignedIn, profiles: merged);
} }
/// The Jellyfin-era method name is retained for existing callers;
/// [JellyfinConnection] represents both Jellyfin and Emby.
Future<int> pruneUnreferencedJellyfinConnections() async { Future<int> pruneUnreferencedJellyfinConnections() async {
final all = await connections.list(); final all = await connections.list();
final referencedConnectionIds = (await profileConnections.listAll()).map((row) => row.connectionId).toSet(); final referencedConnectionIds = (await profileConnections.listAll()).map((row) => row.connectionId).toSet();
+5 -5
View File
@@ -306,7 +306,7 @@ class CompanionRemoteProvider with ChangeNotifier, DisposableChangeNotifierMixin
), ),
); );
case JellyfinConnection(): case JellyfinConnection():
addContext(await _createJellyfinAuthContext(connection: connection)); addContext(await _createMediaBrowserAuthContext(connection: connection));
} }
} }
@@ -355,7 +355,7 @@ class CompanionRemoteProvider with ChangeNotifier, DisposableChangeNotifierMixin
return RemoteAuthContext( return RemoteAuthContext(
id: auth.computeAuthContextId(homeSecret), id: auth.computeAuthContextId(homeSecret),
backend: 'plex', backend: account.kind.id,
connectionId: account.id, connectionId: account.id,
homeSecret: homeSecret, homeSecret: homeSecret,
discoveryKey: await auth.deriveDiscoveryKey(homeSecret), discoveryKey: await auth.deriveDiscoveryKey(homeSecret),
@@ -365,9 +365,9 @@ class CompanionRemoteProvider with ChangeNotifier, DisposableChangeNotifierMixin
); );
} }
Future<RemoteAuthContext?> _createJellyfinAuthContext({required JellyfinConnection connection}) async { Future<RemoteAuthContext?> _createMediaBrowserAuthContext({required JellyfinConnection connection}) async {
if (connection.accessToken.isEmpty || connection.userId.isEmpty || connection.serverMachineId.isEmpty) { if (connection.accessToken.isEmpty || connection.userId.isEmpty || connection.serverMachineId.isEmpty) {
appLogger.w('CompanionRemote: Skipping Jellyfin remote identity — incomplete connection ${connection.id}'); appLogger.w('CompanionRemote: Skipping MediaBrowser remote identity — incomplete connection ${connection.id}');
return null; return null;
} }
@@ -378,7 +378,7 @@ class CompanionRemoteProvider with ChangeNotifier, DisposableChangeNotifierMixin
); );
return RemoteAuthContext( return RemoteAuthContext(
id: auth.computeAuthContextId(homeSecret), id: auth.computeAuthContextId(homeSecret),
backend: 'jellyfin', backend: connection.kind.id,
connectionId: connection.id, connectionId: connection.id,
homeSecret: homeSecret, homeSecret: homeSecret,
discoveryKey: await auth.deriveDiscoveryKey(homeSecret), discoveryKey: await auth.deriveDiscoveryKey(homeSecret),
+5 -4
View File
@@ -403,7 +403,7 @@ class DownloadProvider extends ChangeNotifier with DisposableChangeNotifierMixin
// Load all downloads from database // Load all downloads from database
final downloads = await _downloadManager.getAllDownloads(); final downloads = await _downloadManager.getAllDownloads();
// Bulk-load all pinned metadata across both backends in a single pass // Bulk-load all pinned metadata across every backend in a single pass
// instead of per-item DB calls. // instead of per-item DB calls.
final allMetadata = await _downloadManager.getAllPinnedMetadata( final allMetadata = await _downloadManager.getAllPinnedMetadata(
preferActiveScope: true, preferActiveScope: true,
@@ -637,10 +637,11 @@ class DownloadProvider extends ChangeNotifier with DisposableChangeNotifierMixin
// Fallback: synthesize from episode metadata (missing year, summary) // Fallback: synthesize from episode metadata (missing year, summary)
// Only Plex consumers read `raw['key']` (library-section + folder // Only Plex consumers read `raw['key']` (library-section + folder
// navigation), so we synthesize the Plex URI for Plex shows and // navigation), so we synthesize the Plex URI for Plex shows and
// emit a Jellyfin-shaped item for Jellyfin (Id + Type=Series). // emit a MediaBrowser-shaped item for Jellyfin or Emby
// (`Id` + `Type=Series`).
final synthesizedRaw = switch (meta.backend) { final synthesizedRaw = switch (meta.backend) {
MediaBackend.plex => <String, dynamic>{'key': '/library/metadata/$showRatingKey'}, MediaBackend.plex => <String, dynamic>{'key': '/library/metadata/$showRatingKey'},
MediaBackend.jellyfin => <String, dynamic>{'Id': showRatingKey, 'Type': 'Series'}, MediaBackend.jellyfin || MediaBackend.emby => <String, dynamic>{'Id': showRatingKey, 'Type': 'Series'},
}; };
shows[showRatingKey] = MediaItem( shows[showRatingKey] = MediaItem(
id: showRatingKey, id: showRatingKey,
@@ -1376,7 +1377,7 @@ class DownloadProvider extends ChangeNotifier with DisposableChangeNotifierMixin
/// Queue every track under an album/artist. Expansion is one /// Queue every track under an album/artist. Expansion is one
/// recursive-leaves call ([MediaServerClient.fetchPlayableDescendants]) on /// recursive-leaves call ([MediaServerClient.fetchPlayableDescendants]) on
/// both backends — Plex branches album→/children, Jellyfin retries /// every backend — Plex branches album→/children, while MediaBrowser retries
/// tag-only artists by album-artist credit. /// tag-only artists by album-artist credit.
Future<int> _queueMusicContainerDownload( Future<int> _queueMusicContainerDownload(
MediaItem container, MediaItem container,
+8 -7
View File
@@ -222,7 +222,7 @@ class MultiServerProvider extends ChangeNotifier with DisposableChangeNotifierMi
/// Whether at least one online server is a Plex server. Used to gate /// Whether at least one online server is a Plex server. Used to gate
/// Plex-only chrome (server-activities popover, conflict-resolution /// Plex-only chrome (server-activities popover, conflict-resolution
/// helpers) so they don't render against a Jellyfin-only profile. /// helpers) so it doesn't render against a MediaBrowser-only profile.
bool get hasOnlinePlexServers => onlineServerIds.any((id) => _serverManager.getPlexClient(ServerId(id)) != null); bool get hasOnlinePlexServers => onlineServerIds.any((id) => _serverManager.getPlexClient(ServerId(id)) != null);
/// Visibility-filtered server ids whose latest health probe was rejected /// Visibility-filtered server ids whose latest health probe was rejected
@@ -263,10 +263,9 @@ class MultiServerProvider extends ChangeNotifier with DisposableChangeNotifierMi
/// Check all online servers for DVR/Live TV availability. Plex servers /// Check all online servers for DVR/Live TV availability. Plex servers
/// expose `/livetv/dvrs` (one entry per configured DVR with its own /// expose `/livetv/dvrs` (one entry per configured DVR with its own
/// lineup); Jellyfin servers expose `/LiveTv/Channels` with a single /// lineup); MediaBrowser servers expose `/LiveTv/Channels` with a single
/// flat channel list per server (synthesized into one [LiveTvServerInfo] /// flat channel list per server (synthesized into one [LiveTvServerInfo]
/// with `dvrKey: 'jellyfin'` so the rest of the UI's per-DVR loop works /// whose backend-derived `dvrKey` keeps the UI's per-DVR identity stable).
/// uniformly).
Future<void> checkLiveTvAvailability() async { Future<void> checkLiveTvAvailability() async {
if (isDisposed) return; if (isDisposed) return;
final generation = ++_liveTvCheckGeneration; final generation = ++_liveTvCheckGeneration;
@@ -285,9 +284,11 @@ class MultiServerProvider extends ChangeNotifier with DisposableChangeNotifierMi
newLiveTvServers.add(LiveTvServerInfo(serverId: serverId, dvrKey: dvr.key, lineup: dvr.lineup, dvrs: dvrs)); newLiveTvServers.add(LiveTvServerInfo(serverId: serverId, dvrKey: dvr.key, lineup: dvr.lineup, dvrs: dvrs));
} }
} else if (await liveTv.isAvailable()) { } else if (await liveTv.isAvailable()) {
// Jellyfin: no per-DVR partitioning; synthesize a single entry so // MediaBrowser: no per-DVR partitioning; synthesize a single entry
// the rest of the UI's per-DVR loop works uniformly. // so the rest of the UI's per-DVR loop works uniformly.
newLiveTvServers.add(LiveTvServerInfo(serverId: serverId, dvrKey: 'jellyfin', lineup: null, dvrs: const [])); newLiveTvServers.add(
LiveTvServerInfo(serverId: serverId, dvrKey: genericClient.backend.id, lineup: null, dvrs: const []),
);
} }
} catch (e) { } catch (e) {
appLogger.d('LiveTV check failed for server $serverId', error: e); appLogger.d('LiveTV check failed for server $serverId', error: e);
+7 -7
View File
@@ -19,8 +19,8 @@ import '../utils/app_logger.dart';
/// Holds the *current user's playback preferences* (audio/subtitle language /// Holds the *current user's playback preferences* (audio/subtitle language
/// defaults) for the active profile. Plex profiles fetch from /// defaults) for the active profile. Plex profiles fetch from
/// `https://clients.plex.tv/api/v2/user`; Jellyfin profiles fetch from /// `https://clients.plex.tv/api/v2/user`; MediaBrowser profiles use their
/// `/Users/Me` on the bound Jellyfin server. /// dialect's current-user route on the bound server.
/// ///
/// Profile *identity* and *switching* are owned by [ActiveProfileProvider] /// Profile *identity* and *switching* are owned by [ActiveProfileProvider]
/// and [ActiveProfileBinder]. This provider is just the settings cache so /// and [ActiveProfileBinder]. This provider is just the settings cache so
@@ -172,12 +172,12 @@ class UserProfileProvider extends ChangeNotifier with DisposableChangeNotifierMi
final settingsConnection = await _resolveActiveSettingsConnection(); final settingsConnection = await _resolveActiveSettingsConnection();
final connection = settingsConnection?.connection; final connection = settingsConnection?.connection;
if (connection is JellyfinConnection) { if (connection is JellyfinConnection) {
final jellyfinClient = _resolveJellyfinClient(connection); final mediaBrowserClient = _resolveMediaBrowserClient(connection);
if (jellyfinClient == null) { if (mediaBrowserClient == null) {
appLogger.d('UserProfileProvider: default Jellyfin client unavailable, skipping settings refresh'); appLogger.d('UserProfileProvider: default MediaBrowser client unavailable, skipping settings refresh');
return; return;
} }
final profile = await jellyfinClient.fetchUserProfile(); final profile = await mediaBrowserClient.fetchUserProfile();
if (profile != null && !stale()) { if (profile != null && !stale()) {
_profileSettings = profile; _profileSettings = profile;
safeNotifyListeners(); safeNotifyListeners();
@@ -202,7 +202,7 @@ class UserProfileProvider extends ChangeNotifier with DisposableChangeNotifierMi
} }
} }
JellyfinClient? _resolveJellyfinClient(JellyfinConnection conn) { JellyfinClient? _resolveMediaBrowserClient(JellyfinConnection conn) {
final manager = _serverManager; final manager = _serverManager;
if (manager == null) return null; if (manager == null) return null;
final client = manager.getClient(ServerId(conn.serverMachineId)); final client = manager.getClient(ServerId(conn.serverMachineId));
+23 -5
View File
@@ -24,6 +24,7 @@ import '../focus/focusable_button.dart';
import '../focus/focusable_text_field.dart'; import '../focus/focusable_text_field.dart';
import '../focus/key_event_utils.dart'; import '../focus/key_event_utils.dart';
import '../media/media_backend.dart'; import '../media/media_backend.dart';
import '../media/media_browser_dialect.dart';
import '../navigation/profile_session_screen.dart'; import '../navigation/profile_session_screen.dart';
import '../utils/navigation_transitions.dart'; import '../utils/navigation_transitions.dart';
import '../widgets/backend_badge.dart'; import '../widgets/backend_badge.dart';
@@ -231,10 +232,13 @@ class _AuthScreenState extends State<AuthScreen> {
_showDebugTokenDialog(); _showDebugTokenDialog();
} }
Future<void> _connectToJellyfin() async { Future<void> _connectToMediaBrowser(MediaBrowserDialect dialect) async {
if (!await _prepareDatabaseRecoveryForSignIn()) return; if (!await _prepareDatabaseRecoveryForSignIn()) return;
if (!mounted) return; if (!mounted) return;
final added = await Navigator.push<bool>(context, MaterialPageRoute(builder: (_) => const AddJellyfinScreen())); final added = await Navigator.push<bool>(
context,
MaterialPageRoute(builder: (_) => AddJellyfinScreen(dialect: dialect)),
);
if (!mounted || added != true) return; if (!mounted || added != true) return;
// The connection persisted and the manager registered the client; move // The connection persisted and the manager registered the client; move
// straight to the main screen. [MainScreen] reads the active client // straight to the main screen. [MainScreen] reads the active client
@@ -350,6 +354,10 @@ class _AuthScreenState extends State<AuthScreen> {
final isAppleTV = PlatformDetector.isAppleTV(); final isAppleTV = PlatformDetector.isAppleTV();
void startBrowserAfterRecovery() => unawaited(_startPlexAfterRecovery(startBrowser)); void startBrowserAfterRecovery() => unawaited(_startPlexAfterRecovery(startBrowser));
void startQrAfterRecovery() => unawaited(_startPlexAfterRecovery(startQr)); void startQrAfterRecovery() => unawaited(_startPlexAfterRecovery(startQr));
const jellyfinDialect = MediaBrowserDialect.jellyfin;
const embyDialect = MediaBrowserDialect.emby;
void connectToJellyfin() => unawaited(_connectToMediaBrowser(jellyfinDialect));
void connectToEmby() => unawaited(_connectToMediaBrowser(embyDialect));
return Column( return Column(
mainAxisSize: .min, mainAxisSize: .min,
crossAxisAlignment: .stretch, crossAxisAlignment: .stretch,
@@ -423,12 +431,22 @@ class _AuthScreenState extends State<AuthScreen> {
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
FocusableButton( FocusableButton(
onPressed: _connectToJellyfin, onPressed: connectToJellyfin,
child: OutlinedButton.icon( child: OutlinedButton.icon(
onPressed: _connectToJellyfin, onPressed: connectToJellyfin,
style: OutlinedButton.styleFrom(padding: const EdgeInsets.symmetric(vertical: 16)), style: OutlinedButton.styleFrom(padding: const EdgeInsets.symmetric(vertical: 16)),
icon: const BackendBadge(backend: MediaBackend.jellyfin, size: 18), icon: const BackendBadge(backend: MediaBackend.jellyfin, size: 18),
label: Text(t.auth.connectToJellyfin), label: Text(t.auth.connectToMediaBrowser(product: jellyfinDialect.productName)),
),
),
const SizedBox(height: 12),
FocusableButton(
onPressed: connectToEmby,
child: OutlinedButton.icon(
onPressed: connectToEmby,
style: OutlinedButton.styleFrom(padding: const EdgeInsets.symmetric(vertical: 16)),
icon: const BackendBadge(backend: MediaBackend.emby, size: 18),
label: Text(t.auth.connectToMediaBrowser(product: embyDialect.productName)),
), ),
), ),
if (kDebugMode) ...[ if (kDebugMode) ...[
+2 -2
View File
@@ -549,14 +549,14 @@ class _CatalogItemDetailScreenState extends State<CatalogItemDetailScreen> {
} }
Widget _buildLibraryMatchTile(MediaItem match, int index) { Widget _buildLibraryMatchTile(MediaItem match, int index) {
// Plex matches carry their library title; Jellyfin's search-based lookup // Plex matches carry their library title; MediaBrowser search-based lookup
// only does when the ancestors call succeeded, so fall back to the server // only does when the ancestors call succeeded, so fall back to the server
// name alone. The subtitle carries whatever else tells two copies apart. // name alone. The subtitle carries whatever else tells two copies apart.
final details = [?_libraryMatchQuality(match), ?(match.libraryTitle == null ? null : match.serverName)]; final details = [?_libraryMatchQuality(match), ?(match.libraryTitle == null ? null : match.serverName)];
return FocusableListTile( return FocusableListTile(
focusNode: _libraryMatchFocusNodes[index], focusNode: _libraryMatchFocusNodes[index],
leading: BackendBadge(backend: match.backend, size: 24), leading: BackendBadge(backend: match.backend, size: 24),
title: Text(match.libraryTitle ?? match.serverName ?? match.backend.name), title: Text(match.libraryTitle ?? match.serverName ?? match.backend.dialect?.productName ?? 'Plex'),
subtitle: details.isEmpty ? null : Text(details.join('')), subtitle: details.isEmpty ? null : Text(details.join('')),
trailing: const AppIcon(Symbols.chevron_right_rounded, fill: 1), trailing: const AppIcon(Symbols.chevron_right_rounded, fill: 1),
onTap: () => unawaited(navigateToMediaItemDetails(context, match)), onTap: () => unawaited(navigateToMediaItemDetails(context, match)),
+2 -3
View File
@@ -5,7 +5,6 @@ import '../media/ids.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import '../media/library_query.dart'; import '../media/library_query.dart';
import '../media/media_backend.dart';
import '../media/media_hub.dart'; import '../media/media_hub.dart';
import '../media/media_item.dart'; import '../media/media_item.dart';
import '../media/media_server_client.dart'; import '../media/media_server_client.dart';
@@ -276,7 +275,7 @@ class _HubDetailScreenState extends State<HubDetailScreen>
} }
bool _shouldUsePaginatedLoader(MediaServerClient client) => bool _shouldUsePaginatedLoader(MediaServerClient client) =>
client.backend == MediaBackend.jellyfin && widget.hub.id.endsWith('.recent'); client.backend.usesMediaBrowserApi && widget.hub.id.endsWith('.recent');
@override @override
Future<LibraryPage<MediaItem>> fetchPage(int start, int size, AbortController? abort) async { Future<LibraryPage<MediaItem>> fetchPage(int start, int size, AbortController? abort) async {
@@ -358,7 +357,7 @@ class _HubDetailScreenState extends State<HubDetailScreen>
_applySort(); _applySort();
if (!usesCustomLoader && !_usesPaginatedLoader && client != null && loadedCount < totalCount) { if (!usesCustomLoader && !_usesPaginatedLoader && client != null && loadedCount < totalCount) {
_replaceContinuationItems = client.backend == MediaBackend.plex; _replaceContinuationItems = !client.backend.usesMediaBrowserApi;
if (_replaceContinuationItems) { if (_replaceContinuationItems) {
_continuation.setContinuation(startIndex: 0, totalCount: 1); _continuation.setContinuation(startIndex: 0, totalCount: 1);
} else { } else {
+9 -9
View File
@@ -52,7 +52,7 @@ class FolderTreeViewState extends State<FolderTreeView> {
/// Folders/items returned by the backend's folder API and mapped to neutral /// Folders/items returned by the backend's folder API and mapped to neutral
/// [MediaItem]s. Plex folder URLs survive in [MediaItem.raw]['key']; /// [MediaItem]s. Plex folder URLs survive in [MediaItem.raw]['key'];
/// Jellyfin folders use the item id as their recursive parent id. /// MediaBrowser folders use the item id as their recursive parent id.
List<MediaItem> _rootFolders = []; List<MediaItem> _rootFolders = [];
final Map<String, List<MediaItem>> _childrenCache = {}; final Map<String, List<MediaItem>> _childrenCache = {};
final Set<String> _expandedFolders = {}; final Set<String> _expandedFolders = {};
@@ -60,9 +60,9 @@ class FolderTreeViewState extends State<FolderTreeView> {
bool _isLoadingRoot = false; bool _isLoadingRoot = false;
String? _errorMessage; String? _errorMessage;
/// Generation counter for in-flight loads. Jellyfin folder fetches render /// Generation counter for in-flight loads. MediaBrowser folder fetches
/// page-by-page via `onPage`; a root reload or deletion refresh bumps the /// render page-by-page via `onPage`; a root reload or deletion refresh
/// epoch so superseded pagination callbacks are dropped. /// bumps the epoch so superseded pagination callbacks are dropped.
int _loadEpoch = 0; int _loadEpoch = 0;
/// Stable expand/cache key for an expandable row: the backend folder key /// Stable expand/cache key for an expandable row: the backend folder key
@@ -250,7 +250,7 @@ class FolderTreeViewState extends State<FolderTreeView> {
/// [widget.serverId], not `forItem`'s fall-back-to-any-online resolution. /// [widget.serverId], not `forItem`'s fall-back-to-any-online resolution.
Future<void> _launchFolder(MediaItem folder, {required bool shuffle}) async { Future<void> _launchFolder(MediaItem folder, {required bool shuffle}) async {
final MediaListPlaybackLauncher launcher; final MediaListPlaybackLauncher launcher;
if (folder.backend == MediaBackend.jellyfin) { if (folder.backend.usesMediaBrowserApi) {
launcher = JellyfinSequentialLauncher(context: context); launcher = JellyfinSequentialLauncher(context: context);
} else { } else {
final client = context.getPlexClientForServer(ServerId(widget.serverId!)); final client = context.getPlexClientForServer(ServerId(widget.serverId!));
@@ -259,22 +259,22 @@ class FolderTreeViewState extends State<FolderTreeView> {
await launcher.launchFromFolder(folder: folder, shuffle: shuffle); await launcher.launchFromFolder(folder: folder, shuffle: shuffle);
} }
/// Expandable rows: directory rows plus Jellyfin media containers whose /// Expandable rows: directory rows plus MediaBrowser media containers whose
/// direct children form the folder tree. Music libraries expose folder- /// direct children form the folder tree. Music libraries expose folder-
/// backed artists and albums as MusicArtist/MusicAlbum rather than generic /// backed artists and albums as MusicArtist/MusicAlbum rather than generic
/// Folder DTOs, so those rows must expand instead of opening empty details. /// Folder DTOs, so those rows must expand instead of opening empty details.
bool _isExpandable(MediaItem item) { bool _isExpandable(MediaItem item) {
return item.kind == MediaKind.folder || (item.backend == MediaBackend.jellyfin && _isJellyfinMediaContainer(item)); return item.kind == MediaKind.folder || (item.backend.usesMediaBrowserApi && _isMediaBrowserMediaContainer(item));
} }
bool _isJellyfinMediaContainer(MediaItem item) { bool _isMediaBrowserMediaContainer(MediaItem item) {
if (item.kind == MediaKind.show || item.kind == MediaKind.season) return true; if (item.kind == MediaKind.show || item.kind == MediaKind.season) return true;
return widget.libraryKind?.isMusic == true && (item.kind == MediaKind.artist || item.kind == MediaKind.album); return widget.libraryKind?.isMusic == true && (item.kind == MediaKind.artist || item.kind == MediaKind.album);
} }
bool _canPlayFolder(MediaItem item) { bool _canPlayFolder(MediaItem item) {
if (item.backend == MediaBackend.plex) return true; if (item.backend == MediaBackend.plex) return true;
if (item.backend == MediaBackend.jellyfin) return widget.libraryKind?.isMusic != true; if (item.backend.usesMediaBrowserApi) return widget.libraryKind?.isMusic != true;
return false; return false;
} }
@@ -10,10 +10,10 @@ import 'alpha_jump_helper.dart';
/// driven — tapping a letter scrolls to that letter's cumulative offset and /// driven — tapping a letter scrolls to that letter's cumulative offset and
/// the highlighted letter follows the visible row. /// the highlighted letter follows the visible row.
/// ///
/// Jellyfin libraries have no per-letter count endpoint. The bar synthesises /// MediaBrowser libraries have no per-letter count endpoint. The bar
/// the 27-letter alphabet (`#`, `A``Z`) and acts as a name-prefix filter /// synthesises the 27-letter alphabet (`#`, `A``Z`) and acts as a name-prefix
/// that refetches the page when the user picks a letter (matches the JF web /// filter that refetches the page when the user picks a letter (matching the
/// client's UX). /// server web clients' UX).
abstract class LibraryAlphaBarStrategy { abstract class LibraryAlphaBarStrategy {
/// Whether the bar should be rendered at all. Implementations consider /// Whether the bar should be rendered at all. Implementations consider
/// total item count, sort key, and current filter state. /// total item count, sort key, and current filter state.
@@ -22,7 +22,7 @@ abstract class LibraryAlphaBarStrategy {
required int loadedCharacterCount, required int loadedCharacterCount,
required String? sortKey, required String? sortKey,
required bool isFolderGrouping, required bool isFolderGrouping,
required String? jellyfinAlphaPrefix, required String? mediaBrowserAlphaPrefix,
required bool isPhone, required bool isPhone,
}); });
@@ -36,22 +36,22 @@ abstract class LibraryAlphaBarStrategy {
}); });
/// Letter to highlight given the current scroll-derived index. Plex maps /// Letter to highlight given the current scroll-derived index. Plex maps
/// the index back through the cumulative offsets; Jellyfin echoes back /// the index back through the cumulative offsets; MediaBrowser backends echo
/// whatever filter is active. /// back whatever filter is active.
String currentLetter(int index, AlphaJumpHelper helper, {String? jellyfinAlphaPrefix}); String currentLetter(int index, AlphaJumpHelper helper, {String? mediaBrowserAlphaPrefix});
/// Handle a tap on the letter at [targetIndex]. Plex strategies invoke /// Handle a tap on the letter at [targetIndex]. Plex strategies invoke
/// [onPlexJump] with the cumulative item index for in-grid scrolling; /// [onPlexJump] with the cumulative item index for in-grid scrolling;
/// Jellyfin strategies invoke [onJellyfinPrefixChange] with the next /// MediaBrowser strategies invoke [onMediaBrowserPrefixChange] with the next
/// `NameStartsWith` prefix (or `null` to clear the filter when the user /// `NameStartsWith` prefix (or `null` to clear the filter when the user
/// re-taps the active letter). Each strategy ignores the callback that /// re-taps the active letter). Each strategy ignores the callback that
/// doesn't apply to its UX, so callers can wire both unconditionally. /// doesn't apply to its UX, so callers can wire both unconditionally.
void onLetterPressed( void onLetterPressed(
int targetIndex, int targetIndex,
AlphaJumpHelper helper, { AlphaJumpHelper helper, {
required String? currentJellyfinPrefix, required String? currentMediaBrowserPrefix,
required void Function(int index) onPlexJump, required void Function(int index) onPlexJump,
required void Function(String? nextPrefix) onJellyfinPrefixChange, required void Function(String? nextPrefix) onMediaBrowserPrefixChange,
}); });
/// Construct the right strategy for [backend]. /// Construct the right strategy for [backend].
@@ -67,7 +67,7 @@ abstract class LibraryAlphaBarStrategy {
libraryKey: libraryKey, libraryKey: libraryKey,
isShared: isShared, isShared: isShared,
), ),
MediaBackend.jellyfin => const JellyfinAlphaBarStrategy(), MediaBackend.jellyfin || MediaBackend.emby => const MediaBrowserAlphaBarStrategy(),
}; };
} }
} }
@@ -88,7 +88,7 @@ class PlexAlphaBarStrategy implements LibraryAlphaBarStrategy {
required int loadedCharacterCount, required int loadedCharacterCount,
required String? sortKey, required String? sortKey,
required bool isFolderGrouping, required bool isFolderGrouping,
required String? jellyfinAlphaPrefix, required String? mediaBrowserAlphaPrefix,
required bool isPhone, required bool isPhone,
}) { }) {
if (isFolderGrouping) return false; if (isFolderGrouping) return false;
@@ -115,7 +115,8 @@ class PlexAlphaBarStrategy implements LibraryAlphaBarStrategy {
} }
@override @override
String currentLetter(int index, AlphaJumpHelper helper, {String? jellyfinAlphaPrefix}) => helper.currentLetter(index); String currentLetter(int index, AlphaJumpHelper helper, {String? mediaBrowserAlphaPrefix}) =>
helper.currentLetter(index);
/// Plex jumps the grid to the cumulative offset for the tapped letter — /// Plex jumps the grid to the cumulative offset for the tapped letter —
/// the helper's letter list already encodes the per-letter ranges from /// the helper's letter list already encodes the per-letter ranges from
@@ -124,17 +125,17 @@ class PlexAlphaBarStrategy implements LibraryAlphaBarStrategy {
void onLetterPressed( void onLetterPressed(
int targetIndex, int targetIndex,
AlphaJumpHelper helper, { AlphaJumpHelper helper, {
required String? currentJellyfinPrefix, required String? currentMediaBrowserPrefix,
required void Function(int index) onPlexJump, required void Function(int index) onPlexJump,
required void Function(String? nextPrefix) onJellyfinPrefixChange, required void Function(String? nextPrefix) onMediaBrowserPrefixChange,
}) { }) {
onPlexJump(targetIndex); onPlexJump(targetIndex);
} }
} }
/// Jellyfin strategy — synthesises the 27-letter alphabet locally and uses /// MediaBrowser strategy — synthesises the 27-letter alphabet locally and
/// the bar as a `NameStartsWith` filter. /// uses the bar as a `NameStartsWith` filter.
class JellyfinAlphaBarStrategy implements LibraryAlphaBarStrategy { class MediaBrowserAlphaBarStrategy implements LibraryAlphaBarStrategy {
static const _letters = [ static const _letters = [
'#', '#',
'A', 'A',
@@ -165,7 +166,7 @@ class JellyfinAlphaBarStrategy implements LibraryAlphaBarStrategy {
'Z', 'Z',
]; ];
const JellyfinAlphaBarStrategy(); const MediaBrowserAlphaBarStrategy();
@override @override
bool shouldShow({ bool shouldShow({
@@ -173,13 +174,13 @@ class JellyfinAlphaBarStrategy implements LibraryAlphaBarStrategy {
required int loadedCharacterCount, required int loadedCharacterCount,
required String? sortKey, required String? sortKey,
required bool isFolderGrouping, required bool isFolderGrouping,
required String? jellyfinAlphaPrefix, required String? mediaBrowserAlphaPrefix,
required bool isPhone, required bool isPhone,
}) { }) {
if (isPhone) return false; if (isPhone) return false;
if (isFolderGrouping) return false; if (isFolderGrouping) return false;
if (loadedCharacterCount == 0) return false; if (loadedCharacterCount == 0) return false;
return totalItemCount >= 80 || jellyfinAlphaPrefix != null; return totalItemCount >= 80 || mediaBrowserAlphaPrefix != null;
} }
@override @override
@@ -193,23 +194,24 @@ class JellyfinAlphaBarStrategy implements LibraryAlphaBarStrategy {
} }
@override @override
String currentLetter(int index, AlphaJumpHelper helper, {String? jellyfinAlphaPrefix}) => jellyfinAlphaPrefix ?? ''; String currentLetter(int index, AlphaJumpHelper helper, {String? mediaBrowserAlphaPrefix}) =>
mediaBrowserAlphaPrefix ?? '';
/// Jellyfin reuses the alpha bar as a `NameStartsWith` filter. We map the /// MediaBrowser backends reuse the alpha bar as a `NameStartsWith` filter.
/// bar offset back to a letter (the synthesised `size: 1` entries make /// We map the bar offset back to a letter (the synthesised `size: 1` entries
/// offset == position in [helper.letters]) and toggle the filter — re-tap /// make offset == position in [helper.letters]) and toggle the filter —
/// the active letter to clear, otherwise set the new prefix. /// re-tap the active letter to clear, otherwise set the new prefix.
@override @override
void onLetterPressed( void onLetterPressed(
int targetIndex, int targetIndex,
AlphaJumpHelper helper, { AlphaJumpHelper helper, {
required String? currentJellyfinPrefix, required String? currentMediaBrowserPrefix,
required void Function(int index) onPlexJump, required void Function(int index) onPlexJump,
required void Function(String? nextPrefix) onJellyfinPrefixChange, required void Function(String? nextPrefix) onMediaBrowserPrefixChange,
}) { }) {
if (targetIndex < 0 || targetIndex >= helper.letters.length) return; if (targetIndex < 0 || targetIndex >= helper.letters.length) return;
final letter = helper.letters[targetIndex]; final letter = helper.letters[targetIndex];
final next = (currentJellyfinPrefix == letter) ? null : letter; final next = (currentMediaBrowserPrefix == letter) ? null : letter;
onJellyfinPrefixChange(next); onMediaBrowserPrefixChange(next);
} }
} }
@@ -6,7 +6,6 @@ import 'package:material_symbols_icons/symbols.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../../../media/library_first_character.dart'; import '../../../media/library_first_character.dart';
import '../../../media/library_query.dart'; import '../../../media/library_query.dart';
import '../../../media/media_backend.dart';
import '../../../media/media_item.dart'; import '../../../media/media_item.dart';
import '../../../media/media_kind.dart'; import '../../../media/media_kind.dart';
import '../../../media/media_library.dart'; import '../../../media/media_library.dart';
@@ -211,15 +210,14 @@ class _LibraryBrowseTabState extends BaseLibraryTabState<MediaItem, LibraryBrows
AlphaJumpHelper _alphaHelper = AlphaJumpHelper(const []); AlphaJumpHelper _alphaHelper = AlphaJumpHelper(const []);
late LibraryAlphaBarStrategy _alphaStrategy = _createAlphaStrategy(); late LibraryAlphaBarStrategy _alphaStrategy = _createAlphaStrategy();
/// On Jellyfin libraries the alpha bar acts as a filter (matches the /// On MediaBrowser libraries the alpha bar acts as a filter. Holds the
/// JF web client's UX). Holds the active letter (`#`, `A``Z`) or null /// active letter (`#`, `A``Z`) or null when no filter is applied.
/// when no filter is applied. String? _mediaBrowserAlphaPrefix;
String? _jellyfinAlphaPrefix;
/// Pre-fetched filter values for Jellyfin libraries — populated by /// Pre-fetched filter values for MediaBrowser libraries — populated by
/// `_loadContent` and consumed by the FiltersBottomSheet so the sheet /// `_loadContent` and consumed by the FiltersBottomSheet so the sheet
/// doesn't need to call back into a Plex client for value listings. /// doesn't need to call back into a Plex client for value listings.
Map<String, List<MediaFilterValue>> _jellyfinFilterValues = const {}; Map<String, List<MediaFilterValue>> _mediaBrowserFilterValues = const {};
final ValueNotifier<int> _currentFirstVisibleIndex = ValueNotifier<int>(0); final ValueNotifier<int> _currentFirstVisibleIndex = ValueNotifier<int>(0);
LibraryAlphaScrollMetrics _scrollMetrics = LibraryAlphaScrollMetrics.empty; LibraryAlphaScrollMetrics _scrollMetrics = LibraryAlphaScrollMetrics.empty;
@@ -257,7 +255,7 @@ class _LibraryBrowseTabState extends BaseLibraryTabState<MediaItem, LibraryBrows
int _firstCharactersRequestId = 0; int _firstCharactersRequestId = 0;
static const int _fetchSize = 200; static const int _fetchSize = 200;
static const int _jellyfinFetchSize = 72; static const int _mediaBrowserFetchSize = 72;
Timer? _scrollIdleTimer; Timer? _scrollIdleTimer;
bool _rangeLoadScheduled = false; bool _rangeLoadScheduled = false;
bool _topScrollResetScheduled = false; bool _topScrollResetScheduled = false;
@@ -306,8 +304,8 @@ class _LibraryBrowseTabState extends BaseLibraryTabState<MediaItem, LibraryBrows
} }
} }
bool get _isJellyfinLibrary => widget.library.backend == MediaBackend.jellyfin; bool get _isMediaBrowserLibrary => widget.library.backend.usesMediaBrowserApi;
int get _activeFetchSize => _isJellyfinLibrary ? _jellyfinFetchSize : _fetchSize; int get _activeFetchSize => _isMediaBrowserLibrary ? _mediaBrowserFetchSize : _fetchSize;
// Focus nodes for filter chips // Focus nodes for filter chips
final FocusNode _groupingChipFocusNode = FocusNode(debugLabel: 'grouping_chip'); final FocusNode _groupingChipFocusNode = FocusNode(debugLabel: 'grouping_chip');
@@ -518,9 +516,9 @@ class _LibraryBrowseTabState extends BaseLibraryTabState<MediaItem, LibraryBrows
_currentFirstVisibleIndex.value = 0; _currentFirstVisibleIndex.value = 0;
// Plex returns categories from `/library/sections/{id}/filters` + // Plex returns categories from `/library/sections/{id}/filters` +
// `/sorts`; Jellyfin maps `/Items/Filters` into the same shape with // `/sorts`; MediaBrowser clients map their filter endpoints into the same
// values pre-cached and a hardcoded client-side sort list. Both flow // shape with values pre-cached and a hardcoded client-side sort list. Both
// through the unified [MediaServerClient.fetchLibraryFiltersWithValues]. // flow through [MediaServerClient.fetchLibraryFiltersWithValues].
try { try {
final client = context.getMediaClientForLibrary(library); final client = context.getMediaClientForLibrary(library);
final loader = LibraryFilterSortLoader(clientFor: (_) => client); final loader = LibraryFilterSortLoader(clientFor: (_) => client);
@@ -536,10 +534,10 @@ class _LibraryBrowseTabState extends BaseLibraryTabState<MediaItem, LibraryBrows
final sortLibraryType = _sortOptionsLibraryType(restoredGrouping); final sortLibraryType = _sortOptionsLibraryType(restoredGrouping);
final LoadedFiltersAndSorts loaded; final LoadedFiltersAndSorts loaded;
if (library.backend == MediaBackend.jellyfin) { if (library.backend.usesMediaBrowserApi) {
// `/Items/Filters` can be much slower than the paged `/Items` browse // MediaBrowser filter discovery can be much slower than the paged
// request on large Jellyfin libraries. Load only the local sort list // `/Items` browse request on large libraries. Load only the local sort
// before page 1, then fill filter values in the background. // list before page 1, then fill filter values in the background.
final sorts = await client.fetchSortOptions(library.id, libraryType: sortLibraryType); final sorts = await client.fetchSortOptions(library.id, libraryType: sortLibraryType);
loaded = LoadedFiltersAndSorts(filters: const [], sorts: sorts); loaded = LoadedFiltersAndSorts(filters: const [], sorts: sorts);
} else { } else {
@@ -554,8 +552,8 @@ class _LibraryBrowseTabState extends BaseLibraryTabState<MediaItem, LibraryBrows
_filters = loaded.filters; _filters = loaded.filters;
_sortOptions = loaded.sorts; _sortOptions = loaded.sorts;
// Plex returns no cached values (filters fetched lazily per-category); // Plex returns no cached values (filters fetched lazily per-category);
// assigning the empty map is a no-op for Plex and a real payload for Jellyfin. // assigning the empty map is a no-op for Plex and a real payload for MediaBrowser libraries.
_jellyfinFilterValues = loaded.cachedValues; _mediaBrowserFilterValues = loaded.cachedValues;
_selectedFilters = Map.from(savedFilters); _selectedFilters = Map.from(savedFilters);
_selectedGrouping = restoredGrouping; _selectedGrouping = restoredGrouping;
@@ -573,8 +571,8 @@ class _LibraryBrowseTabState extends BaseLibraryTabState<MediaItem, LibraryBrows
}); });
_notifyFiltersActive(); _notifyFiltersActive();
if (library.backend == MediaBackend.jellyfin) { if (library.backend.usesMediaBrowserApi) {
_loadJellyfinFiltersInBackground(generation, libraryGlobalKey, library); _loadMediaBrowserFiltersInBackground(generation, libraryGlobalKey, library);
} }
// Load items and first characters in parallel. // Load items and first characters in parallel.
@@ -593,7 +591,7 @@ class _LibraryBrowseTabState extends BaseLibraryTabState<MediaItem, LibraryBrows
} }
} }
void _loadJellyfinFiltersInBackground(int generation, String libraryGlobalKey, MediaLibrary library) { void _loadMediaBrowserFiltersInBackground(int generation, String libraryGlobalKey, MediaLibrary library) {
final client = context.tryGetMediaClientForServer(serverIdOrNull(library.serverId)); final client = context.tryGetMediaClientForServer(serverIdOrNull(library.serverId));
if (client == null) return; if (client == null) return;
unawaited( unawaited(
@@ -603,12 +601,16 @@ class _LibraryBrowseTabState extends BaseLibraryTabState<MediaItem, LibraryBrows
if (!isCurrentLibraryLoad(generation, libraryGlobalKey)) return; if (!isCurrentLibraryLoad(generation, libraryGlobalKey)) return;
setState(() { setState(() {
_filters = result.filters; _filters = result.filters;
_jellyfinFilterValues = result.cachedValues; _mediaBrowserFilterValues = result.cachedValues;
}); });
}) })
.catchError((Object e, StackTrace st) { .catchError((Object e, StackTrace st) {
if (!isCurrentLibraryLoad(generation, libraryGlobalKey)) return; if (!isCurrentLibraryLoad(generation, libraryGlobalKey)) return;
appLogger.w('Jellyfin library filters failed; browse content remains available', error: e, stackTrace: st); appLogger.w(
'MediaBrowser library filters failed; browse content remains available',
error: e,
stackTrace: st,
);
}), }),
); );
} }
@@ -627,7 +629,7 @@ class _LibraryBrowseTabState extends BaseLibraryTabState<MediaItem, LibraryBrows
}); });
} }
/// Initial UI state both Plex and Jellyfin paths need before fetching: /// Initial UI state both Plex and MediaBrowser paths need before fetching:
/// loading flag set, lists cleared, filter/sort caches reset. /// loading flag set, lists cleared, filter/sort caches reset.
void _resetTopOfPageState() { void _resetTopOfPageState() {
setState(() { setState(() {
@@ -637,8 +639,8 @@ class _LibraryBrowseTabState extends BaseLibraryTabState<MediaItem, LibraryBrows
resetPaginationState(); resetPaginationState();
_filters = []; _filters = [];
_sortOptions = []; _sortOptions = [];
_jellyfinFilterValues = const {}; _mediaBrowserFilterValues = const {};
_jellyfinAlphaPrefix = null; _mediaBrowserAlphaPrefix = null;
_selectedFilters = {}; _selectedFilters = {};
_selectedSort = null; _selectedSort = null;
_isSortDescending = false; _isSortDescending = false;
@@ -673,10 +675,10 @@ class _LibraryBrowseTabState extends BaseLibraryTabState<MediaItem, LibraryBrows
filterParams['includeCollections'] = '1'; filterParams['includeCollections'] = '1';
// Jellyfin alpha-bar filter — picked up by DataAggregationService and // MediaBrowser alpha-bar filter — converted to NameStartsWith /
// converted to NameStartsWith / NameLessThan on the wire. // NameLessThan on the wire.
if (_jellyfinAlphaPrefix != null) { if (_mediaBrowserAlphaPrefix != null) {
filterParams['alphaPrefix'] = _jellyfinAlphaPrefix!; filterParams['alphaPrefix'] = _mediaBrowserAlphaPrefix!;
} }
return filterParams; return filterParams;
@@ -1008,10 +1010,9 @@ class _LibraryBrowseTabState extends BaseLibraryTabState<MediaItem, LibraryBrows
libraryKey: widget.library.globalKey, libraryKey: widget.library.globalKey,
loadFilterValues: _loadFilterValues, loadFilterValues: _loadFilterValues,
onBack: onBack, onBack: onBack,
// Pre-populated values arrive only from backends that bundle them // Pre-populated values arrive from MediaBrowser filter discovery. The
// with the category listing (Jellyfin's `/Items/Filters`). The empty // empty map for Plex libraries falls through to lazy `getFilterValues`.
// map for Plex libraries falls through to lazy `getFilterValues`. cachedValues: _mediaBrowserFilterValues.isEmpty ? null : _mediaBrowserFilterValues,
cachedValues: _jellyfinFilterValues.isEmpty ? null : _jellyfinFilterValues,
onFiltersChanged: _applyFilters, onFiltersChanged: _applyFilters,
); );
} }
@@ -1039,9 +1040,9 @@ class _LibraryBrowseTabState extends BaseLibraryTabState<MediaItem, LibraryBrows
final client = context.tryGetPlexClientForServer(serverIdOrNull(widget.library.serverId)); final client = context.tryGetPlexClientForServer(serverIdOrNull(widget.library.serverId));
if (client != null) return client.getFilterValues(filter.key); if (client != null) return client.getFilterValues(filter.key);
// Jellyfin's canonical filter values come from the cached `/Items/Filters` // MediaBrowser canonical filter values come from the cached filter
// payload. If that payload missed a category, there is no neutral endpoint // discovery payload. If that payload missed a category, there is no
// to query yet, so return an empty list instead of routing to a Plex-only API. // neutral endpoint to query, so don't route to a Plex-only API.
return const []; return const [];
} }
@@ -1217,7 +1218,7 @@ class _LibraryBrowseTabState extends BaseLibraryTabState<MediaItem, LibraryBrows
/// how many items we've scrolled past relative to the API's cumulative /// how many items we've scrolled past relative to the API's cumulative
/// firstCharacter counts. /// firstCharacter counts.
String _alphaLetterFor(int index) => String _alphaLetterFor(int index) =>
_alphaStrategy.currentLetter(index, _alphaHelper, jellyfinAlphaPrefix: _jellyfinAlphaPrefix); _alphaStrategy.currentLetter(index, _alphaHelper, mediaBrowserAlphaPrefix: _mediaBrowserAlphaPrefix);
/// Whether the alpha jump bar should be shown. /// Whether the alpha jump bar should be shown.
/// Only shown when sorting by title (titleSort) and not in folders mode. /// Only shown when sorting by title (titleSort) and not in folders mode.
@@ -1233,7 +1234,7 @@ class _LibraryBrowseTabState extends BaseLibraryTabState<MediaItem, LibraryBrows
loadedCharacterCount: _firstCharacters.length, loadedCharacterCount: _firstCharacters.length,
sortKey: _selectedSort?.key, sortKey: _selectedSort?.key,
isFolderGrouping: _selectedGrouping == 'folders', isFolderGrouping: _selectedGrouping == 'folders',
jellyfinAlphaPrefix: _jellyfinAlphaPrefix, mediaBrowserAlphaPrefix: _mediaBrowserAlphaPrefix,
isPhone: _isPhone(context), isPhone: _isPhone(context),
); );
@@ -1351,15 +1352,15 @@ class _LibraryBrowseTabState extends BaseLibraryTabState<MediaItem, LibraryBrows
/// Handle a tap on the letter at [targetIndex] in the alpha bar. The /// Handle a tap on the letter at [targetIndex] in the alpha bar. The
/// active [LibraryAlphaBarStrategy] owns the per-backend behaviour and /// active [LibraryAlphaBarStrategy] owns the per-backend behaviour and
/// invokes one of the two callbacks — Plex scrolls the grid to the /// invokes one of the two callbacks — Plex scrolls the grid to the
/// cumulative item offset, Jellyfin toggles a `NameStartsWith` filter /// cumulative item offset, while MediaBrowser backends toggle a
/// (matches the JF web client UX). /// `NameStartsWith` filter.
void _jumpToIndex(int targetIndex) { void _jumpToIndex(int targetIndex) {
_alphaStrategy.onLetterPressed( _alphaStrategy.onLetterPressed(
targetIndex, targetIndex,
_alphaHelper, _alphaHelper,
currentJellyfinPrefix: _jellyfinAlphaPrefix, currentMediaBrowserPrefix: _mediaBrowserAlphaPrefix,
onPlexJump: _scrollGridToIndex, onPlexJump: _scrollGridToIndex,
onJellyfinPrefixChange: _applyJellyfinAlphaPrefix, onMediaBrowserPrefixChange: _applyMediaBrowserAlphaPrefix,
); );
} }
@@ -1376,12 +1377,12 @@ class _LibraryBrowseTabState extends BaseLibraryTabState<MediaItem, LibraryBrows
_scrollToItemIndex(clamped); _scrollToItemIndex(clamped);
} }
/// Apply the new Jellyfin `NameStartsWith` prefix from the alpha bar and /// Apply a MediaBrowser `NameStartsWith` prefix from the alpha bar and
/// refetch from the top of the now-filtered dataset. Used by /// refetch from the top of the now-filtered dataset. Used by
/// [JellyfinAlphaBarStrategy] via [_jumpToIndex]. /// [MediaBrowserAlphaBarStrategy] via [_jumpToIndex].
void _applyJellyfinAlphaPrefix(String? nextPrefix) { void _applyMediaBrowserAlphaPrefix(String? nextPrefix) {
setState(() { setState(() {
_jellyfinAlphaPrefix = nextPrefix; _mediaBrowserAlphaPrefix = nextPrefix;
// Clear loaded items + total so the grid blanks while the new filtered // Clear loaded items + total so the grid blanks while the new filtered
// page loads. PaginatedItemLoader internals will repopulate from // page loads. PaginatedItemLoader internals will repopulate from
// offset 0 once the next fetchPage call returns. // offset 0 once the next fetchPage call returns.
@@ -1595,8 +1596,8 @@ class _LibraryBrowseTabState extends BaseLibraryTabState<MediaItem, LibraryBrows
if (rowHeight <= 0) return _activeFetchSize; if (rowHeight <= 0) return _activeFetchSize;
final visibleRows = (screenSize.height / rowHeight).ceil() + 1; final visibleRows = (screenSize.height / rowHeight).ceil() + 1;
final visibleCount = visibleRows * columnCount; final visibleCount = visibleRows * columnCount;
if (_isJellyfinLibrary) { if (_isMediaBrowserLibrary) {
return (visibleCount * 2).clamp(36, _jellyfinFetchSize).toInt(); return (visibleCount * 2).clamp(36, _mediaBrowserFetchSize).toInt();
} }
return (visibleCount * 3).clamp(100, 500).toInt(); return (visibleCount * 3).clamp(100, 500).toInt();
} catch (_) { } catch (_) {
@@ -5,6 +5,7 @@ import 'package:material_symbols_icons/symbols.dart';
import '../../focus/focusable_wrapper.dart'; import '../../focus/focusable_wrapper.dart';
import '../../i18n/strings.g.dart'; import '../../i18n/strings.g.dart';
import '../../media/media_backend.dart'; import '../../media/media_backend.dart';
import '../../media/media_browser_dialect.dart';
import '../../theme/mono_tokens.dart'; import '../../theme/mono_tokens.dart';
import '../../profiles/profile.dart'; import '../../profiles/profile.dart';
import '../../widgets/backend_badge.dart'; import '../../widgets/backend_badge.dart';
@@ -18,8 +19,8 @@ import 'add_plex_account_screen.dart';
/// When [targetProfile] is provided, also offers a "Borrow from another /// When [targetProfile] is provided, also offers a "Borrow from another
/// profile" option that opens [BorrowConnectionScreen] for the target. The /// profile" option that opens [BorrowConnectionScreen] for the target. The
/// global Connections screen invokes this without a target — Plex auto- /// global Connections screen invokes this without a target — Plex auto-
/// surfaces its Home users as new profiles, Jellyfin binds to the active /// surfaces its Home users as new profiles, while MediaBrowser servers bind
/// profile via [AddJellyfinScreen]. /// to the active profile via [AddJellyfinScreen].
/// ///
/// Pops with `true` after the underlying flow succeeds so the parent list /// Pops with `true` after the underlying flow succeeds so the parent list
/// refreshes; pops with `null` (the default) when the user backs out. /// refreshes; pops with `null` (the default) when the user backs out.
@@ -31,6 +32,8 @@ class AddConnectionScreen extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final scoped = targetProfile != null; final scoped = targetProfile != null;
const jellyfinDialect = MediaBrowserDialect.jellyfin;
const embyDialect = MediaBrowserDialect.emby;
final options = <_BackendOption>[ final options = <_BackendOption>[
_BackendOption( _BackendOption(
backend: MediaBackend.plex, backend: MediaBackend.plex,
@@ -40,11 +43,25 @@ class AddConnectionScreen extends StatelessWidget {
), ),
_BackendOption( _BackendOption(
backend: MediaBackend.jellyfin, backend: MediaBackend.jellyfin,
title: t.addServer.connectToJellyfinCard, title: t.addServer.connectToMediaBrowserCard(product: jellyfinDialect.productName),
subtitle: scoped subtitle: scoped
? t.addServer.connectToJellyfinCardSubtitleScoped(name: targetProfile!.displayName) ? t.addServer.connectToMediaBrowserCardSubtitleScoped(
: t.addServer.connectToJellyfinCardSubtitle, product: jellyfinDialect.productName,
builder: (_) => AddJellyfinScreen(targetProfile: targetProfile), name: targetProfile!.displayName,
)
: t.addServer.connectToMediaBrowserCardSubtitle,
builder: (_) => AddJellyfinScreen(targetProfile: targetProfile, dialect: jellyfinDialect),
),
_BackendOption(
backend: MediaBackend.emby,
title: t.addServer.connectToMediaBrowserCard(product: embyDialect.productName),
subtitle: scoped
? t.addServer.connectToMediaBrowserCardSubtitleScoped(
product: embyDialect.productName,
name: targetProfile!.displayName,
)
: t.addServer.connectToMediaBrowserCardSubtitle,
builder: (_) => AddJellyfinScreen(targetProfile: targetProfile, dialect: embyDialect),
), ),
if (scoped) if (scoped)
_BackendOption( _BackendOption(
+37 -19
View File
@@ -14,6 +14,7 @@ import '../../focus/focusable_button.dart';
import '../../focus/focusable_text_field.dart'; import '../../focus/focusable_text_field.dart';
import '../../focus/focusable_wrapper.dart'; import '../../focus/focusable_wrapper.dart';
import '../../i18n/strings.g.dart'; import '../../i18n/strings.g.dart';
import '../../media/media_browser_dialect.dart';
import '../../mixins/controller_disposer_mixin.dart'; import '../../mixins/controller_disposer_mixin.dart';
import '../../profiles/active_profile_binder.dart'; import '../../profiles/active_profile_binder.dart';
import '../../profiles/active_profile_provider.dart'; import '../../profiles/active_profile_provider.dart';
@@ -69,26 +70,28 @@ bool shouldPromptForJellyfinProfileSelection({
return targetProfile == null && activeProfile == null && hasProfiles; return targetProfile == null && activeProfile == null && hasProfiles;
} }
/// Three-step form to add a Jellyfin server: /// Three-step form to add a Jellyfin or Emby server:
/// 1. Probe URL candidates (`/System/Info/Public`). /// 1. Probe URL candidates (`/System/Info/Public`).
/// 2. Username + password (`/Users/AuthenticateByName`) **or** Quick Connect /// 2. Username + password (`/Users/AuthenticateByName`) or Quick Connect
/// (`/QuickConnect/Initiate` → poll → `/Users/AuthenticateWithQuickConnect`). /// when supported by the selected [dialect].
/// 3. Persist via [ConnectionRegistry] and create a [ProfileConnection] /// 3. Persist via [ConnectionRegistry] and create a [ProfileConnection]
/// row binding the server to [targetProfile] (or the active profile, /// row binding the server to [targetProfile] (or the active profile,
/// if not provided). When the target *is* the active profile we also /// if not provided). When the target *is* the active profile we also
/// register the client with the manager so libraries refresh /// register the client with the manager so libraries refresh
/// immediately; otherwise the binder picks it up on the next switch. /// immediately; otherwise the binder picks it up on the next switch.
class AddJellyfinScreen extends StatefulWidget { class AddJellyfinScreen extends StatefulWidget {
/// When set, the new Jellyfin connection is bound to this profile via a /// When set, the new MediaBrowser connection is bound to this profile via a
/// [ProfileConnection] row. When null, falls back to the currently active /// [ProfileConnection] row. When null, falls back to the currently active
/// profile (typical for the global Connections screen entry point). /// profile (typical for the global Connections screen entry point).
final Profile? targetProfile; final Profile? targetProfile;
final MediaBrowserDialect dialect;
final FutureOr<JellyfinConnectionAuthService> Function()? _authServiceFactory; final FutureOr<JellyfinConnectionAuthService> Function()? _authServiceFactory;
final FutureOr<List<DiscoveredJellyfinServer>> Function()? _localDiscoveryFactory; final FutureOr<List<DiscoveredJellyfinServer>> Function()? _localDiscoveryFactory;
const AddJellyfinScreen({ const AddJellyfinScreen({
super.key, super.key,
this.targetProfile, this.targetProfile,
this.dialect = MediaBrowserDialect.jellyfin,
@visibleForTesting this._authServiceFactory, @visibleForTesting this._authServiceFactory,
@visibleForTesting this._localDiscoveryFactory, @visibleForTesting this._localDiscoveryFactory,
}); });
@@ -157,7 +160,10 @@ class _AddJellyfinScreenState extends State<AddJellyfinScreen> with AsyncFormSta
final factory = widget._localDiscoveryFactory; final factory = widget._localDiscoveryFactory;
final servers = factory != null final servers = factory != null
? await factory() ? await factory()
: await JellyfinLanDiscoveryService().discover(responseWindow: const Duration(milliseconds: 1300)); : await JellyfinLanDiscoveryService().discover(
dialect: widget.dialect,
responseWindow: const Duration(milliseconds: 1300),
);
if (!mounted || attemptId != _localDiscoveryAttemptId) return; if (!mounted || attemptId != _localDiscoveryAttemptId) return;
setState(() { setState(() {
_localServers = servers; _localServers = servers;
@@ -165,7 +171,7 @@ class _AddJellyfinScreenState extends State<AddJellyfinScreen> with AsyncFormSta
_syncDiscoveredServerFocusNodes(servers); _syncDiscoveredServerFocusNodes(servers);
}); });
} catch (e, st) { } catch (e, st) {
appLogger.w('Add Jellyfin local discovery failed', error: e, stackTrace: st); appLogger.w('Add ${widget.dialect.productName} local discovery failed', error: e, stackTrace: st);
if (!mounted || attemptId != _localDiscoveryAttemptId) return; if (!mounted || attemptId != _localDiscoveryAttemptId) return;
setState(() => _isDiscoveringLocalServers = false); setState(() => _isDiscoveringLocalServers = false);
} }
@@ -201,9 +207,9 @@ class _AddJellyfinScreenState extends State<AddJellyfinScreen> with AsyncFormSta
} }
Future<void> _probe() async { Future<void> _probe() async {
final input = JellyfinEndpointDiscovery.buildUserInputCandidates(_enteredUrls()); final input = JellyfinEndpointDiscovery.buildUserInputCandidates(_enteredUrls(), dialect: widget.dialect);
if (input.probeBaseUrls.isEmpty) { if (input.probeBaseUrls.isEmpty) {
setErrorText(t.addServer.enterJellyfinUrlError); setErrorText(t.addServer.enterMediaBrowserUrlError(product: widget.dialect.productName));
return; return;
} }
final autoStartQuickConnect = await runAsync<bool>( final autoStartQuickConnect = await runAsync<bool>(
@@ -214,7 +220,10 @@ class _AddJellyfinScreenState extends State<AddJellyfinScreen> with AsyncFormSta
baseUrlsToPersist: input.explicitBaseUrls, baseUrlsToPersist: input.explicitBaseUrls,
baseUrlValidationGroups: input.validationBaseUrlGroups, baseUrlValidationGroups: input.validationBaseUrlGroups,
); );
final qcEnabled = await auth.isQuickConnectEnabled(endpoint.activeBaseUrl); final serverDialect = endpoint.serverInfo.dialect ?? widget.dialect;
final qcEnabled = widget.dialect.supportsQuickConnect && serverDialect.supportsQuickConnect
? await auth.isQuickConnectEnabled(endpoint.activeBaseUrl)
: false;
if (!mounted) return false; if (!mounted) return false;
setState(() { setState(() {
_serverEndpoint = endpoint; _serverEndpoint = endpoint;
@@ -268,7 +277,7 @@ class _AddJellyfinScreenState extends State<AddJellyfinScreen> with AsyncFormSta
}, },
errorMapper: (e) { errorMapper: (e) {
if (e is MediaServerAuthException) return e.message; if (e is MediaServerAuthException) return e.message;
appLogger.e('Add Jellyfin failed', error: e); appLogger.e('Add ${widget.dialect.productName} failed', error: e);
return t.addServer.signInFailed(error: e.toString()); return t.addServer.signInFailed(error: e.toString());
}, },
); );
@@ -278,6 +287,7 @@ class _AddJellyfinScreenState extends State<AddJellyfinScreen> with AsyncFormSta
final info = _serverInfo; final info = _serverInfo;
final endpoint = _serverEndpoint; final endpoint = _serverEndpoint;
if (info == null || endpoint == null) return; if (info == null || endpoint == null) return;
if (!widget.dialect.supportsQuickConnect || !(info.dialect ?? widget.dialect).supportsQuickConnect) return;
final attemptId = ++_qcAttemptId; final attemptId = ++_qcAttemptId;
setState(() => _qcCancelled = false); setState(() => _qcCancelled = false);
await runAsync<void>( await runAsync<void>(
@@ -440,7 +450,12 @@ class _AddJellyfinScreenState extends State<AddJellyfinScreen> with AsyncFormSta
if (authServiceFactory != null) return await authServiceFactory(); if (authServiceFactory != null) return await authServiceFactory();
final clientVersion = await resolveJellyfinClientVersion(); final clientVersion = await resolveJellyfinClientVersion();
final deviceName = await _resolveDeviceName(); final deviceName = await _resolveDeviceName();
return JellyfinConnectionAuthService(clientName: 'Plezy', clientVersion: clientVersion, deviceName: deviceName); return JellyfinConnectionAuthService(
clientName: 'Plezy',
clientVersion: clientVersion,
deviceName: deviceName,
dialect: widget.dialect,
);
} }
/// The raw name, not a header-sanitized one: the Jellyfin `MediaBrowser` /// The raw name, not a header-sanitized one: the Jellyfin `MediaBrowser`
@@ -455,9 +470,9 @@ class _AddJellyfinScreenState extends State<AddJellyfinScreen> with AsyncFormSta
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); final theme = Theme.of(context);
return FocusedScrollScaffold( return FocusedScrollScaffold(
title: Text(t.addServer.addJellyfinTitle), title: Text(t.addServer.addMediaBrowserTitle(product: widget.dialect.productName)),
slivers: [ slivers: [
if (_qcInitiation != null) if (widget.dialect.supportsQuickConnect && _qcInitiation != null)
SliverFillRemaining( SliverFillRemaining(
hasScrollBody: false, hasScrollBody: false,
child: Padding( child: Padding(
@@ -509,7 +524,7 @@ class _AddJellyfinScreenState extends State<AddJellyfinScreen> with AsyncFormSta
decoration: InputDecoration( decoration: InputDecoration(
labelText: t.addServer.serverUrls, labelText: t.addServer.serverUrls,
// URL example — intentionally not localized. // URL example — intentionally not localized.
hintText: 'https://jellyfin.example.com', hintText: widget.dialect.exampleBaseUrl,
helperText: _serverInfo == null ? t.addServer.serverUrlsHelper : null, helperText: _serverInfo == null ? t.addServer.serverUrlsHelper : null,
prefixIcon: const AppIcon(Symbols.link_rounded, fill: 1), prefixIcon: const AppIcon(Symbols.link_rounded, fill: 1),
), ),
@@ -560,7 +575,7 @@ class _AddJellyfinScreenState extends State<AddJellyfinScreen> with AsyncFormSta
labelText: t.addServer.password, labelText: t.addServer.password,
prefixIcon: const AppIcon(Symbols.lock_rounded, fill: 1), prefixIcon: const AppIcon(Symbols.lock_rounded, fill: 1),
), ),
// Empty password is valid for some Jellyfin setups, so don't // Empty passwords are valid on some MediaBrowser servers, so don't
// require a value. // require a value.
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
@@ -574,7 +589,7 @@ class _AddJellyfinScreenState extends State<AddJellyfinScreen> with AsyncFormSta
label: Text(t.addServer.signIn), label: Text(t.addServer.signIn),
), ),
), ),
if (_quickConnectEnabled) ...[ if (widget.dialect.supportsQuickConnect && _quickConnectEnabled) ...[
const SizedBox(height: 12), const SizedBox(height: 12),
FocusableButton( FocusableButton(
focusNode: _quickConnectFocus, focusNode: _quickConnectFocus,
@@ -609,7 +624,7 @@ class _AddJellyfinScreenState extends State<AddJellyfinScreen> with AsyncFormSta
children: [ children: [
Text(_serverInfo!.serverName, style: theme.textTheme.titleSmall), Text(_serverInfo!.serverName, style: theme.textTheme.titleSmall),
Text( Text(
'Jellyfin ${_serverInfo!.version}', '${widget.dialect.productName} ${_serverInfo!.version}',
style: theme.textTheme.bodySmall?.copyWith(color: theme.colorScheme.onSurface.withValues(alpha: 0.7)), style: theme.textTheme.bodySmall?.copyWith(color: theme.colorScheme.onSurface.withValues(alpha: 0.7)),
), ),
], ],
@@ -649,7 +664,7 @@ class _AddJellyfinScreenState extends State<AddJellyfinScreen> with AsyncFormSta
const SizedBox(width: 10), const SizedBox(width: 10),
Expanded( Expanded(
child: Text( child: Text(
t.addServer.searchingLocalServers, t.addServer.searchingLocalMediaBrowserServers(product: widget.dialect.productName),
style: theme.textTheme.bodyMedium?.copyWith(color: theme.colorScheme.onSurface.withValues(alpha: 0.7)), style: theme.textTheme.bodyMedium?.copyWith(color: theme.colorScheme.onSurface.withValues(alpha: 0.7)),
), ),
), ),
@@ -662,7 +677,10 @@ class _AddJellyfinScreenState extends State<AddJellyfinScreen> with AsyncFormSta
final tokensRef = tokens(context); final tokensRef = tokens(context);
return [ return [
const SizedBox(height: 16), const SizedBox(height: 16),
Text(t.addServer.localServers, style: theme.textTheme.titleSmall), Text(
t.addServer.localMediaBrowserServers(product: widget.dialect.productName),
style: theme.textTheme.titleSmall,
),
const SizedBox(height: 8), const SizedBox(height: 8),
for (final (i, server) in _localServers.indexed) ...[ for (final (i, server) in _localServers.indexed) ...[
if (i > 0) SizedBox(height: tokensRef.groupGap), if (i > 0) SizedBox(height: tokensRef.groupGap),
@@ -43,8 +43,11 @@ class _EditJellyfinConnectionScreenState extends State<EditJellyfinConnectionScr
if (!(_formKey.currentState?.validate() ?? false)) return; if (!(_formKey.currentState?.validate() ?? false)) return;
await runAsync<void>( await runAsync<void>(
() async { () async {
final input = JellyfinEndpointDiscovery.buildUserInputCandidates(_enteredUrls()); final input = JellyfinEndpointDiscovery.buildUserInputCandidates(
final endpoint = await JellyfinEndpointDiscovery().raceEndpoints( _enteredUrls(),
dialect: widget.connection.dialect,
);
final endpoint = await JellyfinEndpointDiscovery(dialect: widget.connection.dialect).raceEndpoints(
input.probeBaseUrls, input.probeBaseUrls,
preferredUrl: widget.connection.baseUrl, preferredUrl: widget.connection.baseUrl,
expectedMachineId: widget.connection.serverMachineId, expectedMachineId: widget.connection.serverMachineId,
@@ -63,7 +66,7 @@ class _EditJellyfinConnectionScreenState extends State<EditJellyfinConnectionScr
}, },
errorMapper: (e) { errorMapper: (e) {
if (e is MediaServerUrlException) return e.message; if (e is MediaServerUrlException) return e.message;
appLogger.e('Edit Jellyfin connection failed', error: e); appLogger.e('Edit ${widget.connection.dialect.productName} connection failed', error: e);
return t.addServer.couldNotReachServer(error: e.toString()); return t.addServer.couldNotReachServer(error: e.toString());
}, },
); );
@@ -75,7 +78,7 @@ class _EditJellyfinConnectionScreenState extends State<EditJellyfinConnectionScr
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); final theme = Theme.of(context);
return FocusedScrollScaffold( return FocusedScrollScaffold(
title: Text(t.connections.editJellyfinTitle), title: Text(t.connections.editMediaBrowserTitle(product: widget.connection.dialect.productName)),
slivers: [ slivers: [
SliverPadding( SliverPadding(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
@@ -86,7 +89,7 @@ class _EditJellyfinConnectionScreenState extends State<EditJellyfinConnectionScr
crossAxisAlignment: .stretch, crossAxisAlignment: .stretch,
children: [ children: [
Text( Text(
t.connections.editJellyfinIntro(serverName: widget.connection.serverName), t.connections.editMediaBrowserIntro(serverName: widget.connection.serverName),
style: theme.textTheme.bodyMedium, style: theme.textTheme.bodyMedium,
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
+20 -14
View File
@@ -12,18 +12,21 @@ import '../utils/isolate_helper.dart';
/// ///
/// Stores raw JSON keyed by `serverId:endpoint` in the shared `ApiCache` /// Stores raw JSON keyed by `serverId:endpoint` in the shared `ApiCache`
/// Drift table. `serverId` values are globally unique across connected /// Drift table. `serverId` values are globally unique across connected
/// backends, so Plex and Jellyfin entries never collide despite sharing /// backends, so Plex and MediaBrowser entries never collide despite sharing
/// the same table. /// the same table.
/// ///
/// Plex- and Jellyfin-specific helpers (item-id pinning, metadata parsing) /// Plex- and MediaBrowser-specific helpers (item-id pinning, metadata parsing)
/// live on subclasses [PlexApiCache] / [JellyfinApiCache], which also /// live on subclasses [PlexApiCache] / [JellyfinApiCache], which also
/// implement the abstract [getMetadata] / [pinForOffline] / [deleteForItem] /// implement the abstract [getMetadata] / [pinForOffline] / [deleteForItem]
/// methods so callers can dispatch via [forBackend] instead of switching on /// methods so callers can dispatch via [forBackend] instead of switching on
/// the backend type at every call site. /// the backend type at every call site.
class ApiCacheSingleton<T extends ApiCache> { class ApiCacheSingleton<T extends ApiCache> {
ApiCacheSingleton(this.backend, this.typeName); ApiCacheSingleton(this.backends, this.typeName);
final MediaBackend backend; /// Every backend this cache answers for. Jellyfin and Emby share one
/// instance: their DTO shapes are identical and cache rows are keyed by the
/// compound `machineId/userId` scope, so there is nothing to isolate.
final Set<MediaBackend> backends;
final String typeName; final String typeName;
T? _instance; T? _instance;
@@ -37,7 +40,7 @@ class ApiCacheSingleton<T extends ApiCache> {
void install(T instance) { void install(T instance) {
_instance = instance; _instance = instance;
ApiCache.registerInstance(backend, instance); ApiCache.registerInstance(instance, backends);
} }
} }
@@ -65,14 +68,17 @@ Map<String, MediaItem> decodeCachedMediaRows<T>(
abstract class ApiCache { abstract class ApiCache {
static final Map<MediaBackend, ApiCache> _byBackend = {}; static final Map<MediaBackend, ApiCache> _byBackend = {};
/// Registers a backend cache. A new database marks a new application/test /// Registers [instance] for each requested backend. A new database marks a
/// lifecycle, so registrations tied to the previous database are discarded /// new application/test lifecycle, so registrations tied to the previous
/// instead of leaving backend dispatch pointed at a closed connection. /// database are discarded instead of leaving backend dispatch pointed at a
static void registerInstance(MediaBackend backend, ApiCache cache) { /// closed connection.
if (_byBackend.values.any((registered) => !identical(registered.database, cache.database))) { static void registerInstance(ApiCache instance, Set<MediaBackend> backends) {
if (_byBackend.values.any((registered) => !identical(registered.database, instance.database))) {
_byBackend.clear(); _byBackend.clear();
} }
_byBackend[backend] = cache; for (final backend in backends) {
_byBackend[backend] = instance;
}
} }
/// Pick the cache for [backend]. Plex is the legacy default — covers items /// Pick the cache for [backend]. Plex is the legacy default — covers items
@@ -169,7 +175,7 @@ abstract class ApiCache {
/// Pull pinned rows for [serverId] and extract the first capture group of /// Pull pinned rows for [serverId] and extract the first capture group of
/// [keyPattern] from each `cacheKey`. Returns the unique set of captured /// [keyPattern] from each `cacheKey`. Returns the unique set of captured
/// ids — backend subclasses use this to enumerate their pinned items /// ids — backend subclasses use this to enumerate their pinned items
/// (Plex ratingKeys, Jellyfin item ids). /// (Plex ratingKeys, MediaBrowser item ids).
Future<Set<String>> extractPinnedIds(ServerId serverId, RegExp keyPattern) async { Future<Set<String>> extractPinnedIds(ServerId serverId, RegExp keyPattern) async {
final rows = await (_db.select( final rows = await (_db.select(
_db.apiCache, _db.apiCache,
@@ -215,7 +221,7 @@ abstract class ApiCache {
/// [itemId] so reloads (`getMetadata` / `getAllPinnedMetadata`) reflect the /// [itemId] so reloads (`getMetadata` / `getAllPinnedMetadata`) reflect the
/// state without having to refetch from the server. No-op when the row /// state without having to refetch from the server. No-op when the row
/// isn't cached. Backend subclasses know which JSON fields to mutate /// isn't cached. Backend subclasses know which JSON fields to mutate
/// (Plex `viewCount`, Jellyfin `UserData.PlayCount` / `Played`). /// (Plex `viewCount`, MediaBrowser `UserData.PlayCount` / `Played`).
/// ///
/// Optional positional progress fields ([viewOffsetMs], [lastViewedAt], /// Optional positional progress fields ([viewOffsetMs], [lastViewedAt],
/// [viewedLeafCount]) let the offline-watch-sync service mirror richer /// [viewedLeafCount]) let the offline-watch-sync service mirror richer
@@ -228,7 +234,7 @@ abstract class ApiCache {
/// shape + units are not. Adding a new watch-state input here means /// shape + units are not. Adding a new watch-state input here means
/// updating *both* concrete impls ([PlexApiCache.applyWatchState], /// updating *both* concrete impls ([PlexApiCache.applyWatchState],
/// [JellyfinApiCache.applyWatchState]) — Plex stores epoch-seconds and /// [JellyfinApiCache.applyWatchState]) — Plex stores epoch-seconds and
/// flat fields, Jellyfin stores ISO-8601 + ticks under `UserData`. /// flat fields, while Jellyfin and Emby store ISO-8601 + ticks under `UserData`.
/// The mutations are too short (~3 lines per backend) for a shared /// The mutations are too short (~3 lines per backend) for a shared
/// adapter to be a net win, so they live duplicated by design. /// adapter to be a net win, so they live duplicated by design.
Future<void> applyWatchState({ Future<void> applyWatchState({
@@ -23,7 +23,12 @@ class CachedPlaybackMetadataService {
try { try {
return switch (backend) { return switch (backend) {
MediaBackend.plex => _fetchPlexMediaSourceInfo(ServerId(cacheServerId), itemId, mediaIndex: mediaIndex), MediaBackend.plex => _fetchPlexMediaSourceInfo(ServerId(cacheServerId), itemId, mediaIndex: mediaIndex),
MediaBackend.jellyfin => _fetchJellyfinMediaSourceInfo(cacheServerId, itemId, mediaIndex: mediaIndex), MediaBackend.jellyfin || MediaBackend.emby => _fetchJellyfinMediaSourceInfo(
cacheServerId,
itemId,
backend: backend,
mediaIndex: mediaIndex,
),
}; };
} catch (e) { } catch (e) {
appLogger.d('Cached media source info unavailable for $cacheServerId:$itemId', error: e); appLogger.d('Cached media source info unavailable for $cacheServerId:$itemId', error: e);
@@ -48,9 +53,10 @@ class CachedPlaybackMetadataService {
creditsPattern: creditsPattern, creditsPattern: creditsPattern,
forceChapterFallback: forceChapterFallback, forceChapterFallback: forceChapterFallback,
), ),
MediaBackend.jellyfin => _fetchJellyfinPlaybackExtras( MediaBackend.jellyfin || MediaBackend.emby => _fetchJellyfinPlaybackExtras(
cacheServerId, cacheServerId,
itemId, itemId,
backend: backend,
introPattern: introPattern, introPattern: introPattern,
creditsPattern: creditsPattern, creditsPattern: creditsPattern,
forceChapterFallback: forceChapterFallback, forceChapterFallback: forceChapterFallback,
@@ -96,9 +102,10 @@ class CachedPlaybackMetadataService {
static Future<MediaSourceInfo?> _fetchJellyfinMediaSourceInfo( static Future<MediaSourceInfo?> _fetchJellyfinMediaSourceInfo(
String cacheServerId, String cacheServerId,
String itemId, { String itemId, {
required MediaBackend backend,
required int mediaIndex, required int mediaIndex,
}) async { }) async {
final resolved = await _jellyfinRawItem(cacheServerId, itemId); final resolved = await _jellyfinRawItem(cacheServerId, itemId, backend: backend);
final raw = resolved.raw; final raw = resolved.raw;
final sources = raw['MediaSources']; final sources = raw['MediaSources'];
if (sources is! List || sources.isEmpty) return null; if (sources is! List || sources.isEmpty) return null;
@@ -110,13 +117,14 @@ class CachedPlaybackMetadataService {
static Future<PlaybackExtras?> _fetchJellyfinPlaybackExtras( static Future<PlaybackExtras?> _fetchJellyfinPlaybackExtras(
String cacheServerId, String cacheServerId,
String itemId, { String itemId, {
required MediaBackend backend,
String? introPattern, String? introPattern,
String? creditsPattern, String? creditsPattern,
bool forceChapterFallback = false, bool forceChapterFallback = false,
}) async { }) async {
final resolved = await _jellyfinRawItem(cacheServerId, itemId); final resolved = await _jellyfinRawItem(cacheServerId, itemId, backend: backend);
final raw = resolved.raw; final raw = resolved.raw;
final markers = await _jellyfinMediaSegmentMarkers(resolved.scopeId, itemId); final markers = await _jellyfinMediaSegmentMarkers(resolved.scopeId, itemId, backend: backend);
return jellyfinPlaybackExtrasFromRaw( return jellyfinPlaybackExtrasFromRaw(
raw, raw,
itemId, itemId,
@@ -127,10 +135,14 @@ class CachedPlaybackMetadataService {
); );
} }
static Future<List<MediaMarker>> _jellyfinMediaSegmentMarkers(String cacheServerId, String itemId) async { static Future<List<MediaMarker>> _jellyfinMediaSegmentMarkers(
String cacheServerId,
String itemId, {
required MediaBackend backend,
}) async {
try { try {
final raw = await ApiCache.forBackend( final raw = await ApiCache.forBackend(
MediaBackend.jellyfin, backend,
).get(ServerId(cacheServerId), JellyfinApiCache.mediaSegmentsEndpoint(itemId)); ).get(ServerId(cacheServerId), JellyfinApiCache.mediaSegmentsEndpoint(itemId));
return jellyfinMediaSegmentsToMarkers(raw); return jellyfinMediaSegmentsToMarkers(raw);
} catch (e) { } catch (e) {
@@ -141,11 +153,12 @@ class CachedPlaybackMetadataService {
static Future<({Map<String, dynamic> raw, String scopeId})> _jellyfinRawItem( static Future<({Map<String, dynamic> raw, String scopeId})> _jellyfinRawItem(
String cacheServerId, String cacheServerId,
String itemId, String itemId, {
) async { required MediaBackend backend,
final cache = ApiCache.forBackend(MediaBackend.jellyfin); }) async {
final cache = ApiCache.forBackend(backend);
final resolved = await JellyfinCacheResolver(cache.database).findItem(cacheServerId, itemId); final resolved = await JellyfinCacheResolver(cache.database).findItem(cacheServerId, itemId);
if (resolved == null) throw StateError('No Jellyfin cache row for $cacheServerId:$itemId'); if (resolved == null) throw StateError('No MediaBrowser cache row for $cacheServerId:$itemId');
return (raw: jsonDecode(resolved.cacheRow.data) as Map<String, dynamic>, scopeId: resolved.key.scopeId); return (raw: jsonDecode(resolved.cacheRow.data) as Map<String, dynamic>, scopeId: resolved.key.scopeId);
} }
} }
@@ -12,10 +12,11 @@ import '../../utils/app_logger.dart';
/// ///
/// Proves same-account membership via a backend-derived shared secret. /// Proves same-account membership via a backend-derived shared secret.
/// ///
/// Plex uses the Plex Home metadata available to signed-in devices. Jellyfin /// Plex uses the Plex Home metadata available to signed-in devices. The
/// uses the stable server/user identity available after sign-in, matching the /// MediaBrowser backends (Jellyfin and Emby) use the stable server/user
/// same local-LAN trust model: peers that know the same backend identity can /// identity available after sign-in, matching the same local-LAN trust model:
/// discover and authenticate each other without a central pairing round-trip. /// peers that know the same backend identity can discover and authenticate
/// each other without a central pairing round-trip.
class RemoteAuthService { class RemoteAuthService {
RemoteAuthService._(); RemoteAuthService._();
static final instance = RemoteAuthService._(); static final instance = RemoteAuthService._();
@@ -76,7 +77,14 @@ class RemoteAuthService {
return deriveHomeSecret(home.id, admin.uuid); return deriveHomeSecret(home.id, admin.uuid);
} }
/// Derive a companion remote secret from a Jellyfin server/user identity. /// Derive a companion remote secret from a MediaBrowser server/user identity
/// — Jellyfin or Emby.
///
/// Both peers of a pair are signed into the same server and user, so both
/// derive the same secret regardless of dialect. The `jellyfin-secret` HKDF
/// `info` label and the `jellyfin:` cache prefix are therefore fixed domain
/// separators, not dialect discriminators: changing either would invalidate
/// every existing pairing for zero benefit.
Future<List<int>> deriveJellyfinSecret({required String serverMachineId, required String userId}) async { Future<List<int>> deriveJellyfinSecret({required String serverMachineId, required String userId}) async {
final normalizedServerId = serverMachineId.toLowerCase(); final normalizedServerId = serverMachineId.toLowerCase();
final normalizedUserId = userId.toLowerCase(); final normalizedUserId = userId.toLowerCase();
@@ -98,7 +106,7 @@ class RemoteAuthService {
_cachedSecret = await secretKey.extractBytes(); _cachedSecret = await secretKey.extractBytes();
_cachedSecretKey = cacheKey; _cachedSecretKey = cacheKey;
appLogger.d('RemoteAuth: Derived Jellyfin secret'); appLogger.d('RemoteAuth: Derived MediaBrowser secret');
return _cachedSecret!; return _cachedSecret!;
} }
+11 -10
View File
@@ -63,11 +63,7 @@ class CredentialVault {
static Future<Map<String, Object?>> protectConnectionConfig(String kind, Map<String, Object?> config) async { static Future<Map<String, Object?>> protectConnectionConfig(String kind, Map<String, Object?> config) async {
final copy = Map<String, Object?>.from(config); final copy = Map<String, Object?>.from(config);
final tokenKey = switch (kind) { final tokenKey = _tokenKeyForKind(kind);
'plex' => 'accountToken',
'jellyfin' => 'accessToken',
_ => null,
};
final token = tokenKey == null ? null : copy[tokenKey]; final token = tokenKey == null ? null : copy[tokenKey];
if (token is String) copy[tokenKey!] = await protect(token); if (token is String) copy[tokenKey!] = await protect(token);
if (kind == 'plex') { if (kind == 'plex') {
@@ -81,11 +77,7 @@ class CredentialVault {
Map<String, dynamic> config, Map<String, dynamic> config,
) async { ) async {
final copy = Map<String, dynamic>.from(config); final copy = Map<String, dynamic>.from(config);
final tokenKey = switch (kind) { final tokenKey = _tokenKeyForKind(kind);
'plex' => 'accountToken',
'jellyfin' => 'accessToken',
_ => null,
};
var migrated = false; var migrated = false;
final token = tokenKey == null ? null : copy[tokenKey]; final token = tokenKey == null ? null : copy[tokenKey];
if (token is String && token.isNotEmpty) { if (token is String && token.isNotEmpty) {
@@ -103,6 +95,15 @@ class CredentialVault {
return (config: copy, migrated: migrated); return (config: copy, migrated: migrated);
} }
/// Config key holding the long-lived credential for a `connections.kind`
/// value. Returning `null` means "nothing to encrypt", so every new kind MUST
/// be listed here — an omission silently persists the token in plaintext.
static String? _tokenKeyForKind(String kind) => switch (kind) {
'plex' => 'accountToken',
'jellyfin' || 'emby' => 'accessToken',
_ => null,
};
static Future<Object?> _protectPlexServers(Object? rawServers) async { static Future<Object?> _protectPlexServers(Object? rawServers) async {
if (rawServers is! List) return rawServers; if (rawServers is! List) return rawServers;
final servers = <Object?>[]; final servers = <Object?>[];
+19 -15
View File
@@ -459,19 +459,20 @@ class DownloadManagerService {
/// Returns the cache namespace visible to [activeProfileId] for [serverId]. /// Returns the cache namespace visible to [activeProfileId] for [serverId].
/// ///
/// Jellyfin prefers the persisted profile-to-user binding so a cold launch /// MediaBrowser backends prefer the persisted profile-to-user binding so a
/// and a profile switch cannot inherit the physical download row's creator /// cold launch and a profile switch cannot inherit the physical download
/// scope. A live scope is used only when no persisted binding exists. /// row's creator scope. A live scope is used only when no persisted binding
/// exists.
Future<String?> profileClientScopeIdForServer(ServerId serverId, String? activeProfileId) async { Future<String?> profileClientScopeIdForServer(ServerId serverId, String? activeProfileId) async {
if (activeProfileId == null || activeProfileId.isEmpty) return null; if (activeProfileId == null || activeProfileId.isEmpty) return null;
final backend = await _backendForServer(serverId); final backend = await _backendForServer(serverId);
if (backend == MediaBackend.plex) { if (backend == null) return null;
return buildPlexProfileScopeId(serverId: serverId, profileId: activeProfileId); if (backend.usesMediaBrowserApi) {
}
if (backend != MediaBackend.jellyfin) return null;
final persisted = await JellyfinCacheResolver(_database).findProfileScopeId(serverId, activeProfileId); final persisted = await JellyfinCacheResolver(_database).findProfileScopeId(serverId, activeProfileId);
return persisted ?? activeClientScopeIdForServer(serverId); return persisted ?? activeClientScopeIdForServer(serverId);
} }
return buildPlexProfileScopeId(serverId: serverId, profileId: activeProfileId);
}
/// Bulk-load pinned metadata. Profile-visible hydration reads only exact /// Bulk-load pinned metadata. Profile-visible hydration reads only exact
/// owner namespaces; it never pre-merges another user's rows. /// owner namespaces; it never pre-merges another user's rows.
@@ -568,16 +569,19 @@ class DownloadManagerService {
/// is currently offline (the connection persists across launches). /// is currently offline (the connection persists across launches).
/// ///
/// [JellyfinCacheResolver] reconciles bare machine ids with compound /// [JellyfinCacheResolver] reconciles bare machine ids with compound
/// `${serverMachineId}/$userId` connection ids without treating `_` or `%` /// `${serverMachineId}/$userId` MediaBrowser connection ids without treating
/// as wildcards. /// `_` or `%` as wildcards.
Future<MediaBackend?> _backendForServer(ServerId serverId) async { Future<MediaBackend?> _backendForServer(ServerId serverId) async {
// Prefer a live client — `MediaServerClient.backend` is in memory. // Prefer a live client — `MediaServerClient.backend` is in memory.
final live = _getClient(serverId); final live = _getClient(serverId);
if (live != null) return live.backend; if (live != null) return live.backend;
final row = await JellyfinCacheResolver(_database).findConnection(serverId); final row = await JellyfinCacheResolver(_database).findConnection(serverId);
if (row == null) return null; if (row == null) return null;
// Keep the persisted discriminator intact even though both MediaBrowser
// values dispatch to the same cache implementation.
return switch (row.kind) { return switch (row.kind) {
'jellyfin' => MediaBackend.jellyfin, 'jellyfin' => MediaBackend.jellyfin,
'emby' => MediaBackend.emby,
'plex' => MediaBackend.plex, 'plex' => MediaBackend.plex,
_ => null, _ => null,
}; };
@@ -590,8 +594,8 @@ class DownloadManagerService {
/// When [_backendForServer] can't resolve the backend (no live client and /// When [_backendForServer] can't resolve the backend (no live client and
/// no `connections` row — happens when a server has been removed but old /// no `connections` row — happens when a server has been removed but old
/// download rows still reference it), fan out to every registered backend /// download rows still reference it), fan out to every registered backend
/// cache instead of silently defaulting to Plex. Otherwise Jellyfin items /// cache instead of silently defaulting to Plex. Otherwise MediaBrowser
/// would render with blank metadata after a connection is severed. /// items would render with blank metadata after a connection is severed.
Future<MediaItem?> _lookupMetadata(ServerId serverId, String itemId, {String? clientScopeId}) async { Future<MediaItem?> _lookupMetadata(ServerId serverId, String itemId, {String? clientScopeId}) async {
final backend = await _backendForServer(serverId); final backend = await _backendForServer(serverId);
final live = _getClient(serverId, clientScopeId: clientScopeId); final live = _getClient(serverId, clientScopeId: clientScopeId);
@@ -712,8 +716,8 @@ class DownloadManagerService {
} }
} }
/// Backend-aware "ensure cached & pin". Jellyfin loads playback extras so /// Backend-aware "ensure cached & pin". MediaBrowser backends load playback
/// both item metadata and native media segments are available offline; other /// extras; Jellyfin can additionally cache native media segments. Other
/// backends only need the item metadata row. Then pin cached rows so they /// backends only need the item metadata row. Then pin cached rows so they
/// survive general cache eviction. /// survive general cache eviction.
Future<void> _pinMetadataForOffline(MediaServerClient client, MediaItem metadata) async { Future<void> _pinMetadataForOffline(MediaServerClient client, MediaItem metadata) async {
@@ -722,7 +726,7 @@ class DownloadManagerService {
appLogger.w('Cannot pin metadata without serverId'); appLogger.w('Cannot pin metadata without serverId');
return; return;
} }
if (client.backend == MediaBackend.jellyfin) { if (client.backend.usesMediaBrowserApi) {
try { try {
await client.fetchPlaybackExtras(metadata.id); await client.fetchPlaybackExtras(metadata.id);
} catch (e) { } catch (e) {
@@ -3564,7 +3568,7 @@ class DownloadManagerService {
/// Save metadata for a media item (show, season, movie, or episode) /// Save metadata for a media item (show, season, movie, or episode)
/// Used to persist parent metadata (shows/seasons) for offline display. /// Used to persist parent metadata (shows/seasons) for offline display.
/// ///
/// Both backends now have read-path cache-through, so the work is just to /// All backends have read-path cache-through, so the work is just to
/// hit `client.fetchItem` (idempotent) and pin the resulting row. /// hit `client.fetchItem` (idempotent) and pin the resulting row.
Future<void> saveMetadata(MediaItem metadata, MediaServerClient client) async { Future<void> saveMetadata(MediaItem metadata, MediaServerClient client) async {
if (metadata.serverId == null) { if (metadata.serverId == null) {
+37 -31
View File
@@ -5,6 +5,7 @@ import 'package:drift/drift.dart';
import '../database/app_database.dart'; import '../database/app_database.dart';
import '../media/media_backend.dart'; import '../media/media_backend.dart';
import '../media/media_browser_dialect.dart';
import '../media/media_item.dart'; import '../media/media_item.dart';
import '../utils/app_logger.dart'; import '../utils/app_logger.dart';
import '../utils/global_key_utils.dart'; import '../utils/global_key_utils.dart';
@@ -14,22 +15,26 @@ import 'credential_vault.dart';
import 'jellyfin_cache_resolver.dart'; import 'jellyfin_cache_resolver.dart';
import 'jellyfin_mappers.dart'; import 'jellyfin_mappers.dart';
/// Jellyfin-shape helpers on top of the shared [ApiCache] substrate. /// MediaBrowser-shape helpers on top of the shared [ApiCache] substrate.
/// ///
/// Cache rows for Jellyfin item metadata use the compound connection id /// Cache rows for Jellyfin and Emby item metadata use the compound connection
/// (`{machineId}/{userId}`) plus the read-path endpoint key /// id (`{machineId}/{userId}`) plus the read-path endpoint key
/// `/Users/{userId}/Items/{itemId}`. The public [MediaItem.serverId] remains /// `/Users/{userId}/Items/{itemId}`. The public [MediaItem.serverId] remains
/// the bare machine id; the compound prefix only isolates local user-scoped /// the bare machine id; the compound prefix only isolates local user-scoped
/// state such as `UserData`. /// state such as `UserData`.
class JellyfinApiCache extends ApiCache { class JellyfinApiCache extends ApiCache {
static final _singleton = ApiCacheSingleton<JellyfinApiCache>(MediaBackend.jellyfin, 'JellyfinApiCache'); static final _singleton = ApiCacheSingleton<JellyfinApiCache>(const {
MediaBackend.jellyfin,
MediaBackend.emby,
}, 'JellyfinApiCache');
static JellyfinApiCache get instance => _singleton.instance; static JellyfinApiCache get instance => _singleton.instance;
JellyfinApiCache._(super.db); JellyfinApiCache._(super.db);
/// Initialize the singleton with an [AppDatabase] instance. Also registers /// Initialize the singleton with an [AppDatabase] instance. Also registers
/// this instance with the [ApiCache] backend dispatch so callers using /// this instance with the [ApiCache] backend dispatch so callers using
/// `ApiCache.forBackend(MediaBackend.jellyfin)` resolve here. /// `ApiCache.forBackend(MediaBackend.jellyfin)` or
/// `ApiCache.forBackend(MediaBackend.emby)` resolve here.
static void initialize(AppDatabase db) => _singleton.install(JellyfinApiCache._(db)); static void initialize(AppDatabase db) => _singleton.install(JellyfinApiCache._(db));
JellyfinCacheResolver get _resolver => JellyfinCacheResolver(database); JellyfinCacheResolver get _resolver => JellyfinCacheResolver(database);
@@ -111,6 +116,7 @@ class JellyfinApiCache extends ApiCache {
serverId: ServerId(ctx.machineId), serverId: ServerId(ctx.machineId),
serverName: ctx.name, serverName: ctx.name,
absolutizer: absolutizer, absolutizer: absolutizer,
dialect: ctx.dialect,
); );
} catch (_) { } catch (_) {
return null; return null;
@@ -118,18 +124,17 @@ class JellyfinApiCache extends ApiCache {
} }
/// Persist a watched/unwatched flip into cached `BaseItemDto` rows for /// Persist a watched/unwatched flip into cached `BaseItemDto` rows for
/// [itemId]. Compound Jellyfin scope ids update only their user. A legacy /// [itemId]. Compound MediaBrowser scope ids update only their user. A legacy
/// bare machine id is accepted only when its matching rows belong to one /// bare machine id is accepted only when its matching rows belong to one
/// user; ambiguous multi-user writes are skipped rather than bleeding watch /// user; ambiguous multi-user writes are skipped rather than bleeding watch
/// state across profiles. /// state across profiles.
/// ///
/// [viewOffsetMs] is converted to Jellyfin's 100-ns ticks for /// [viewOffsetMs] is converted to MediaBrowser 100-ns ticks for
/// `UserData.PlaybackPositionTicks`. [lastViewedAt] is treated as Plex's /// `UserData.PlaybackPositionTicks`. [lastViewedAt] is treated as Plex's
/// epoch-seconds and translated to Jellyfin's ISO-8601 `LastPlayedDate`. /// epoch-seconds and translated to the MediaBrowser ISO-8601
/// [viewedLeafCount] is ignored — Jellyfin tracks per-show rollup via /// `LastPlayedDate`. [viewedLeafCount] is ignored — both dialects compute
/// `UserData.UnplayedItemCount`, computed from individual children rather /// per-show rollup from individual children via `UserData.UnplayedItemCount`.
/// than aggregated on the parent. The parameter is accepted for API parity /// The parameter is accepted for API parity with the Plex caller.
/// with the Plex caller.
@override @override
Future<void> applyWatchState({ Future<void> applyWatchState({
required ServerId serverId, required ServerId serverId,
@@ -150,7 +155,7 @@ class JellyfinApiCache extends ApiCache {
}; };
if (userIds.length > 1) { if (userIds.length > 1) {
appLogger.w( appLogger.w(
'Skipping ambiguous bare-scope Jellyfin watch-state cache write', 'Skipping ambiguous bare-scope MediaBrowser watch-state cache write',
error: {'serverId': serverId, 'itemId': itemId, 'userCount': userIds.length}, error: {'serverId': serverId, 'itemId': itemId, 'userCount': userIds.length},
); );
return; return;
@@ -192,7 +197,7 @@ class JellyfinApiCache extends ApiCache {
} }
} }
/// Load all pinned Jellyfin metadata in a single query. /// Load all pinned MediaBrowser metadata in a single query.
/// ///
/// Returns a map keyed by `buildGlobalKey(ServerId(serverId), itemId)` for O(1) /// Returns a map keyed by `buildGlobalKey(ServerId(serverId), itemId)` for O(1)
/// lookups, mirroring [PlexApiCache.getAllPinnedMetadata] so callers can /// lookups, mirroring [PlexApiCache.getAllPinnedMetadata] so callers can
@@ -213,9 +218,10 @@ class JellyfinApiCache extends ApiCache {
// Resolve the connection context per serverId once on the main thread // Resolve the connection context per serverId once on the main thread
// (DB queries can't move into the isolate). Each context carries the // (DB queries can't move into the isolate). Each context carries the
// serverName used to stamp the [MediaItem] plus the baseUrl/accessToken // serverName and dialect used to stamp the [MediaItem] plus the
// required to absolutize image paths. // baseUrl/accessToken required to absolutize image paths.
final contexts = <String, ({String machineId, String name, String baseUrl, String accessToken})>{}; final contexts =
<String, ({String machineId, String name, String baseUrl, String accessToken, MediaBrowserDialect dialect})>{};
final absolutizers = <String, JellyfinImageAbsolutizer>{}; final absolutizers = <String, JellyfinImageAbsolutizer>{};
for (final entry in entries) { for (final entry in entries) {
final id = entry.connection.id; final id = entry.connection.id;
@@ -240,6 +246,7 @@ class JellyfinApiCache extends ApiCache {
serverId: ServerId(ctx.machineId), serverId: ServerId(ctx.machineId),
serverName: ctx.name, serverName: ctx.name,
absolutizer: absolutizer, absolutizer: absolutizer,
dialect: ctx.dialect,
); );
if (mapped == null) return null; if (mapped == null) return null;
return MapEntry(buildGlobalKey(ServerId(entry.key.scopeId), entry.key.itemId), mapped); return MapEntry(buildGlobalKey(ServerId(entry.key.scopeId), entry.key.itemId), mapped);
@@ -248,27 +255,26 @@ class JellyfinApiCache extends ApiCache {
); );
} }
/// Resolve the connection context (server name + base URL + access token) /// Resolve the connection context (server name, dialect, base URL, and access
/// for a cache row keyed by the server's machineId. The [Connections] /// token) for a cache row keyed by the server's machineId. The [Connections]
/// row's `id` is `${serverMachineId}/$userId`, so a direct `id == serverId` /// row's `id` is `${serverMachineId}/$userId`, so a direct `id == serverId`
/// lookup misses; fall back to a prefix match. /// lookup misses; fall back to a prefix match.
/// ///
/// `name` matches what the live [JellyfinClient] stamps onto online /// `name` and `dialect` match what the live [JellyfinClient] stamps onto
/// MediaItems (`connection.serverName`, not the compound `displayName`). /// online MediaItems. `baseUrl` and `accessToken` come from the same
/// `baseUrl` and `accessToken` come from the same `configJson` payload /// `configJson` payload [JellyfinConnection.toConfigJson] writes, so
/// [JellyfinConnection.toConfigJson] writes, so cache-read absolutization /// cache-read absolutization uses the current values — token/URL rotations
/// uses the current values — token/URL rotations Just Work. /// Just Work.
/// ///
/// Returns `null` when no row matches or the row carries an empty /// Returns `null` when no row matches or the row carries an empty `baseUrl`
/// `baseUrl` (no honest URL we can build). /// (no honest URL we can build).
Future<({String machineId, String name, String baseUrl, String accessToken})?> _serverContext( Future<({String machineId, String name, String baseUrl, String accessToken, MediaBrowserDialect dialect})?>
ConnectionRow row, { _serverContext(ConnectionRow row, {required String machineId}) async {
required String machineId,
}) async {
String? configName; String? configName;
String? configMachineId; String? configMachineId;
String baseUrl = ''; String baseUrl = '';
String accessToken = ''; String accessToken = '';
final dialect = MediaBrowserDialect.fromIdOrJellyfin(row.kind);
try { try {
final rawConfig = jsonDecode(row.configJson) as Map<String, dynamic>; final rawConfig = jsonDecode(row.configJson) as Map<String, dynamic>;
final config = (await CredentialVault.revealConnectionConfig(row.kind, rawConfig)).config; final config = (await CredentialVault.revealConnectionConfig(row.kind, rawConfig)).config;
@@ -282,6 +288,6 @@ class JellyfinApiCache extends ApiCache {
if (baseUrl.isEmpty) return null; if (baseUrl.isEmpty) return null;
configMachineId ??= machineId; configMachineId ??= machineId;
final name = (configName != null && configName.isNotEmpty) ? configName : row.displayName; final name = (configName != null && configName.isNotEmpty) ? configName : row.displayName;
return (machineId: configMachineId, name: name, baseUrl: baseUrl, accessToken: accessToken); return (machineId: configMachineId, name: name, baseUrl: baseUrl, accessToken: accessToken, dialect: dialect);
} }
} }
+14 -15
View File
@@ -1,22 +1,21 @@
import '../utils/device_identity.dart'; import '../utils/device_identity.dart';
/// Build the `MediaBrowser` Authorization header value the way the official /// Builds the `MediaBrowser` Authorization header value understood by both
/// Jellyfin SDK formats it: every field value is percent-encoded, and the /// Jellyfin and Emby. Every field value is percent-encoded, and the server
/// server reverses that with `WebUtility.UrlDecode` while parsing the header. /// reverses that encoding while parsing the header. The same value is used at
/// Used at auth time and on every authenticated request so the server sees a /// auth time and on every authenticated request so either dialect sees a
/// consistent client identity. /// consistent client identity.
/// ///
/// Encoding is what keeps the header sendable at all. A device name like /// Encoding is what keeps the header sendable at all. A device name like
/// `Bjørn PC` cannot travel verbatim: `dart:io` rejects header values above /// `Bjørn PC` cannot travel verbatim: `dart:io` rejects header values above
/// 0x7F outright, and CFNetwork puts the raw code unit on the wire as a /// 0x7F outright, and CFNetwork puts the raw code unit on the wire as a
/// Latin-1 byte, which Kestrel — the HTTP server hosting Jellyfin — refuses /// Latin-1 byte, which the server rejects as a malformed header before the
/// as a malformed header with 400 before the request is ever routed. It also /// request is routed. It also removes the grammar hazards the header has no
/// removes the grammar hazards the header has no escape for: quotes, commas, /// escape for: quotes, commas, and `=` inside a value.
/// and `=` inside a value.
/// ///
/// Jellyfin requires non-empty client, device, and version fields when /// Both dialects require non-empty client, device, and version fields when
/// creating a session, so those values use stable fallbacks. An empty device /// creating a session, so those values use stable fallbacks. An empty device
/// ID is omitted for authenticated requests, where Jellyfin can recover it /// ID is omitted for authenticated requests, where the server can recover it
/// from the token; unauthenticated entry points must call /// from the token; unauthenticated entry points must call
/// [requireJellyfinDeviceId]. /// [requireJellyfinDeviceId].
String buildJellyfinAuthHeader({ String buildJellyfinAuthHeader({
@@ -49,14 +48,14 @@ final RegExp _controlCharacters = RegExp(r'[\x00-\x1f\x7f-\x9f]');
/// Percent-encoding makes any byte transportable, so the only values worth /// Percent-encoding makes any byte transportable, so the only values worth
/// filtering are the ones that carry no identity at all — a name of control /// filtering are the ones that carry no identity at all — a name of control
/// characters would otherwise reach Jellyfin's device list as `%00` noise /// characters would otherwise reach the server's device list as `%00` noise
/// instead of falling back to a readable label. /// instead of falling back to a readable label.
String _meaningful(String value) => value.replaceAll(_controlCharacters, '').trim(); String _meaningful(String value) => value.replaceAll(_controlCharacters, '').trim();
/// Validates the stable device identity required by unauthenticated Jellyfin /// Validates the stable device identity required by unauthenticated
/// session creation. Never substitute a placeholder: Jellyfin keys sessions /// MediaBrowser session creation. Never substitute a placeholder: both
/// and access tokens by this value, so a shared fallback would collide across /// dialects key sessions and access tokens by this value, so a shared fallback
/// installations. /// would collide across installations.
String requireJellyfinDeviceId(String deviceId) { String requireJellyfinDeviceId(String deviceId) {
final sanitized = sanitizeHeaderValue(deviceId); final sanitized = sanitizeHeaderValue(deviceId);
if (sanitized == null || sanitized != deviceId || sanitized.contains('"')) { if (sanitized == null || sanitized != deviceId || sanitized.contains('"')) {
+31 -11
View File
@@ -6,6 +6,7 @@ import 'package:http/http.dart' as http;
import '../connection/connection.dart'; import '../connection/connection.dart';
import '../exceptions/media_server_exceptions.dart'; import '../exceptions/media_server_exceptions.dart';
import '../media/media_browser_dialect.dart';
import '../utils/app_logger.dart'; import '../utils/app_logger.dart';
import '../utils/media_server_http_client.dart'; import '../utils/media_server_http_client.dart';
import '../utils/media_server_timeouts.dart'; import '../utils/media_server_timeouts.dart';
@@ -13,6 +14,7 @@ import '../utils/log_redaction_manager.dart';
import '../utils/poll_with_backoff.dart'; import '../utils/poll_with_backoff.dart';
import 'jellyfin_auth_header.dart'; import 'jellyfin_auth_header.dart';
import 'jellyfin_endpoint_discovery.dart'; import 'jellyfin_endpoint_discovery.dart';
import 'media_browser_paths.dart';
/// Result of `POST /QuickConnect/Initiate`. The [code] is shown to the user /// Result of `POST /QuickConnect/Initiate`. The [code] is shown to the user
/// and entered in their Jellyfin web UI to approve sign-in; the [secret] is /// and entered in their Jellyfin web UI to approve sign-in; the [secret] is
@@ -42,7 +44,7 @@ class _JellyfinAuthenticationResponse {
/// Auth flow for adding or refreshing a [JellyfinConnection]. /// Auth flow for adding or refreshing a [JellyfinConnection].
/// ///
/// Lifecycle for adding a server: /// Lifecycle for adding a server:
/// 1. [probe] — validates the URL responds as a Jellyfin server. /// 1. [probe] — validates the URL responds as a MediaBrowser server.
/// 2. [authenticateByName] (or future Quick Connect equivalent) — exchanges /// 2. [authenticateByName] (or future Quick Connect equivalent) — exchanges
/// credentials for a long-lived access token and returns a built /// credentials for a long-lived access token and returns a built
/// [JellyfinConnection] ready to insert into [ConnectionRegistry]. /// [JellyfinConnection] ready to insert into [ConnectionRegistry].
@@ -53,12 +55,15 @@ class JellyfinConnectionAuthService {
required this.clientName, required this.clientName,
required this.clientVersion, required this.clientVersion,
required this.deviceName, required this.deviceName,
MediaBrowserDialect dialect = MediaBrowserDialect.jellyfin,
@visibleForTesting this._testHttpClientFactory, @visibleForTesting this._testHttpClientFactory,
}) : _endpointDiscovery = JellyfinEndpointDiscovery(testHttpClientFactory: _testHttpClientFactory); }) : dialect = dialect,
_endpointDiscovery = JellyfinEndpointDiscovery(dialect: dialect, testHttpClientFactory: _testHttpClientFactory);
/// App identity sent in the `MediaBrowser` Authorization header. Jellyfin /// App identity sent in the `MediaBrowser` Authorization header. Jellyfin
/// uses `Client`/`Device`/`DeviceId`/`Version` to populate the device list /// and Emby use `Client`/`Device`/`DeviceId`/`Version` to populate the
/// in its admin UI and to issue tokens. /// device list in their admin UI and to issue tokens.
final MediaBrowserDialect dialect;
final String clientName; final String clientName;
final String clientVersion; final String clientVersion;
final String deviceName; final String deviceName;
@@ -81,7 +86,7 @@ class JellyfinConnectionAuthService {
/// Probe the server identified by [baseUrl] without authenticating. Returns /// Probe the server identified by [baseUrl] without authenticating. Returns
/// the public info used by the UI to confirm "yes that's the right server" /// the public info used by the UI to confirm "yes that's the right server"
/// before asking for credentials. Throws [MediaServerUrlException] when the /// before asking for credentials. Throws [MediaServerUrlException] when the
/// URL is unreachable or doesn't look like a Jellyfin server. /// URL is unreachable or doesn't look like the selected media server.
Future<JellyfinServerInfo> probe(String baseUrl) async { Future<JellyfinServerInfo> probe(String baseUrl) async {
return _endpointDiscovery.probe(baseUrl); return _endpointDiscovery.probe(baseUrl);
} }
@@ -158,10 +163,12 @@ class JellyfinConnectionAuthService {
} }
} }
/// Whether [baseUrl] has Quick Connect enabled. Returns `false` for any /// Whether [baseUrl] has Quick Connect enabled. Returns `false` without a
/// failure — Jellyfin <10.7 returns 404 on this path, and an offline server /// request for dialects that do not support it, and for any probe failure —
/// is functionally indistinguishable from QC-disabled for UI purposes. /// Jellyfin <10.7 returns 404 on this path, and an offline server is
/// functionally indistinguishable from QC-disabled for UI purposes.
Future<bool> isQuickConnectEnabled(String baseUrl) async { Future<bool> isQuickConnectEnabled(String baseUrl) async {
if (!dialect.supportsQuickConnect) return false;
final normalised = _normaliseBaseUrl(baseUrl); final normalised = _normaliseBaseUrl(baseUrl);
final client = _buildHttpClient(baseUrl: normalised); final client = _buildHttpClient(baseUrl: normalised);
try { try {
@@ -184,6 +191,7 @@ class JellyfinConnectionAuthService {
required String baseUrl, required String baseUrl,
required String deviceId, required String deviceId,
}) async { }) async {
_requireQuickConnectSupport();
final validDeviceId = requireJellyfinDeviceId(deviceId); final validDeviceId = requireJellyfinDeviceId(deviceId);
final normalised = _normaliseBaseUrl(baseUrl); final normalised = _normaliseBaseUrl(baseUrl);
final authHeader = buildJellyfinAuthHeader( final authHeader = buildJellyfinAuthHeader(
@@ -228,7 +236,7 @@ class JellyfinConnectionAuthService {
/// in their Jellyfin web UI, then exchange the approved secret for a token /// in their Jellyfin web UI, then exchange the approved secret for a token
/// and return a fully-formed [JellyfinConnection]. Returns `null` on /// and return a fully-formed [JellyfinConnection]. Returns `null` on
/// cancel, timeout, or server-side secret expiry (404 mid-poll). Throws /// cancel, timeout, or server-side secret expiry (404 mid-poll). Throws
/// [MediaServerAuthException] on auth failures (401/403). /// [MediaServerAuthException] on auth failures or an unsupported dialect.
Future<JellyfinConnection?> authenticateByQuickConnect({ Future<JellyfinConnection?> authenticateByQuickConnect({
required String baseUrl, required String baseUrl,
List<String>? baseUrls, List<String>? baseUrls,
@@ -238,6 +246,7 @@ class JellyfinConnectionAuthService {
Duration timeout = const Duration(minutes: 5), Duration timeout = const Duration(minutes: 5),
bool Function()? shouldCancel, bool Function()? shouldCancel,
}) async { }) async {
_requireQuickConnectSupport();
final validDeviceId = requireJellyfinDeviceId(deviceId); final validDeviceId = requireJellyfinDeviceId(deviceId);
final normalised = _normaliseBaseUrl(baseUrl); final normalised = _normaliseBaseUrl(baseUrl);
final info = serverInfo ?? await probe(normalised); final info = serverInfo ?? await probe(normalised);
@@ -331,8 +340,9 @@ class JellyfinConnectionAuthService {
Future<bool> validate(Connection connection) async { Future<bool> validate(Connection connection) async {
if (connection is! JellyfinConnection) return false; if (connection is! JellyfinConnection) return false;
final client = _authenticatedClient(connection); final client = _authenticatedClient(connection);
final currentUser = MediaBrowserPaths(dialect: dialect, userId: connection.userId).currentUser;
try { try {
final response = await client.get('/Users/Me', timeout: MediaServerTimeouts.jellyfinProbe); final response = await client.get(currentUser, timeout: MediaServerTimeouts.jellyfinProbe);
return response.statusCode == 200; return response.statusCode == 200;
} on MediaServerHttpException catch (e) { } on MediaServerHttpException catch (e) {
if (e.statusCode == 401 || e.statusCode == 403) return false; if (e.statusCode == 401 || e.statusCode == 403) return false;
@@ -368,6 +378,15 @@ class JellyfinConnectionAuthService {
} }
} }
/// Emby 4.9.5 returns 404 for every `/QuickConnect/*` route. Emby Connect is
/// a separate account-level product and is not an authentication flow Plezy
/// implements.
void _requireQuickConnectSupport() {
if (!dialect.supportsQuickConnect) {
throw MediaServerAuthException('Quick Connect rejected by server');
}
}
MediaServerHttpClient _authenticatedClient(JellyfinConnection connection) { MediaServerHttpClient _authenticatedClient(JellyfinConnection connection) {
LogRedactionManager.registerToken(connection.accessToken); LogRedactionManager.registerToken(connection.accessToken);
return _buildHttpClient( return _buildHttpClient(
@@ -441,7 +460,7 @@ class JellyfinConnectionAuthService {
/// Build a [JellyfinConnection] from a successful auth/exchange response. /// Build a [JellyfinConnection] from a successful auth/exchange response.
/// Connection id is derived from `(machineId, userId)` so each user on a /// Connection id is derived from `(machineId, userId)` so each user on a
/// given server has a single stable connection row. /// given server has a single stable connection row.
static JellyfinConnection _buildConnection({ JellyfinConnection _buildConnection({
required JellyfinServerInfo info, required JellyfinServerInfo info,
required String normalisedBaseUrl, required String normalisedBaseUrl,
List<String>? baseUrls, List<String>? baseUrls,
@@ -463,6 +482,7 @@ class JellyfinConnectionAuthService {
userName: userName, userName: userName,
accessToken: accessToken, accessToken: accessToken,
deviceId: deviceId, deviceId: deviceId,
dialect: info.dialect ?? dialect,
isAdministrator: isAdministrator, isAdministrator: isAdministrator,
primaryImageTag: primaryImageTag, primaryImageTag: primaryImageTag,
status: ConnectionStatus.online, status: ConnectionStatus.online,
+11 -8
View File
@@ -8,7 +8,7 @@ typedef JellyfinItemCacheKey = ({String scopeId, String machineId, String userId
typedef JellyfinCacheItem = ({ApiCacheData cacheRow, JellyfinItemCacheKey key}); typedef JellyfinCacheItem = ({ApiCacheData cacheRow, JellyfinItemCacheKey key});
typedef ResolvedJellyfinCacheItem = ({ApiCacheData cacheRow, ConnectionRow connection, JellyfinItemCacheKey key}); typedef ResolvedJellyfinCacheItem = ({ApiCacheData cacheRow, ConnectionRow connection, JellyfinItemCacheKey key});
/// Canonical Jellyfin connection and item-cache key resolution. /// Canonical MediaBrowser connection and item-cache key resolution.
class JellyfinCacheResolver { class JellyfinCacheResolver {
JellyfinCacheResolver(this.database); JellyfinCacheResolver(this.database);
@@ -18,6 +18,9 @@ class JellyfinCacheResolver {
static const _usersMarker = ':/Users/'; static const _usersMarker = ':/Users/';
static const _itemsMarker = '/Items/'; static const _itemsMarker = '/Items/';
static Expression<bool> _mediaBrowserKind(GeneratedColumn<String> kind) =>
kind.equals('jellyfin') | kind.equals('emby');
Expression<bool> itemKeyPredicate(GeneratedColumn<String> column, String serverOrScopeId, String itemId) { Expression<bool> itemKeyPredicate(GeneratedColumn<String> column, String serverOrScopeId, String itemId) {
final scope = _splitScope(serverOrScopeId); final scope = _splitScope(serverOrScopeId);
final escapedItemId = _escapeLike(itemId); final escapedItemId = _escapeLike(itemId);
@@ -78,7 +81,7 @@ class JellyfinCacheResolver {
.get(); .get();
if (rows.isEmpty) return const []; if (rows.isEmpty) return const [];
final connections = await (database.select(database.connections)..where((t) => t.kind.equals('jellyfin'))).get(); final connections = await (database.select(database.connections)..where((t) => _mediaBrowserKind(t.kind))).get();
final connectionById = {for (final connection in connections) connection.id: connection}; final connectionById = {for (final connection in connections) connection.id: connection};
final bindings = await database.select(database.profileConnections).get(); final bindings = await database.select(database.profileConnections).get();
final bindingsByConnection = <String, List<ProfileConnectionRow>>{}; final bindingsByConnection = <String, List<ProfileConnectionRow>>{};
@@ -111,11 +114,11 @@ class JellyfinCacheResolver {
return matches; return matches;
} }
/// Resolves the exact persisted Jellyfin cache namespace owned by /// Resolves the exact persisted MediaBrowser cache namespace owned by
/// [profileId] for [serverOrScopeId]. /// [profileId] for [serverOrScopeId].
/// ///
/// The physical download row is deliberately not consulted: it is shared /// The physical download row is deliberately not consulted: it is shared
/// across profiles and may have been created by a different Jellyfin user. /// across profiles and may have been created by a different MediaBrowser user.
Future<String?> findProfileScopeId(String serverOrScopeId, String profileId) async { Future<String?> findProfileScopeId(String serverOrScopeId, String profileId) async {
if (profileId.isEmpty) return null; if (profileId.isEmpty) return null;
final requested = _splitScope(serverOrScopeId); final requested = _splitScope(serverOrScopeId);
@@ -132,7 +135,7 @@ class JellyfinCacheResolver {
if (binding.userIdentifier.isEmpty) continue; if (binding.userIdentifier.isEmpty) continue;
final connection = await (database.select( final connection = await (database.select(
database.connections, database.connections,
)..where((t) => t.id.equals(binding.connectionId) & t.kind.equals('jellyfin'))).getSingleOrNull(); )..where((t) => t.id.equals(binding.connectionId) & _mediaBrowserKind(t.kind))).getSingleOrNull();
if (connection == null) continue; if (connection == null) continue;
final connectionScope = _splitScope(connection.id); final connectionScope = _splitScope(connection.id);
@@ -169,12 +172,12 @@ class JellyfinCacheResolver {
final compoundId = '${scope.machineId}/$expectedUserId'; final compoundId = '${scope.machineId}/$expectedUserId';
final compound = await (database.select( final compound = await (database.select(
database.connections, database.connections,
)..where((t) => t.id.equals(compoundId) & t.kind.equals('jellyfin'))).getSingleOrNull(); )..where((t) => t.id.equals(compoundId) & _mediaBrowserKind(t.kind))).getSingleOrNull();
if (compound != null && await _matchesProfileBinding(compound.id, expectedUserId)) return compound; if (compound != null && await _matchesProfileBinding(compound.id, expectedUserId)) return compound;
final legacy = await (database.select( final legacy = await (database.select(
database.connections, database.connections,
)..where((t) => t.id.equals(scope.machineId) & t.kind.equals('jellyfin'))).getSingleOrNull(); )..where((t) => t.id.equals(scope.machineId) & _mediaBrowserKind(t.kind))).getSingleOrNull();
if (legacy != null && await _matchesProfileBinding(legacy.id, expectedUserId)) return legacy; if (legacy != null && await _matchesProfileBinding(legacy.id, expectedUserId)) return legacy;
return null; return null;
} }
@@ -189,7 +192,7 @@ class JellyfinCacheResolver {
final prefix = '${scope.machineId}/'; final prefix = '${scope.machineId}/';
return (database.select(database.connections) return (database.select(database.connections)
..where((t) => t.id.substr(1, prefix.length).equals(prefix) & t.kind.equals('jellyfin')) ..where((t) => t.id.substr(1, prefix.length).equals(prefix) & _mediaBrowserKind(t.kind))
..orderBy([(t) => OrderingTerm.asc(t.id)]) ..orderBy([(t) => OrderingTerm.asc(t.id)])
..limit(1)) ..limit(1))
.getSingleOrNull(); .getSingleOrNull();
+110 -17
View File
@@ -1,4 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert';
import 'package:flutter/foundation.dart' show visibleForTesting; import 'package:flutter/foundation.dart' show visibleForTesting;
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
@@ -17,6 +18,7 @@ import '../media/media_filter.dart';
import '../media/live_tv_support.dart'; import '../media/live_tv_support.dart';
import '../media/lyrics.dart'; import '../media/lyrics.dart';
import '../media/media_backend.dart'; import '../media/media_backend.dart';
import '../media/media_browser_dialect.dart';
import '../media/media_file_info.dart'; import '../media/media_file_info.dart';
import '../media/media_hub.dart'; import '../media/media_hub.dart';
import '../media/media_item.dart'; import '../media/media_item.dart';
@@ -61,6 +63,7 @@ import 'jellyfin_media_info_mapper.dart';
import 'jellyfin_playback_bundle.dart'; import 'jellyfin_playback_bundle.dart';
import 'jellyfin_playback_urls.dart'; import 'jellyfin_playback_urls.dart';
import 'jellyfin_trickplay_service.dart'; import 'jellyfin_trickplay_service.dart';
import 'media_browser_paths.dart';
import 'playback_initialization_types.dart'; import 'playback_initialization_types.dart';
import 'scrub_preview_source.dart'; import 'scrub_preview_source.dart';
import 'subtitle_preference.dart'; import 'subtitle_preference.dart';
@@ -86,18 +89,79 @@ part 'jellyfin_client/parts/metadata_edit.dart';
/// used by a single part stay declared in that part. /// used by a single part stay declared in that part.
mixin _JellyfinClientInternals on MediaServerCacheMixin { mixin _JellyfinClientInternals on MediaServerCacheMixin {
JellyfinConnection get connection; JellyfinConnection get connection;
MediaBrowserDialect get dialect;
MediaBrowserPaths get paths;
FailoverHttpClient get _http; FailoverHttpClient get _http;
MediaItem? _mapItem(Map<String, dynamic> json); MediaItem? _mapItem(Map<String, dynamic> json);
List<MediaItem> _mapItems(Iterable<Map<String, dynamic>> items); List<MediaItem> _mapItems(Iterable<Map<String, dynamic>> items);
String? _absolutizeImagePath(String? path); String? _absolutizeImagePath(String? path);
/// Row metadata Jellyfin volunteers on `/Items` list responses but Emby
/// withholds unless it is named in `Fields`.
///
/// Measured on Emby 4.9.5 against a movie that carries them all: a row built
/// from [_baseBrowseFields] came back with no `ProductionYear`,
/// `OfficialRating`, `PremiereDate` or `DateCreated`, while the same query
/// naming them returned `2011`, `PG-13`, the premiere date and the library-add
/// time. Jellyfin 10.11 includes the first three in every row regardless.
/// Emby's *detail* route volunteers everything, so only list rows are
/// affected — but that is every card in the app, which would otherwise lose
/// its year and age-rating badge.
///
/// `DateCreated` is load-bearing beyond display: it is the `addedAt` every
/// recency-ordered surface degrades to when a row has never been played.
///
/// `UserDataLastPlayedDate` is the odd one out: it is not an `ItemFields`
/// member but an Emby-specific token, and it is the only way to get
/// `UserData.LastPlayedDate` onto a list row. Measured on Emby 4.9.5: the
/// played date is absent under `Fields=UserData`, `EnableUserData=true` and the
/// user-scoped `Ids=` form, and present only on the single-item detail route or
/// when this token is named. Jellyfin 10.11 volunteers the date on every row and
/// accepts the token without changing its responses, but since it is undocumented
/// there, only Emby is asked for it.
///
/// Every row set needs it, not just the recency-ordered ones: a null played date
/// on a *watched* row makes [JellyfinApiCache.applyWatchState] stamp
/// `DateTime.now()`, so an offline watch-state pull over episode rows would
/// rewrite the cached play time of everything it touched.
static const _embyWithheldRowFields = [
'ProductionYear',
'OfficialRating',
'PremiereDate',
'DateCreated',
'UserDataLastPlayedDate',
];
/// Append the fields this dialect withholds, skipping any the set already
/// names so Jellyfin's request strings stay byte-identical.
String _withDialectRowFields(String fields) {
if (dialect != MediaBrowserDialect.emby) return fields;
final present = fields.split(',').map((field) => field.trim()).toSet();
final missing = _embyWithheldRowFields.where((field) => !present.contains(field));
return missing.isEmpty ? fields : '$fields,${missing.join(',')}';
} }
/// [MediaServerClient] over a Jellyfin server. String get _browseFields => _withDialectRowFields(_baseBrowseFields);
String get _hubRowFields => _withDialectRowFields(_baseHubRowFields);
String get _episodeRowFields => _withDialectRowFields(_baseEpisodeRowFields);
String get _folderBrowseFields => _withDialectRowFields(_baseFolderBrowseFields);
String get _folderRowFields => _withDialectRowFields(_baseFolderRowFields);
String get _musicAlbumRowFields => _withDialectRowFields(_baseMusicAlbumRowFields);
String get _musicTrackRowFields => _withDialectRowFields(_baseMusicTrackRowFields);
String get _queueFields => _withDialectRowFields(_baseQueueFields);
}
/// [MediaServerClient] over a MediaBrowser-family server — Jellyfin or Emby.
/// ///
/// Constructs from a [JellyfinConnection] and a [MediaServerHttpClient] (the /// Constructs from a [JellyfinConnection] and a [MediaServerHttpClient] (the
/// HTTP wrapper is backend-agnostic despite the name). Implements the full /// HTTP wrapper is backend-agnostic despite the name). Implements the full
/// neutral interface: browse, watch state, playlist read, playback session /// neutral interface: browse, watch state, playlist read, playback session
/// reporting, and live TV via [LiveTvSupport]. /// reporting, and live TV via [LiveTvSupport].
///
/// Jellyfin forked from Emby 3.5.2 and the wire contract is still ~95% shared,
/// so one client serves both. [dialect] selects the divergent routes (via
/// [paths]) and the features that exist on only one side — see
/// [MediaBrowserDialect].
class JellyfinClient class JellyfinClient
with with
MediaServerCacheMixin, MediaServerCacheMixin,
@@ -119,7 +183,8 @@ class JellyfinClient
ScopedMediaServerClient, ScopedMediaServerClient,
GracefullyCloseable { GracefullyCloseable {
JellyfinClient._({required this._connection, required this._http, FavoriteChannelsRepository? favoritesRepository}) JellyfinClient._({required this._connection, required this._http, FavoriteChannelsRepository? favoritesRepository})
: _favoritesRepository = favoritesRepository ?? const SharedPreferencesFavoriteChannelsRepository(); : _favoritesRepository = favoritesRepository ?? const SharedPreferencesFavoriteChannelsRepository(),
_paths = MediaBrowserPaths(dialect: _connection.dialect, userId: _connection.userId);
/// Build a fully-initialised [JellyfinClient]. Endpoint reachability is /// Build a fully-initialised [JellyfinClient]. Endpoint reachability is
/// raced before construction by onboarding/profile binding; this factory /// raced before construction by onboarding/profile binding; this factory
@@ -131,6 +196,10 @@ class JellyfinClient
/// reject requests that only carry the legacy `X-Emby-Token` header, /// reject requests that only carry the legacy `X-Emby-Token` header,
/// returning 404 from the proxy or a routing-level handler instead of /// returning 404 from the proxy or a routing-level handler instead of
/// 401. We send `X-Emby-Token` too for old Emby/Jellyfin builds. /// 401. We send `X-Emby-Token` too for old Emby/Jellyfin builds.
///
/// Emby accepts this header pair verbatim: it authored both the
/// `MediaBrowser` Authorization scheme and `X-Emby-Token`, so no dialect
/// branch is needed here (verified against Emby 4.9.5).
static Future<JellyfinClient> create( static Future<JellyfinClient> create(
JellyfinConnection connection, { JellyfinConnection connection, {
FavoriteChannelsRepository? favoritesRepository, FavoriteChannelsRepository? favoritesRepository,
@@ -140,7 +209,7 @@ class JellyfinClient
// HTTP traffic. Orchestration logs contain no literals; this additionally // HTTP traffic. Orchestration logs contain no literals; this additionally
// protects unavoidable network-layer diagnostics. // protects unavoidable network-layer diagnostics.
_registerConnectionDiagnostics(connection); _registerConnectionDiagnostics(connection);
final endpointDiscovery = JellyfinEndpointDiscovery(); final endpointDiscovery = JellyfinEndpointDiscovery(dialect: connection.dialect);
String version = '1.0'; String version = '1.0';
try { try {
final pkg = await PackageInfo.fromPlatform(); final pkg = await PackageInfo.fromPlatform();
@@ -203,7 +272,10 @@ class JellyfinClient
void Function()? onAllEndpointsExhausted, void Function()? onAllEndpointsExhausted,
}) { }) {
_registerConnectionDiagnostics(connection); _registerConnectionDiagnostics(connection);
final endpointDiscovery = JellyfinEndpointDiscovery(testHttpClientFactory: endpointProbeHttpClientFactory); final endpointDiscovery = JellyfinEndpointDiscovery(
dialect: connection.dialect,
testHttpClientFactory: endpointProbeHttpClientFactory,
);
late JellyfinClient client; late JellyfinClient client;
final mediaHttp = FailoverHttpClient( final mediaHttp = FailoverHttpClient(
baseUrl: connection.baseUrl, baseUrl: connection.baseUrl,
@@ -221,11 +293,23 @@ class JellyfinClient
} }
/// Mutable so [isHealthy] can refresh `Policy.IsAdministrator` from the /// Mutable so [isHealthy] can refresh `Policy.IsAdministrator` from the
/// `/Users/Me` probe response — admin status changed server-side should /// current-user probe response — admin status changed server-side should
/// propagate without forcing the user to re-auth. /// propagate without forcing the user to re-auth.
JellyfinConnection _connection; JellyfinConnection _connection;
@override @override
JellyfinConnection get connection => _connection; JellyfinConnection get connection => _connection;
/// Which MediaBrowser dialect this server speaks. Fixed for the lifetime of
/// the client: an endpoint switch can move the base URL but never turns a
/// Jellyfin server into an Emby one.
@override
MediaBrowserDialect get dialect => _connection.dialect;
/// Route builders for the endpoints where the two dialects diverge.
@override
MediaBrowserPaths get paths => _paths;
final MediaBrowserPaths _paths;
@override @override
final FailoverHttpClient _http; final FailoverHttpClient _http;
final FavoriteChannelsRepository _favoritesRepository; final FavoriteChannelsRepository _favoritesRepository;
@@ -275,8 +359,13 @@ class JellyfinClient
String? _absolutizeImagePath(String? path) => _absolutizer.absolutize(path); String? _absolutizeImagePath(String? path) => _absolutizer.absolutize(path);
@override @override
MediaItem? _mapItem(Map<String, dynamic> json) => MediaItem? _mapItem(Map<String, dynamic> json) => JellyfinMappers.mediaItem(
JellyfinMappers.mediaItem(json, serverId: serverId, serverName: serverName, absolutizer: _absolutizer); json,
serverId: serverId,
serverName: serverName,
absolutizer: _absolutizer,
dialect: dialect,
);
@override @override
List<MediaItem> _mapItems(Iterable<Map<String, dynamic>> items) => List<MediaItem> _mapItems(Iterable<Map<String, dynamic>> items) =>
@@ -292,20 +381,23 @@ class JellyfinClient
String? get serverName => connection.serverName; String? get serverName => connection.serverName;
@override @override
MediaBackend get backend => MediaBackend.jellyfin; MediaBackend get backend => dialect.backend;
@override @override
ServerCapabilities get capabilities => ServerCapabilities.jellyfin; ServerCapabilities get capabilities => switch (dialect) {
MediaBrowserDialect.jellyfin => ServerCapabilities.jellyfin,
MediaBrowserDialect.emby => ServerCapabilities.emby,
};
/// Jellyfin doesn't expose a per-server played-threshold pref, so we mirror /// Neither dialect exposes a per-server played-threshold pref, so we mirror
/// Plex's default of 90%. /// Plex's default of 90%.
@override @override
double get watchedThreshold => 0.9; double get watchedThreshold => 0.9;
/// Jellyfin marks an item played from `/Sessions/Playing/Stopped` itself /// Both dialects mark an item played from `/Sessions/Playing/Stopped`
/// (server `MaxResumePct`, default 90%), so the in-player auto-scrobble must /// themselves (server `MaxResumePct`, default 90%), so the in-player
/// not also `POST /UserPlayedItems` — that double-scrobbles via the Trakt /// auto-scrobble must not also POST the played route — that double-scrobbles
/// plugin (#1287). Manual mark-watched still hits `/UserPlayedItems`. /// via the Trakt plugin (#1287). Manual mark-watched still writes it.
@override @override
bool get marksWatchedOnPlaybackStopped => true; bool get marksWatchedOnPlaybackStopped => true;
@@ -316,7 +408,8 @@ class JellyfinClient
Future<void> closeGracefully({Duration drainTimeout = const Duration(seconds: 2)}) => Future<void> closeGracefully({Duration drainTimeout = const Duration(seconds: 2)}) =>
_http.closeGracefully(drainTimeout: drainTimeout); _http.closeGracefully(drainTimeout: drainTimeout);
/// Reachable *and* token-valid. We probe `/Users/Me` (auth-required) /// Reachable *and* token-valid. We probe the current-user route
/// ([MediaBrowserPaths.currentUser], auth-required)
/// rather than `/System/Info/Public` so a revoked token surfaces as /// rather than `/System/Info/Public` so a revoked token surfaces as
/// unhealthy on the very next sweep, instead of waiting for the first /// unhealthy on the very next sweep, instead of waiting for the first
/// real call to 401. /// real call to 401.
@@ -332,7 +425,7 @@ class JellyfinClient
@override @override
Future<HealthStatus> checkHealth() async { Future<HealthStatus> checkHealth() async {
try { try {
final response = await _http.get('/Users/Me', timeout: MediaServerTimeouts.jellyfinProbe); final response = await _http.get(paths.currentUser, timeout: MediaServerTimeouts.jellyfinProbe);
final ok = response.statusCode >= 200 && response.statusCode < 300; final ok = response.statusCode >= 200 && response.statusCode < 300;
if (ok) { if (ok) {
final data = response.data; final data = response.data;
@@ -382,7 +475,7 @@ class JellyfinClient
/// Returns null on transport failures — caller treats as "no preference". /// Returns null on transport failures — caller treats as "no preference".
Future<JellyfinUserProfile?> fetchUserProfile() async { Future<JellyfinUserProfile?> fetchUserProfile() async {
try { try {
final response = await _http.get('/Users/Me'); final response = await _http.get(paths.currentUser);
throwIfHttpError(response); throwIfHttpError(response);
final data = response.data; final data = response.data;
if (data is! Map<String, dynamic>) return null; if (data is! Map<String, dynamic>) return null;
+388 -61
View File
@@ -68,7 +68,7 @@ LibraryPage<T> _pagedItems<T>(
/// `CommaDelimitedCollectionModelBinder` drops them element-by-element and /// `CommaDelimitedCollectionModelBinder` drops them element-by-element and
/// they never did anything. `UserData` is governed by `EnableUserData` /// they never did anything. `UserData` is governed by `EnableUserData`
/// (default true) and `dto.PremiereDate` is set unconditionally. /// (default true) and `dto.PremiereDate` is set unconditionally.
const _browseFields = 'RecursiveItemCount,ChildCount,OriginalTitle,SortName,Overview'; const _baseBrowseFields = 'RecursiveItemCount,ChildCount,OriginalTitle,SortName,Overview';
/// Field set for the home / per-library hub rows (Recently Added, Continue /// Field set for the home / per-library hub rows (Recently Added, Continue
/// Watching, Next Up). Poster cards render artwork, title, year and the /// Watching, Next Up). Poster cards render artwork, title, year and the
@@ -87,7 +87,7 @@ const _browseFields = 'RecursiveItemCount,ChildCount,OriginalTitle,SortName,Over
/// (`Folder.FillUserDataDtoValues`), and [MediaItem.unwatchedCount] falls back /// (`Folder.FillUserDataDtoValues`), and [MediaItem.unwatchedCount] falls back
/// to `UserData.UnplayedItemCount`. Only the season progress bar needs real /// to `UserData.UnplayedItemCount`. Only the season progress bar needs real
/// leaf totals, and seasons never appear on a hub row. /// leaf totals, and seasons never appear on a hub row.
const _hubRowFields = 'Overview'; const _baseHubRowFields = 'Overview';
/// How far back `/Shows/NextUp` looks for a series to resume, mirroring /// How far back `/Shows/NextUp` looks for a series to resume, mirroring
/// Jellyfin web's `maxDaysForNextUp` default. Without it the server's /// Jellyfin web's `maxDaysForNextUp` default. Without it the server's
@@ -101,7 +101,7 @@ String _nextUpDateCutoff() =>
/// Existing episode-row requests can show Plex-style quality labels when the /// Existing episode-row requests can show Plex-style quality labels when the
/// response includes `MediaSources`. Keep this off broad library/search/latest /// response includes `MediaSources`. Keep this off broad library/search/latest
/// queries because it is the heaviest item field Jellyfin returns. /// queries because it is the heaviest item field Jellyfin returns.
const _episodeRowFields = '$_browseFields,MediaSources'; const _baseEpisodeRowFields = '$_baseBrowseFields,MediaSources';
/// Media types global search surfaces. Episodes are included so a user can /// Media types global search surfaces. Episodes are included so a user can
/// find a single episode by name. /// find a single episode by name.
@@ -115,13 +115,13 @@ const _searchItemTypes = 'Movie,Series,Episode,MusicAlbum,Audio';
/// Jellyfin web's folder view requests none of them either. The unwatched /// Jellyfin web's folder view requests none of them either. The unwatched
/// badge survives via `UserData.UnplayedItemCount` /// badge survives via `UserData.UnplayedItemCount`
/// ([MediaItem.unwatchedCount] fallback). /// ([MediaItem.unwatchedCount] fallback).
const _folderBrowseFields = 'UserData,PremiereDate,OriginalTitle,SortName'; const _baseFolderBrowseFields = 'UserData,PremiereDate,OriginalTitle,SortName';
/// Folder-tree field set for FILESYSTEM FOLDER children, which render only /// Folder-tree field set for FILESYSTEM FOLDER children, which render only
/// their name. Queried with `EnableUserData=false`: user data on a folder dto /// their name. Queried with `EnableUserData=false`: user data on a folder dto
/// makes the server compute a recursive unplayed count per folder, by far the /// makes the server compute a recursive unplayed count per folder, by far the
/// dominant cost of folder browsing (see [_fetchFolderChildren]). /// dominant cost of folder browsing (see [_fetchFolderChildren]).
const _folderRowFields = 'SortName'; const _baseFolderRowFields = 'SortName';
/// Latest Albums hub row. `/Users/{id}/Items/Latest` on a music library /// Latest Albums hub row. `/Users/{id}/Items/Latest` on a music library
/// returns MusicAlbum FOLDER dtos, so [_browseFields] would trigger the same /// returns MusicAlbum FOLDER dtos, so [_browseFields] would trigger the same
@@ -133,13 +133,13 @@ const _folderRowFields = 'SortName';
/// filesystem folder rows. Trade-off: fully played albums lose the watched /// filesystem folder rows. Trade-off: fully played albums lose the watched
/// checkmark on this row (Jellyfin web's latest-albums row shows no play /// checkmark on this row (Jellyfin web's latest-albums row shows no play
/// state either). /// state either).
const _musicAlbumRowFields = 'PremiereDate,OriginalTitle,SortName'; const _baseMusicAlbumRowFields = 'PremiereDate,OriginalTitle,SortName';
/// Played-track hub rows (Recently Played / Most Played): Audio LEAF dtos. /// Played-track hub rows (Recently Played / Most Played): Audio LEAF dtos.
/// Keeps `UserData` — a cheap direct lookup on leaves that drives the /// Keeps `UserData` — a cheap direct lookup on leaves that drives the
/// play-state overlay — and drops the folder count fields (meaningless on /// play-state overlay — and drops the folder count fields (meaningless on
/// Audio) and `Overview` (never rendered on track cards). /// Audio) and `Overview` (never rendered on track cards).
const _musicTrackRowFields = 'UserData,PremiereDate,OriginalTitle,SortName'; const _baseMusicTrackRowFields = 'UserData,PremiereDate,OriginalTitle,SortName';
/// Even slimmer set used by [fetchClientSideEpisodeQueue]. Queue rows /// Even slimmer set used by [fetchClientSideEpisodeQueue]. Queue rows
/// only need title, thumbnail (`ImageTags['Primary']`), season/episode /// only need title, thumbnail (`ImageTags['Primary']`), season/episode
@@ -149,7 +149,7 @@ const _musicTrackRowFields = 'UserData,PremiereDate,OriginalTitle,SortName';
/// Specials interleave — see [compareEpisodesByWatchOrder]). Drops /// Specials interleave — see [compareEpisodesByWatchOrder]). Drops
/// `Overview` etc. so even a thousand-episode shounen show fits in one /// `Overview` etc. so even a thousand-episode shounen show fits in one
/// response. /// response.
const _queueFields = 'UserData,PremiereDate'; const _baseQueueFields = 'UserData,PremiereDate';
/// Page size for [fetchClientSideEpisodeQueue]. Keeps each server response /// Page size for [fetchClientSideEpisodeQueue]. Keeps each server response
/// bounded while still returning the full series queue. /// bounded while still returning the full series queue.
@@ -177,11 +177,14 @@ const _seriesLastPlayedLookupLimit = 24;
/// shelf is better off unstamped than waiting on the 10s/120s shared defaults. /// shelf is better off unstamped than waiting on the 10s/120s shared defaults.
const _seriesLastPlayedRequestTimeout = Duration(seconds: 3); const _seriesLastPlayedRequestTimeout = Duration(seconds: 3);
/// Hard ceiling on [_attachSeriesLastPlayed]. On expiry it aborts the in-flight /// Hard ceiling on [_attachSeriesLastPlayed] and on the reconstructed Next Up
/// batch, so it bounds the whole pass regardless of how many batches remain or /// pass, recency probe included. On expiry it aborts whatever is in flight, so it
/// which request phase a lookup is stuck in. Two orders of magnitude under the /// bounds the whole pass regardless of how many batches remain or which request
/// 10s-connect/120s-receive defaults the single unscoped scan ran with, so a /// phase a lookup is stuck in — the per-request timeouts alone cannot, because
/// stalled endpoint cannot make the scoped form slower than the query it fixes. /// each applies to the connect and receive phases independently. Two orders of
/// magnitude under the 10s-connect/120s-receive defaults the single unscoped scan
/// ran with, so a stalled endpoint cannot make the scoped form slower than the
/// query it fixes.
const _seriesLastPlayedBudget = Duration(seconds: 4); const _seriesLastPlayedBudget = Duration(seconds: 4);
const _childrenPageSize = 500; const _childrenPageSize = 500;
@@ -203,8 +206,8 @@ String _jellyfinFolderSortName(Map<String, dynamic> item) {
return raw.toLowerCase(); return raw.toLowerCase();
} }
/// `/Items/Filters` is a legacy unpaged endpoint; keep failures isolated from /// Aggregate/facet filter lookups are kept isolated from the paged Browse tab
/// the paged Browse tab so very large libraries can still open. /// so failures on very large libraries do not prevent the library from opening.
const _filtersTimeout = Duration(seconds: 8); const _filtersTimeout = Duration(seconds: 8);
/// Full field set for the detail screen and the resume / next-up /// Full field set for the detail screen and the resume / next-up
@@ -212,24 +215,22 @@ const _filtersTimeout = Duration(seconds: 8);
const _detailFields = const _detailFields =
'Overview,Genres,People,Studios,ProductionLocations,Tags,Taglines,DateCreated,DateLastSaved,' 'Overview,Genres,People,Studios,ProductionLocations,Tags,Taglines,DateCreated,DateLastSaved,'
'PremiereDate,RecursiveItemCount,ChildCount,UserData,MediaSources,OriginalTitle,SortName,' 'PremiereDate,RecursiveItemCount,ChildCount,UserData,MediaSources,OriginalTitle,SortName,'
// Chapters: Jellyfin returns them at the item level; the playback // Chapters: both dialects return them at the item level; playback plucks
// init flow plucks `raw['Chapters']` and feeds the seek-bar tick UI. // `raw['Chapters']` and feeds the seek-bar tick UI.
'Chapters,' 'Chapters,'
// Trickplay: per-resolution sprite-sheet manifest. The scrub-thumbnail // Trickplay: Jellyfin's per-resolution sprite-sheet manifest. Emby 4.9.5
// loader reads `raw['Trickplay']` and computes tile URLs from it. // tolerates this unknown field selection and never populates the field.
'Trickplay,' 'Trickplay,'
// ProviderIds carries Tmdb/Imdb/Tvdb keys — required for Trakt + the // ProviderIds carries Tmdb/Imdb/Tvdb keys — required for Trakt + the
// unified tracker coordinator to scrobble Jellyfin items without // unified tracker coordinator to scrobble MediaBrowser items without an
// any extra round-trip. // extra round-trip.
'ProviderIds'; 'ProviderIds';
mixin _JellyfinBrowseMethods on _JellyfinClientInternals { mixin _JellyfinBrowseMethods on _JellyfinClientInternals {
// Endpoint conventions follow what the official Jellyfin Kotlin SDK // Shared endpoints and query shapes follow the official Jellyfin SDK so
// generates (cross-checked against the Findroid client). The SDK mixes // Jellyfin requests remain unchanged. [MediaBrowserPaths] owns the measured
// `/Users/{userId}/...` for "user library" / "views" / "latest" / "single // route differences: Emby 4.9.5 requires the older user-scoped spellings,
// item" calls and `/Items?userId=...` for the generic list and resume // while the Jellyfin spellings remain unprefixed.
// endpoints. We mirror that exactly so requests hash the same way against
// proxy rules and rate limiters as a stock Jellyfin app.
/// Views as of the last load, reused by scoped search. /// Views as of the last load, reused by scoped search.
/// ///
@@ -278,7 +279,7 @@ mixin _JellyfinBrowseMethods on _JellyfinClientInternals {
abort?.throwIfAborted(); abort?.throwIfAborted();
throwIfHttpError(response); throwIfHttpError(response);
final items = _itemsArray(response.data); final items = _itemsArray(response.data);
// Jellyfin surfaces the user's collection (BoxSet) and playlist roots as // Both MediaBrowser dialects surface collection (BoxSet) and playlist roots as
// top-level views. We expose those as per-library tabs instead of // top-level views. We expose those as per-library tabs instead of
// standalone library entries — matches the Plex shape and avoids // standalone library entries — matches the Plex shape and avoids
// duplicating the same data in two navigation slots. // duplicating the same data in two navigation slots.
@@ -287,7 +288,7 @@ mixin _JellyfinBrowseMethods on _JellyfinClientInternals {
final ct = (view['CollectionType'] as String?)?.toLowerCase(); final ct = (view['CollectionType'] as String?)?.toLowerCase();
return ct != 'boxsets' && ct != 'playlists'; return ct != 'boxsets' && ct != 'playlists';
}) })
.map((view) => JellyfinMappers.library(view, serverId: serverId, serverName: serverName)) .map((view) => JellyfinMappers.library(view, serverId: serverId, serverName: serverName, dialect: dialect))
.whereType<MediaLibrary>() .whereType<MediaLibrary>()
.toList(); .toList();
} }
@@ -348,13 +349,13 @@ mixin _JellyfinBrowseMethods on _JellyfinClientInternals {
} }
/// Jellyfin's `/Items/Filters` returns Genres / OfficialRatings / Tags / /// Jellyfin's `/Items/Filters` returns Genres / OfficialRatings / Tags /
/// Categories + values from `/Items/Filters` in a single call. The /// Years in one call. Emby has no aggregate or official-rating route, so its
/// unwatched/unplayed boolean is synthetic because Jellyfin exposes it as /// branch concurrently reads `/Genres`, `/Tags`, and `/Years`. The
/// an `/Items` query filter, not a filter-listing category. Keys are /// unwatched/unplayed boolean remains synthetic because both dialects expose
/// translated to Plex's filter naming so the existing filter-param map /// it as an `/Items` query filter. Keys are translated to Plex's filter
/// round-trips through `_buildFilterParams` unchanged; the synthesised /// naming so the existing filter-param map round-trips through
/// `MediaFilter.key` is prefixed `jellyfin:` so FiltersBottomSheet can /// `_buildFilterParams` unchanged; the synthesised `MediaFilter.key` keeps
/// recognise it as cached and skip the per-category value fetch. /// the historic `jellyfin:` prefix so existing cached preferences remain valid.
@override @override
Future<LibraryFilterResult> fetchLibraryFiltersWithValues(String libraryId, {MediaKind? libraryKind}) async { Future<LibraryFilterResult> fetchLibraryFiltersWithValues(String libraryId, {MediaKind? libraryKind}) async {
final filters = <MediaFilter>[ final filters = <MediaFilter>[
@@ -382,13 +383,25 @@ mixin _JellyfinBrowseMethods on _JellyfinClientInternals {
return raw.whereType<String>().where((s) => s.isNotEmpty).toList(); return raw.whereType<String>().where((s) => s.isNotEmpty).toList();
} }
List<String> yearList(Object? raw) {
if (raw is! List) return const [];
return raw
.map(
(year) => switch (year) {
final num value => value.toInt().toString(),
final String value => value,
_ => '',
},
)
.where((year) => year.isNotEmpty)
.toList();
}
final raw = <String, List<String>>{ final raw = <String, List<String>>{
'genre': stringList(data['Genres']), 'genre': stringList(data['Genres']),
'contentRating': stringList(data['OfficialRatings']), 'contentRating': stringList(data['OfficialRatings']),
'tag': stringList(data['Tags']), 'tag': stringList(data['Tags']),
'year': (data['Years'] is List) 'year': yearList(data['Years']),
? (data['Years'] as List).whereType<num>().map((y) => y.toInt().toString()).toList()
: const <String>[],
}; };
const order = ['genre', 'year', 'contentRating', 'tag']; const order = ['genre', 'year', 'contentRating', 'tag'];
@@ -417,6 +430,11 @@ mixin _JellyfinBrowseMethods on _JellyfinClientInternals {
} }
Future<Map<String, dynamic>?> _safeFetchFilterPayload(String libraryId) async { Future<Map<String, dynamic>?> _safeFetchFilterPayload(String libraryId) async {
if (!dialect.supportsAggregateItemFilters) {
// Emby 4.9.5 returns 404 for `/Items/Filters`; its three facet routes below are verified 200.
return _safeFetchFilterFacets(libraryId);
}
try { try {
final response = await _http.get( final response = await _http.get(
'/Items/Filters', '/Items/Filters',
@@ -433,6 +451,50 @@ mixin _JellyfinBrowseMethods on _JellyfinClientInternals {
} }
} }
/// Reassemble the `/Items/Filters` payload from Emby's per-facet routes.
///
/// Note the route spellings: Emby serves the ratings facet at
/// `/OfficialRatings`, not the `/Items/OfficialRatings` form its siblings
/// might suggest (that one 404s). Jellyfin has none of these four and answers
/// the aggregate route instead.
Future<Map<String, dynamic>> _safeFetchFilterFacets(String libraryId) async {
final facets = await Future.wait([
_safeFetchFilterFacet('/Genres', libraryId),
_safeFetchFilterFacet('/OfficialRatings', libraryId),
_safeFetchFilterFacet('/Tags', libraryId),
_safeFetchFilterFacet('/Years', libraryId),
]);
return {'Genres': facets[0], 'OfficialRatings': facets[1], 'Tags': facets[2], 'Years': facets[3]};
}
Future<List<String>> _safeFetchFilterFacet(String endpoint, String libraryId) async {
try {
final response = await _http.get(
endpoint,
// `Recursive=true` is required: without it Emby only considers the
// library view's direct children and every facet comes back empty
// (measured against Emby 4.9.5 — `/Years` returns 0 vs 15 rows).
queryParameters: {'UserId': connection.userId, 'ParentId': libraryId, 'Recursive': 'true'},
timeout: _filtersTimeout,
);
throwIfHttpError(response);
final data = response.data;
if (data is! Map<String, dynamic>) return const [];
final items = data['Items'];
if (items is! List) return const [];
final names = <String>[];
for (final item in items) {
if (item is! Map<String, dynamic>) continue;
final name = item['Name'];
if (name is String && name.isNotEmpty) names.add(name);
}
return names;
} catch (e, st) {
appLogger.w('MediaBrowserClient: $endpoint filter facet unavailable', error: e, stackTrace: st);
return const [];
}
}
/// Jellyfin has no `/sorts` listing endpoint, so this returns a hardcoded /// Jellyfin has no `/sorts` listing endpoint, so this returns a hardcoded
/// list based on the broad sort set Streamyfin exposes. Keys remain /// list based on the broad sort set Streamyfin exposes. Keys remain
/// backend-neutral where Plezy already had saved preferences (`rating`, /// backend-neutral where Plezy already had saved preferences (`rating`,
@@ -1498,8 +1560,9 @@ mixin _JellyfinBrowseMethods on _JellyfinClientInternals {
@override @override
Future<List<MediaItem>> fetchContinueWatching({int? count = 20}) async { Future<List<MediaItem>> fetchContinueWatching({int? count = 20}) async {
final results = await Future.wait([ final results = await Future.wait([
_fetchItemsArray('/UserItems/Resume', { _fetchItemsArray(_resumePath, {
'userId': connection.userId, 'userId': connection.userId,
..._resumeFilterQuery,
'Limit': ?count?.toString(), 'Limit': ?count?.toString(),
'Fields': _hubRowFields, 'Fields': _hubRowFields,
'MediaTypes': 'Video', 'MediaTypes': 'Video',
@@ -1507,7 +1570,7 @@ mixin _JellyfinBrowseMethods on _JellyfinClientInternals {
'EnableTotalRecordCount': 'false', 'EnableTotalRecordCount': 'false',
...jellyfinImageQueryParameters, ...jellyfinImageQueryParameters,
}, retry: _continueWatchingRetry), }, retry: _continueWatchingRetry),
_safeFetchItemsArray('/Shows/NextUp', { _fetchNextUpRows({
'userId': connection.userId, 'userId': connection.userId,
'Limit': ?count?.toString(), 'Limit': ?count?.toString(),
'Fields': _hubRowFields, 'Fields': _hubRowFields,
@@ -1518,9 +1581,13 @@ mixin _JellyfinBrowseMethods on _JellyfinClientInternals {
}, retry: _continueWatchingRetry), }, retry: _continueWatchingRetry),
]); ]);
final nextUp = _mapItems(results[1]);
return _mergeContinueWatchingAndNextUp( return _mergeContinueWatchingAndNextUp(
resume: _mapItems(results.first), resume: _mapItems(results.first),
nextUp: await _attachSeriesLastPlayed(_mapItems(results[1])), // Only the server-side shelf needs enriching: `/Shows/NextUp` rows carry no
// series play date, while the reconstructed rows were already stamped from
// the recency query that ordered them.
nextUp: dialect.supportsGlobalNextUp ? await _attachSeriesLastPlayed(nextUp) : nextUp,
limit: count, limit: count,
); );
} }
@@ -1631,8 +1698,9 @@ mixin _JellyfinBrowseMethods on _JellyfinClientInternals {
final results = await Future.wait([ final results = await Future.wait([
latestFuture, latestFuture,
_safeFetchItemsArray('/UserItems/Resume', { _safeFetchItemsArray(_resumePath, {
'userId': connection.userId, 'userId': connection.userId,
..._resumeFilterQuery,
'ParentId': ?parentId, 'ParentId': ?parentId,
'Limit': limit.toString(), 'Limit': limit.toString(),
'Fields': _hubRowFields, 'Fields': _hubRowFields,
@@ -1642,7 +1710,7 @@ mixin _JellyfinBrowseMethods on _JellyfinClientInternals {
...jellyfinImageQueryParameters, ...jellyfinImageQueryParameters,
}, retry: retry), }, retry: retry),
includeNextUp includeNextUp
? _safeFetchItemsArray('/Shows/NextUp', { ? _fetchNextUpRows({
'userId': connection.userId, 'userId': connection.userId,
'ParentId': ?parentId, 'ParentId': ?parentId,
'Limit': limit.toString(), 'Limit': limit.toString(),
@@ -1657,7 +1725,10 @@ mixin _JellyfinBrowseMethods on _JellyfinClientInternals {
return [ return [
hub('continue', continueTitle, 'mixed', results[1]), hub('continue', continueTitle, 'mixed', results[1]),
hub('nextup', nextUpTitle, 'episode', results[2]), // The reconstructed Emby shelf is capped by the series lookup limit rather
// than the caller's, so slice it to the requested preview size the way the
// server-side query already does for Jellyfin.
hub('nextup', nextUpTitle, 'episode', results[2].take(limit).toList(growable: false)),
hub('recent', recentTitle, 'mixed', results.first), hub('recent', recentTitle, 'mixed', results.first),
].where((h) => h.items.isNotEmpty).toList(); ].where((h) => h.items.isNotEmpty).toList();
} }
@@ -1816,9 +1887,10 @@ mixin _JellyfinBrowseMethods on _JellyfinClientInternals {
); );
case 'continue': case 'continue':
return _safeFetchMediaPage( return _safeFetchMediaPage(
'/UserItems/Resume', _resumePath,
{ {
'userId': connection.userId, 'userId': connection.userId,
..._resumeFilterQuery,
'StartIndex': offset.toString(), 'StartIndex': offset.toString(),
'Limit': effectiveLimit, 'Limit': effectiveLimit,
'Fields': _hubRowFields, 'Fields': _hubRowFields,
@@ -1832,9 +1904,7 @@ mixin _JellyfinBrowseMethods on _JellyfinClientInternals {
abort: abort, abort: abort,
); );
case 'nextup': case 'nextup':
return _safeFetchMediaPage( final nextUpQuery = <String, dynamic>{
'/Shows/NextUp',
{
'userId': connection.userId, 'userId': connection.userId,
'StartIndex': offset.toString(), 'StartIndex': offset.toString(),
'Limit': effectiveLimit, 'Limit': effectiveLimit,
@@ -1844,11 +1914,21 @@ mixin _JellyfinBrowseMethods on _JellyfinClientInternals {
'NextUpDateCutoff': _nextUpDateCutoff(), 'NextUpDateCutoff': _nextUpDateCutoff(),
'EnableTotalRecordCount': 'true', 'EnableTotalRecordCount': 'true',
...jellyfinImageQueryParameters, ...jellyfinImageQueryParameters,
}, };
if (dialect.supportsGlobalNextUp) {
return _safeFetchMediaPage(
'/Shows/NextUp',
nextUpQuery,
offset: offset, offset: offset,
requestedSize: pageSize, requestedSize: pageSize,
abort: abort, abort: abort,
); );
}
// Emby's shelf is assembled client-side and is capped by
// [_seriesLastPlayedLookupLimit], so it is paged in memory.
final rows = await _fetchNextUpRows(nextUpQuery, abort: abort);
final window = rows.skip(offset).take(pageSize).toList(growable: false);
return LibraryPage<MediaItem>(items: _mapItems(window), totalCount: rows.length, offset: offset);
case 'recentlyplayed': case 'recentlyplayed':
case 'mostplayed': case 'mostplayed':
return _safeFetchMediaPage( return _safeFetchMediaPage(
@@ -1925,22 +2005,16 @@ mixin _JellyfinBrowseMethods on _JellyfinClientInternals {
].where((h) => h.items.isNotEmpty).toList(); ].where((h) => h.items.isNotEmpty).toList();
} }
/// Jellyfin exposes local trailers separately from special features. Combine /// Both dialects expose local trailers separately from special features.
/// both into Plezy's existing extras row, but keep remote/YouTube trailers /// Combine them into Plezy's existing extras row, but keep remote/YouTube
/// out of scope because they are external URLs, not playable Jellyfin items. /// trailers out of scope because they are external URLs, not playable items.
@override @override
Future<List<MediaItem>> fetchExtras(String id) async { Future<List<MediaItem>> fetchExtras(String id) async {
if (isOfflineMode) return const []; if (isOfflineMode) return const [];
final results = await Future.wait([ final results = await Future.wait([
_safeFetchItemsArray('/Items/${_segment(id)}/LocalTrailers', { _safeFetchItemsArray(paths.localTrailers(id), {'userId': connection.userId, ...jellyfinImageQueryParameters}),
'userId': connection.userId, _safeFetchItemsArray(paths.specialFeatures(id), {'userId': connection.userId, ...jellyfinImageQueryParameters}),
...jellyfinImageQueryParameters,
}),
_safeFetchItemsArray('/Items/${_segment(id)}/SpecialFeatures', {
'userId': connection.userId,
...jellyfinImageQueryParameters,
}),
]); ]);
return _playableExtrasFromRaw(results.expand((items) => items)); return _playableExtrasFromRaw(results.expand((items) => items));
@@ -2043,6 +2117,259 @@ mixin _JellyfinBrowseMethods on _JellyfinClientInternals {
]; ];
} }
/// Route and extra query that list genuinely in-progress items.
///
/// Jellyfin's dedicated resume route already returns only started items. Emby's
/// does not: measured against one in-progress movie and 30 started series it
/// returned 30 rows — the movie plus every started series' next episode — with
/// the movie sorted *last*, and no `Filters` value removed them. Paging that is
/// unusable, so Emby reads the ordinary `/Items` route with
/// `Filters=IsResumable`, which returned exactly the movie
/// (`TotalRecordCount: 1`) and supports the same sort, paging and `ParentId`
/// scoping. Those next episodes reach the UI through the Next Up shelf, which
/// is where Plezy models them.
Map<String, dynamic> get _resumeFilterQuery => dialect.resumeReturnsOnlyStartedItems
? const {}
: const {'Filters': 'IsResumable', 'SortBy': 'DatePlayed', 'SortOrder': 'Descending'};
/// `/Items` on Emby (see [_resumeFilterQuery]); the dedicated route otherwise.
String get _resumePath => dialect.resumeReturnsOnlyStartedItems ? paths.resumeItems : '/Items';
/// Library-wide Next Up rows.
///
/// Jellyfin computes this server-side. Emby only computes Next Up per series
/// — an unscoped `/Shows/NextUp` returns nothing there under any parameter
/// combination (see [MediaBrowserDialect.supportsGlobalNextUp]) — so this
/// reconstructs the shelf in two phases:
///
/// 1. one `/Items` query for the most recently played episodes, newest first,
/// which yields the started series in exactly the order the shelf wants;
/// 2. one `/Shows/NextUp?SeriesId=` per distinct series, bounded by the same
/// concurrency and lookup cap [_attachSeriesLastPlayed] uses, and by one
/// [_seriesLastPlayedBudget] wall clock that covers phase 1 as well, so a
/// slow server cannot hold the home screen.
///
/// Rows come back series-recency-ordered, which is what the caller's merge
/// expects, and a failed per-series lookup drops only that series.
Future<List<Map<String, dynamic>>> _fetchNextUpRows(
Map<String, dynamic> queryParameters, {
_HubRetryPolicy? retry,
AbortController? abort,
}) async {
if (dialect.supportsGlobalNextUp) {
return _safeFetchItemsArray('/Shows/NextUp', queryParameters, retry: retry, abort: abort);
}
final budgetAbort = AbortController();
final deadline = Timer(_seriesLastPlayedBudget, budgetAbort.abort);
// The caller's cancellation has to reach the requests this pass owns, which
// all run under [budgetAbort].
if (abort != null) unawaited(abort.trigger.then((_) => budgetAbort.abort()));
try {
return await _reconstructNextUpRows(queryParameters, abort: abort, budgetAbort: budgetAbort);
} finally {
deadline.cancel();
budgetAbort.abort();
}
}
/// The reconstruction itself, under a caller-supplied wall-clock budget.
Future<List<Map<String, dynamic>>> _reconstructNextUpRows(
Map<String, dynamic> queryParameters, {
required AbortController? abort,
required AbortController budgetAbort,
}) async {
final probe = _fetchRecentlyPlayedSeriesIds(parentId: queryParameters['ParentId'] as String?, abort: budgetAbort);
final List<(String, String?)> probed;
try {
// Raced, not just awaited: aborting is advisory at the transport, and the
// probe's own timeout applies to the connect and receive phases
// independently, so a delayed header plus a stalled body could outlive the
// deadline. Same policy as the per-series batches below.
probed = await Future.any([probe, budgetAbort.trigger.then((_) => const <(String, String?)>[])]);
} on MediaServerHttpException catch (e) {
// Our own deadline expiring is not a disrupted fetch: this shelf is
// best-effort, so an over-budget probe costs the Next Up half rather than
// the caller's whole request. A cancellation the *caller* asked for still
// propagates.
if (e.isCancellation && abort?.isAborted != true) return const [];
rethrow;
} finally {
// The loser may still fail later; its outcome is no longer anyone's.
probe.ignore();
}
// Before the early return below: an empty probe can also mean the caller
// cancelled mid-request and the transport reported an ordinary timeout, which
// must not be published as "nothing to watch next".
abort?.throwIfAborted();
final recentSeries = _withinNextUpCutoff(probed, queryParameters['NextUpDateCutoff'] as String?);
if (recentSeries.isEmpty) return const [];
final seriesIds = [for (final (seriesId, _) in recentSeries) seriesId];
// Per-series queries carry the caller's field/image selection but never its
// paging: the shelf is assembled here and sliced by the caller.
final perSeriesQuery = <String, dynamic>{
for (final entry in queryParameters.entries)
if (entry.key != 'StartIndex' && entry.key != 'Limit' && entry.key != 'ParentId') entry.key: entry.value,
'Limit': '1',
};
final rowsBySeries = <String, Map<String, dynamic>>{};
for (var start = 0; start < seriesIds.length; start += _seriesLastPlayedConcurrency) {
if (budgetAbort.isAborted || abort?.isAborted == true) break;
final batch = seriesIds.skip(start).take(_seriesLastPlayedConcurrency);
// Each lookup records itself as it completes, so a sibling that is still
// slow when the deadline fires costs only its own row. Awaiting the batch
// as one future would discard every result it had already collected.
final lookups = [
for (final seriesId in batch)
_fetchSeriesNextUp(seriesId, perSeriesQuery, budgetAbort).then((result) {
final (id, row) = result;
if (row != null) rowsBySeries[id] = row;
}),
];
final batchDone = Future.wait(lookups);
await Future.any([batchDone, budgetAbort.trigger]);
// No-op once the batch has won; suppresses the loser's late completion.
batchDone.ignore();
}
// The loop breaks on either abort, and the caller's means disruption: a
// partial shelf must not be published as though the series had no next
// episode. Our own budget expiring keeps degrading to whatever was collected.
abort?.throwIfAborted();
// Reassemble in series-recency order rather than completion order, stamping
// each row with its series' newest play date. A Next Up episode has never
// been played, so its own `UserData` carries no date, and the shelf's sort
// key would otherwise fall back to when the episode was added to the
// library. The date comes from the same response that established this
// order, so no extra request is needed.
return [
for (final (seriesId, lastPlayed) in recentSeries)
if (rowsBySeries[seriesId] case final row?)
if (lastPlayed == null)
row
else
{
...row,
// Tolerant read: a dto whose `UserData` is absent or not an object
// still gets a date rather than raising past the shelf's guards.
'UserData': {
...?(row['UserData'] is Map<String, dynamic> ? row['UserData'] as Map<String, dynamic> : null),
'LastPlayedDate': lastPlayed,
},
},
];
}
/// Drop series whose newest play predates [cutoff], the window Jellyfin's own
/// `/Shows/NextUp` applies through `NextUpDateCutoff`.
///
/// Measured on Emby 4.9.5: `/Shows/NextUp` ignores `NextUpDateCutoff` entirely
/// — a cutoff of 2030 still returned the row that a 2019 cutoff did, while
/// Jellyfin 10.11 returned nothing for a future cutoff. Without this the
/// reconstructed shelf would resurrect series the user abandoned years ago that
/// Jellyfin hides.
///
/// Filtered here rather than on the server because Emby has no played-date
/// filter for this scan: `MinDatePlayed` and `MinDateLastPlayed` are both
/// silently ignored (`TotalRecordCount` unchanged at 53 with a cutoff of 2030),
/// while `MinDateLastSaved`, `MinDateCreated` and `MinPremiereDate` do filter
/// but on unrelated dates. The scan's own cap therefore still counts rows
/// outside the window; that only shortens an already best-effort shelf.
///
/// A series the server gave no date for is kept: the date is missing only when
/// the server withheld it, and an unfiltered row is a better failure than a
/// silently empty shelf.
List<(String, String?)> _withinNextUpCutoff(List<(String, String?)> series, String? cutoff) {
final threshold = cutoff == null ? null : DateTime.tryParse(cutoff);
if (threshold == null) return series;
return [
for (final entry in series)
if (entry.$2 == null || !(DateTime.tryParse(entry.$2!)?.isBefore(threshold) ?? false)) entry,
];
}
/// Distinct series ids behind the user's most recently played episodes, newest
/// first, capped at [_seriesLastPlayedLookupLimit], each paired with that
/// series' newest play date.
///
/// `Filters=IsPlayed` + `SortBy=DatePlayed` is the only library-wide recency
/// signal Emby exposes for series: a series dto carries neither a usable
/// `PlayCount` nor a `DatePlayed` sort key, so the episodes have to supply the
/// order. Because the rows arrive newest-first, the first row naming a series
/// *is* that series' newest play, so this one request yields both the shelf
/// order and the timestamp each reconstructed row needs — no per-series
/// enrichment round trip.
Future<List<(String, String?)>> _fetchRecentlyPlayedSeriesIds({String? parentId, AbortController? abort}) async {
final rows = await _safeFetchItemsArray(
'/Items',
{
'userId': connection.userId,
'ParentId': ?parentId,
'Recursive': 'true',
'IncludeItemTypes': 'Episode',
'Filters': 'IsPlayed',
'SortBy': 'DatePlayed',
'SortOrder': 'Descending',
// Eight played episodes per shelf slot: enough that a few binged series
// near the top do not starve the rest. A user who watched more than this
// window inside a single series gets a shorter shelf, never a wrong one —
// the series they were last watching still ranks first, which is the
// ordering the shelf exists to show.
'Limit': (_seriesLastPlayedLookupLimit * 8).toString(),
'Fields': _withDialectRowFields('SeriesId'),
'EnableImages': 'false',
'EnableTotalRecordCount': 'false',
},
timeout: _seriesLastPlayedRequestTimeout,
abort: abort,
// Best-effort shelf: a timeout or 5xx here must not move the client's
// active endpoint, which is what every other hub leg also avoids.
//
// This [timeout] bounds a single request phase, not the probe: like every
// per-call timeout it applies to connect and receive independently. The
// wall clock comes from the caller's shared [_seriesLastPlayedBudget],
// which covers this probe and the per-series lookups together.
//
// Deliberately *not* the hub retry policy: `_getItemsResponse` ignores
// `timeout` on the retried path, which would leave this request bounded
// only by the 15s home / 20s library hub deadline.
allowEndpointFailover: false,
);
// Insertion-ordered map: preserves the server's recency ordering, and keeps
// each series' first (newest) play date rather than a later, older one.
final lastPlayedBySeries = <String, String?>{};
for (final row in rows) {
final seriesId = row['SeriesId'];
if (seriesId is! String || seriesId.isEmpty || lastPlayedBySeries.containsKey(seriesId)) continue;
final userData = row['UserData'];
lastPlayedBySeries[seriesId] = userData is Map ? userData['LastPlayedDate'] as String? : null;
if (lastPlayedBySeries.length >= _seriesLastPlayedLookupLimit) break;
}
return [for (final entry in lastPlayedBySeries.entries) (entry.key, entry.value)];
}
/// The single Next Up episode for [seriesId], or null when the series has none
/// left — or when the lookup failed, which drops only this series.
Future<(String, Map<String, dynamic>?)> _fetchSeriesNextUp(
String seriesId,
Map<String, dynamic> queryParameters,
AbortController abort,
) async {
final rows = await _safeFetchItemsArray(
'/Shows/NextUp',
{...queryParameters, 'SeriesId': seriesId},
abort: abort,
timeout: _seriesLastPlayedRequestTimeout,
allowEndpointFailover: false,
);
return (seriesId, rows.isEmpty ? null : rows.first);
}
/// Newest `LastPlayedDate` across [seriesId]'s episodes, or null when the /// Newest `LastPlayedDate` across [seriesId]'s episodes, or null when the
/// series has never been played — or when the lookup failed, in which case the /// series has never been played — or when the lookup failed, in which case the
/// row keeps a null date and degrades to its `addedAt` in the shelf sort. /// row keeps a null date and degrades to its `addedAt` in the shelf sort.
+17 -12
View File
@@ -33,7 +33,7 @@ mixin _JellyfinLiveTvMethods on _JellyfinClientInternals {
} }
return false; return false;
} catch (e) { } catch (e) {
appLogger.d('Jellyfin Live TV probe failed', error: e); appLogger.d('${dialect.productName} Live TV probe failed', error: e);
return false; return false;
} }
} }
@@ -53,8 +53,8 @@ mixin _JellyfinLiveTvMethods on _JellyfinClientInternals {
/// EPG / programs grid. [channelIds] scopes to specific channels (when /// EPG / programs grid. [channelIds] scopes to specific channels (when
/// empty, the server returns programs across all channels). [beginsAt] / /// empty, the server returns programs across all channels). [beginsAt] /
/// [endsAt] are epoch seconds and bound the time window — Jellyfin uses /// [endsAt] are epoch seconds and bound the time window — both MediaBrowser
/// ISO 8601 strings on the wire. /// dialects use ISO 8601 strings on the wire.
Future<List<LiveTvProgram>> fetchLiveTvPrograms({ Future<List<LiveTvProgram>> fetchLiveTvPrograms({
List<String> channelIds = const [], List<String> channelIds = const [],
int? beginsAt, int? beginsAt,
@@ -143,7 +143,7 @@ mixin _JellyfinLiveTvMethods on _JellyfinClientInternals {
LiveTvSupport get liveTv => _JellyfinLiveTvSupport(this as JellyfinClient); LiveTvSupport get liveTv => _JellyfinLiveTvSupport(this as JellyfinClient);
} }
/// Adapter from [LiveTvSupport] to Jellyfin channel/program helpers. /// Adapter from [LiveTvSupport] to MediaBrowser channel/program helpers.
class _JellyfinLiveTvSupport implements LiveTvSupport { class _JellyfinLiveTvSupport implements LiveTvSupport {
final JellyfinClient _client; final JellyfinClient _client;
_JellyfinLiveTvSupport(this._client); _JellyfinLiveTvSupport(this._client);
@@ -178,8 +178,8 @@ class _JellyfinLiveTvSupport implements LiveTvSupport {
if (sources.isEmpty) return null; if (sources.isEmpty) return null;
final firstSource = sources.first; final firstSource = sources.first;
if (firstSource is! Map<String, dynamic>) { if (firstSource is! Map<String, dynamic>) {
throw const PlaybackException( throw PlaybackException(
'Jellyfin returned invalid Live TV playback data', '${_client.dialect.productName} returned invalid Live TV playback data',
reason: PlaybackFailureReason.invalidPlaybackData, reason: PlaybackFailureReason.invalidPlaybackData,
); );
} }
@@ -192,12 +192,12 @@ class _JellyfinLiveTvSupport implements LiveTvSupport {
var liveStreamId = nonEmptyString(source['LiveStreamId']); var liveStreamId = nonEmptyString(source['LiveStreamId']);
final rawUrl = nonEmptyString(source['TranscodingUrl']); final rawUrl = nonEmptyString(source['TranscodingUrl']);
if (rawUrl == null) { if (rawUrl == null) {
appLogger.w('Jellyfin Live TV negotiation returned no HLS transcode URL'); appLogger.w('${_client.dialect.productName} Live TV negotiation returned no HLS transcode URL');
return null; return null;
} }
final rawUri = Uri.tryParse(rawUrl); final rawUri = Uri.tryParse(rawUrl);
if (rawUri == null || !rawUri.path.toLowerCase().endsWith('.m3u8')) { if (rawUri == null || !rawUri.path.toLowerCase().endsWith('.m3u8')) {
appLogger.w('Jellyfin Live TV negotiation returned no HLS transcode URL'); appLogger.w('${_client.dialect.productName} Live TV negotiation returned no HLS transcode URL');
return null; return null;
} }
final url = _client._withApiKey(rawUrl); final url = _client._withApiKey(rawUrl);
@@ -222,8 +222,9 @@ class _JellyfinLiveTvSupport implements LiveTvSupport {
} }
/// SharedPreferences key for the locally-persisted favorite-channel list. /// SharedPreferences key for the locally-persisted favorite-channel list.
/// Keyed by the compound connection id (`{machineId}/{userId}`) so two /// Keyed by the compound connection id (`{machineId}/{userId}`) so users on
/// Jellyfin users on the same server don't share favorites. /// the same MediaBrowser server don't share favorites.
// Keep the legacy prefix: the connection id isolates both dialects, and changing it would lose Jellyfin ordering.
String get _favoritesPrefsKey => 'jellyfin_fav_channels:${_client.connection.id}'; String get _favoritesPrefsKey => 'jellyfin_fav_channels:${_client.connection.id}';
/// Legacy bare-machineId key, kept for one-shot migration. /// Legacy bare-machineId key, kept for one-shot migration.
@@ -266,7 +267,11 @@ class _JellyfinLiveTvSupport implements LiveTvSupport {
} catch (error, stackTrace) { } catch (error, stackTrace) {
firstError ??= error; firstError ??= error;
firstStackTrace ??= stackTrace; firstStackTrace ??= stackTrace;
appLogger.w('Failed to update a Jellyfin favorite channel', error: error, stackTrace: stackTrace); appLogger.w(
'Failed to update a ${_client.dialect.productName} favorite channel',
error: error,
stackTrace: stackTrace,
);
} }
} }
@@ -291,7 +296,7 @@ class _JellyfinLiveTvSupport implements LiveTvSupport {
} }
} }
/// A Jellyfin live playback session: one negotiated HLS transcode URL plus /// A MediaBrowser live playback session: one negotiated HLS transcode URL plus
/// `/Sessions/Playing*` heartbeats via [JellyfinLiveSessionTracker]. No /// `/Sessions/Playing*` heartbeats via [JellyfinLiveSessionTracker]. No
/// program-scoped session and no time-shift — [recover] re-opens the same /// program-scoped session and no time-shift — [recover] re-opens the same
/// negotiated URL. /// negotiated URL.
@@ -50,6 +50,14 @@ mixin _JellyfinMetadataEditMethods on _JellyfinClientInternals {
return response.statusCode >= 200 && response.statusCode < 300; return response.statusCode >= 200 && response.statusCode < 300;
} }
/// Upload custom artwork for [itemId].
///
/// The body must be base64 **text**, not the raw bytes: both dialects reject
/// a binary body with HTTP 500 (Emby 4.9.5 says so explicitly — `The input is
/// not a valid Base-64 string` — and Jellyfin 10.11 answers a bare
/// `Error processing request.`), and both accept the encoded form with 204.
/// The `Content-Type` still names the *image* type, which is how the server
/// decides the on-disk extension.
Future<bool> uploadItemImage( Future<bool> uploadItemImage(
String itemId, { String itemId, {
required String imageType, required String imageType,
@@ -58,7 +66,7 @@ mixin _JellyfinMetadataEditMethods on _JellyfinClientInternals {
}) async { }) async {
final response = await _http.post( final response = await _http.post(
'/Items/${_segment(itemId)}/Images/${_segment(imageType)}', '/Items/${_segment(itemId)}/Images/${_segment(imageType)}',
body: bytes, body: base64Encode(bytes),
headers: {'Content-Type': contentType}, headers: {'Content-Type': contentType},
); );
throwIfHttpError(response); throwIfHttpError(response);
@@ -66,13 +66,17 @@ mixin _JellyfinMusicMethods on _JellyfinClientInternals {
return _mapItems(_itemsArray(response.data)); return _mapItems(_itemsArray(response.data));
} }
/// Lyrics for [track] from `/Audio/{id}/Lyrics`. Jellyfin's `LyricDto` /// Lyrics for [track] from Jellyfin's `/Audio/{id}/Lyrics`. `LyricDto`
/// carries per-line `Start` offsets in ticks when the source is an LRC / /// carries per-line `Start` offsets in ticks when the source is an LRC /
/// synced provider; `IsSynced` is absent on some server versions, so /// synced provider; `IsSynced` is absent on some server versions, so
/// synced-ness is inferred from any line carrying a `Start`. 404 means /// synced-ness is inferred from any line carrying a `Start`. A Jellyfin 404
/// the track has no lyrics → `null`. /// means the track has no lyrics → `null`; Emby is rejected before the request.
@override @override
Future<Lyrics?> fetchLyrics(MediaItem track) async { Future<Lyrics?> fetchLyrics(MediaItem track) async {
if (!dialect.supportsLyrics) {
// Emby 4.9.5 binds `Lyrics` as an audio container and starts a failing ffmpeg transcode, so this call is harmful.
return null;
}
try { try {
final response = await _http.get('/Audio/${_segment(track.id)}/Lyrics'); final response = await _http.get('/Audio/${_segment(track.id)}/Lyrics');
throwIfHttpError(response); throwIfHttpError(response);
@@ -10,10 +10,10 @@ bool _canUseJellyfinStaticStreamFallback(Object error) {
} }
mixin _JellyfinPlaybackMethods on _JellyfinClientInternals { mixin _JellyfinPlaybackMethods on _JellyfinClientInternals {
/// Backend-neutral [PlaybackExtras] for [itemId]. Jellyfin exposes chapters /// Backend-neutral [PlaybackExtras] for [itemId]. Both dialects expose
/// at the item level (`raw['Chapters']`) and native skip segments through a /// chapters at the item level (`raw['Chapters']`), while only Jellyfin exposes
/// separate `/MediaSegments/{itemId}` endpoint. Segment loading is best-effort /// native skip segments through `/MediaSegments/{itemId}`. Segment loading is
/// so older servers still use chapter title fallback. /// best-effort so unsupported and older servers use chapter title fallback.
@override @override
Future<PlaybackExtras> fetchPlaybackExtras( Future<PlaybackExtras> fetchPlaybackExtras(
String itemId, { String itemId, {
@@ -71,6 +71,7 @@ mixin _JellyfinPlaybackMethods on _JellyfinClientInternals {
required MediaItem item, required MediaItem item,
required MediaSourceInfo mediaSource, required MediaSourceInfo mediaSource,
}) async { }) async {
// Emby 4.9.5 has neither the `Trickplay` field nor the tile route; its capabilities stop URL construction here.
if (!capabilities.scrubThumbnails) return null; if (!capabilities.scrubThumbnails) return null;
final manifest = mediaSource.trickplayByWidth; final manifest = mediaSource.trickplayByWidth;
if (manifest == null || manifest.isEmpty) return null; if (manifest == null || manifest.isEmpty) return null;
@@ -83,6 +84,10 @@ mixin _JellyfinPlaybackMethods on _JellyfinClientInternals {
} }
Future<List<MediaMarker>> _fetchMediaSegmentMarkers(String itemId) async { Future<List<MediaMarker>> _fetchMediaSegmentMarkers(String itemId) async {
if (!dialect.supportsMediaSegments) {
// Emby 4.9.5 returns 404 for `/MediaSegments/{itemId}`; empty markers preserve the chapter-name fallback.
return const [];
}
final endpoint = JellyfinApiCache.mediaSegmentsEndpoint(itemId); final endpoint = JellyfinApiCache.mediaSegmentsEndpoint(itemId);
try { try {
return await fetchWithCacheFallback<List<MediaMarker>>( return await fetchWithCacheFallback<List<MediaMarker>>(
@@ -126,14 +131,13 @@ mixin _JellyfinPlaybackMethods on _JellyfinClientInternals {
return uri.replace(queryParameters: params).toString(); return uri.replace(queryParameters: params).toString();
} }
/// Jellyfin playback URL resolution. /// MediaBrowser playback URL resolution.
/// ///
/// Always POSTs `/Items/{id}/PlaybackInfo` so Jellyfin can resolve external /// Always POSTs `/Items/{id}/PlaybackInfo` so the server can resolve external
/// audio/subtitle streams server-side. Uses the returned `TranscodingUrl` /// audio/subtitle streams server-side. Uses the returned `TranscodingUrl`
/// when the caller asked for a capped quality; otherwise — and on any /// when the caller asked for a capped quality; otherwise — and on any
/// DirectPlay decision — builds the static direct stream URL /// DirectPlay decision — builds the shared static direct stream URL
/// (`/Videos/{id}/stream?Static=true&api_key=...`) itself, because Jellyfin /// (`/Videos/{id}/stream?Static=true&api_key=...`) itself.
/// never returns a direct-play URL of its own.
/// ///
/// The returned `MediaSourceInfo` is what the player uses for track-picker /// The returned `MediaSourceInfo` is what the player uses for track-picker
/// labels and auto-track selection by language. /// labels and auto-track selection by language.
@@ -764,13 +768,28 @@ mixin _JellyfinPlaybackMethods on _JellyfinClientInternals {
'PlayMethod': playMethod ?? 'DirectPlay', 'PlayMethod': playMethod ?? 'DirectPlay',
'RepeatMode': 'RepeatNone', 'RepeatMode': 'RepeatNone',
'PlaybackOrder': 'Default', 'PlaybackOrder': 'Default',
'PlaySessionId': ?playSessionId, 'PlaySessionId': ?_resolvePlaySessionId(playSessionId, itemId),
'LiveStreamId': ?liveStreamId, 'LiveStreamId': ?liveStreamId,
}, },
); );
throwIfHttpError(response); throwIfHttpError(response);
} }
/// Session id for a `/Sessions/Playing*` body.
///
/// Normally the caller forwards the id returned by the PlaybackInfo
/// negotiation. Callers that never negotiated one — the offline
/// watch-progress sync, which replays a recorded position — leave it null,
/// which Emby rejects with HTTP 400 (see
/// [MediaBrowserDialect.requiresPlaySessionId]). The synthesized id is
/// derived from [itemId] so the started/progress/stopped triple of one replay
/// lands on a single server-side session row instead of orphaning each call.
String? _resolvePlaySessionId(String? playSessionId, String itemId) {
if (playSessionId != null) return playSessionId;
if (!dialect.requiresPlaySessionId) return null;
return 'plezy-replay-$itemId';
}
/// Tell the server the user has started playing [itemId]. /// Tell the server the user has started playing [itemId].
/// ///
/// [duration] is accepted for interface symmetry with Plex but ignored — /// [duration] is accepted for interface symmetry with Plex but ignored —
@@ -847,7 +866,7 @@ mixin _JellyfinPlaybackMethods on _JellyfinClientInternals {
'MediaSourceId': ?mediaSourceId, 'MediaSourceId': ?mediaSourceId,
'PositionTicks': msToJellyfinTicks(position.inMilliseconds), 'PositionTicks': msToJellyfinTicks(position.inMilliseconds),
'Failed': false, 'Failed': false,
'PlaySessionId': ?playSessionId, 'PlaySessionId': ?_resolvePlaySessionId(playSessionId, itemId),
'LiveStreamId': ?liveStreamId, 'LiveStreamId': ?liveStreamId,
}, },
); );
@@ -37,16 +37,28 @@ mixin _JellyfinPlaylistMethods on _JellyfinClientInternals {
return LibraryPage<MediaPlaylist>(items: const [], totalCount: 0, offset: offset); return LibraryPage<MediaPlaylist>(items: const [], totalCount: 0, offset: offset);
} }
// Emby returns the entire item index when `/Items` carries a `MediaTypes`
// filter alongside `IncludeItemTypes=Playlist`, and never types a playlist
// DTO, so it lists every playlist and the requested type only decides how
// the results are labelled. See
// [MediaBrowserDialect.playlistsFilterByMediaType].
final filterByMediaType = dialect.playlistsFilterByMediaType;
final labelType = requestedType.isEmpty ? 'video' : requestedType;
final response = await _http.get( final response = await _http.get(
'/Items', '/Items',
queryParameters: { queryParameters: {
'userId': connection.userId, 'userId': connection.userId,
'IncludeItemTypes': 'Playlist', 'IncludeItemTypes': 'Playlist',
'Recursive': 'true', 'Recursive': 'true',
'MediaTypes': ?mediaType, 'MediaTypes': ?(filterByMediaType ? mediaType : null),
'StartIndex': offset.toString(), 'StartIndex': offset.toString(),
'Limit': pageSize.toString(), 'Limit': pageSize.toString(),
'Fields': 'Overview,DateCreated,DateLastSaved,ChildCount,Tags', // `DateModified` carries the modification time on Emby, which leaves
// `DateLastSaved` null for playlists. Unlike the detail route, the list
// route honours `Fields` strictly, so the mapper's fallback is dead
// unless the field is requested here.
'Fields': 'Overview,DateCreated,DateLastSaved,DateModified,ChildCount,Tags',
...jellyfinImageQueryParameters, ...jellyfinImageQueryParameters,
}, },
abort: abort, abort: abort,
@@ -56,7 +68,7 @@ mixin _JellyfinPlaylistMethods on _JellyfinClientInternals {
response.data, response.data,
offset: offset, offset: offset,
requestedSize: pageSize, requestedSize: pageSize,
map: (raw) => raw.map(_playlistFromJson).toList(), map: (raw) => raw.map((json) => _playlistFromJson(json, labelType: labelType)).toList(),
); );
} }
@@ -66,7 +78,7 @@ mixin _JellyfinPlaylistMethods on _JellyfinClientInternals {
if (item == null) return null; if (item == null) return null;
return MediaPlaylist( return MediaPlaylist(
id: item.id, id: item.id,
backend: MediaBackend.jellyfin, backend: dialect.backend,
title: item.title ?? t.playlists.playlist, title: item.title ?? t.playlists.playlist,
summary: item.summary, summary: item.summary,
smart: false, smart: false,
@@ -141,13 +153,13 @@ mixin _JellyfinPlaylistMethods on _JellyfinClientInternals {
@override @override
Future<bool> deletePlaylist(MediaPlaylist playlist) async { Future<bool> deletePlaylist(MediaPlaylist playlist) async {
// Jellyfin treats playlists as items — same delete endpoint. // Both MediaBrowser dialects treat playlists as items — same delete endpoint.
final response = await _http.delete('/Items/${_segment(playlist.id)}'); final response = await _http.delete('/Items/${_segment(playlist.id)}');
throwIfHttpError(response); throwIfHttpError(response);
return true; return true;
} }
/// Jellyfin's move endpoint takes an absolute index, so [afterItem] is /// The MediaBrowser move endpoint takes an absolute index, so [afterItem] is
/// ignored — its sibling Plex impl needs it for `?after=`. The "wrong /// ignored — its sibling Plex impl needs it for `?after=`. The "wrong
/// backend" / "missing playlistItemId" branches still return `false` /// backend" / "missing playlistItemId" branches still return `false`
/// (business not-applicable, not a network error) so callers can revert /// (business not-applicable, not a network error) so callers can revert
@@ -193,18 +205,23 @@ mixin _JellyfinPlaylistMethods on _JellyfinClientInternals {
return true; return true;
} }
MediaPlaylist _playlistFromJson(Map<String, dynamic> json) { /// [labelType] backs `MediaType` when the server omits it — always the case
/// on Emby, which leaves playlists untyped.
MediaPlaylist _playlistFromJson(Map<String, dynamic> json, {String labelType = 'video'}) {
final id = json['Id'] as String? ?? ''; final id = json['Id'] as String? ?? '';
return MediaPlaylist( return MediaPlaylist(
id: id, id: id,
backend: MediaBackend.jellyfin, backend: dialect.backend,
title: json['Name'] as String? ?? t.playlists.playlist, title: json['Name'] as String? ?? t.playlists.playlist,
summary: json['Overview'] as String?, summary: json['Overview'] as String?,
smart: false, smart: false,
playlistType: (json['MediaType'] as String?)?.toLowerCase() ?? 'video', playlistType: (json['MediaType'] as String?)?.toLowerCase() ?? labelType,
leafCount: json['ChildCount'] as int?, leafCount: json['ChildCount'] as int?,
addedAt: jellyfinIsoToEpochSeconds(json['DateCreated'] as String?), addedAt: jellyfinIsoToEpochSeconds(json['DateCreated'] as String?),
updatedAt: jellyfinIsoToEpochSeconds(json['DateLastSaved'] as String?), // Emby leaves `DateLastSaved` null on a playlist and carries the
// timestamp in `DateModified`; the item and library mappers already use
// this same fallback.
updatedAt: jellyfinIsoToEpochSeconds(json['DateLastSaved'] as String? ?? json['DateModified'] as String?),
thumbPath: _absolutizeImagePath(_imageTagPath(id, json['ImageTags'])), thumbPath: _absolutizeImagePath(_imageTagPath(id, json['ImageTags'])),
serverId: serverId, serverId: serverId,
serverName: serverName, serverName: serverName,
@@ -3,38 +3,43 @@ part of '../../jellyfin_client.dart';
mixin _JellyfinWatchStateMethods on _JellyfinClientInternals { mixin _JellyfinWatchStateMethods on _JellyfinClientInternals {
@override @override
Future<void> markWatched(MediaItem item) async { Future<void> markWatched(MediaItem item) async {
final response = await _http.post( final response = await _http.post(paths.playedItem(item.id), queryParameters: {'userId': connection.userId});
'/UserPlayedItems/${_segment(item.id)}',
queryParameters: {'userId': connection.userId},
);
throwIfHttpError(response); throwIfHttpError(response);
} }
@override @override
Future<void> markUnwatched(MediaItem item) async { Future<void> markUnwatched(MediaItem item) async {
final response = await _http.delete( final response = await _http.delete(paths.playedItem(item.id), queryParameters: {'userId': connection.userId});
'/UserPlayedItems/${_segment(item.id)}', throwIfHttpError(response);
queryParameters: {'userId': connection.userId}, }
);
/// Hide [item] from Continue Watching while keeping its resume position.
///
/// Emby-only. `POST /Users/{uid}/Items/{id}/HideFromResume?Hide=true` drops
/// the row from `/Users/{uid}/Items/Resume` and leaves
/// `UserData.PlaybackPositionTicks` untouched (verified on Emby 4.9.5).
/// Jellyfin 10.11 has no equivalent route, so it keeps throwing and
/// [ServerCapabilities.continueWatchingRemoval] keeps the affordance hidden.
@override
Future<void> removeFromContinueWatching(MediaItem item) async {
if (!dialect.supportsContinueWatchingRemoval) {
throw UnsupportedError('${dialect.productName} does not support removing items from Continue Watching.');
}
final response = await _http.post(paths.hideFromResume(item.id), queryParameters: {'Hide': 'true'});
throwIfHttpError(response); throwIfHttpError(response);
} }
@override
Future<void> removeFromContinueWatching(MediaItem item) async {
throw UnsupportedError('Jellyfin does not support removing items from Continue Watching.');
}
@override @override
Future<void> rate(MediaItem item, double rating) async { Future<void> rate(MediaItem item, double rating) async {
// Lossy mapping — Jellyfin only stores a binary like/dislike. Treat // Lossy mapping — the MediaBrowser API only stores a binary like/dislike.
// a negative input as "clear the rating" (DELETE), >= 6/10 as a like // Treat a negative input as "clear the rating" (DELETE), >= 6/10 as a like
// (POST Likes=true), and the rest as a dislike (POST Likes=false). // (POST Likes=true), and the rest as a dislike (POST Likes=false).
// No longer reachable from the rate sheet, which uses [setFavorite] // No longer reachable from the rate sheet, which uses [setFavorite]
// for Jellyfin instead; kept as transport for the abstract member. // for MediaBrowser servers instead; kept as transport for the abstract member.
final response = rating < 0 final response = rating < 0
? await _http.delete('/UserItems/${_segment(item.id)}/Rating', queryParameters: {'userId': connection.userId}) ? await _http.delete(paths.itemRating(item.id), queryParameters: {'userId': connection.userId})
: await _http.post( : await _http.post(
'/UserItems/${_segment(item.id)}/Rating', paths.itemRating(item.id),
queryParameters: {'userId': connection.userId, 'Likes': (rating >= 6.0).toString()}, queryParameters: {'userId': connection.userId, 'Likes': (rating >= 6.0).toString()},
); );
throwIfHttpError(response); throwIfHttpError(response);
@@ -44,9 +49,9 @@ mixin _JellyfinWatchStateMethods on _JellyfinClientInternals {
Future<void> setFavorite(MediaItem item, bool isFavorite) => _setItemFavorite(item.id, isFavorite); Future<void> setFavorite(MediaItem item, bool isFavorite) => _setItemFavorite(item.id, isFavorite);
/// Toggle the per-user `IsFavorite` flag for [itemId]. Backs [setFavorite] /// Toggle the per-user `IsFavorite` flag for [itemId]. Backs [setFavorite]
/// and the live-TV favorite-channel adapter; works on any Jellyfin item. /// and the live-TV favorite-channel adapter; works on either MediaBrowser dialect.
Future<void> _setItemFavorite(String itemId, bool isFavorite) async { Future<void> _setItemFavorite(String itemId, bool isFavorite) async {
final path = '/UserFavoriteItems/${_segment(itemId)}'; final path = paths.favoriteItem(itemId);
final response = isFavorite final response = isFavorite
? await _http.post(path, queryParameters: {'userId': connection.userId}) ? await _http.post(path, queryParameters: {'userId': connection.userId})
: await _http.delete(path, queryParameters: {'userId': connection.userId}); : await _http.delete(path, queryParameters: {'userId': connection.userId});
+45 -22
View File
@@ -3,23 +3,28 @@ import 'dart:async';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import '../exceptions/media_server_exceptions.dart'; import '../exceptions/media_server_exceptions.dart';
import '../media/media_browser_dialect.dart';
import '../utils/endpoint_race.dart'; import '../utils/endpoint_race.dart';
import '../utils/log_redaction_manager.dart'; import '../utils/log_redaction_manager.dart';
import '../utils/media_server_http_client.dart'; import '../utils/media_server_http_client.dart';
import '../utils/media_server_timeouts.dart'; import '../utils/media_server_timeouts.dart';
import '../utils/url_utils.dart'; import '../utils/url_utils.dart';
/// Result of a successful Jellyfin URL probe (`/System/Info/Public`). /// Result of a successful MediaBrowser server URL probe (`/System/Info/Public`).
class JellyfinServerInfo { class JellyfinServerInfo {
final String serverName; final String serverName;
/// Server's `Id` field — Jellyfin's machine identifier (UUID hex). /// Server's `Id` field — its stable machine identifier.
final String machineId; final String machineId;
/// Server's reported version string. /// Server's reported version string.
final String version; final String version;
const JellyfinServerInfo({required this.serverName, required this.machineId, required this.version}); /// Dialect detected from public system info, or `null` when the response has
/// no trustworthy Jellyfin/Emby discriminator.
final MediaBrowserDialect? dialect;
const JellyfinServerInfo({required this.serverName, required this.machineId, required this.version, this.dialect});
} }
class JellyfinEndpointRaceResult { class JellyfinEndpointRaceResult {
@@ -100,8 +105,9 @@ class JellyfinEndpointUserInputCandidates {
class JellyfinEndpointDiscovery { class JellyfinEndpointDiscovery {
static const int defaultPort = 8096; static const int defaultPort = 8096;
JellyfinEndpointDiscovery({this._testHttpClientFactory}); JellyfinEndpointDiscovery({this.dialect = MediaBrowserDialect.jellyfin, this._testHttpClientFactory});
final MediaBrowserDialect dialect;
final http.Client Function()? _testHttpClientFactory; final http.Client Function()? _testHttpClientFactory;
MediaServerHttpClient _buildHttpClient({required String baseUrl}) { MediaServerHttpClient _buildHttpClient({required String baseUrl}) {
@@ -140,10 +146,15 @@ class JellyfinEndpointDiscovery {
final id = data['Id']; final id = data['Id'];
final name = data['ServerName'] ?? data['LocalAddress']; final name = data['ServerName'] ?? data['LocalAddress'];
if (id is! String || name is! String) { if (id is! String || name is! String) {
throw MediaServerUrlException('Server response missing Id/ServerName — not a Jellyfin server?'); throw MediaServerUrlException('Server response missing Id/ServerName — not a ${dialect.productName} server?');
} }
return ( return (
serverInfo: JellyfinServerInfo(serverName: name, machineId: id, version: data['Version'] as String? ?? ''), serverInfo: JellyfinServerInfo(
serverName: name,
machineId: id,
version: data['Version'] as String? ?? '',
dialect: MediaBrowserDialect.detectFromPublicSystemInfo(data),
),
effectiveBaseUrl: effectiveBaseUrl, effectiveBaseUrl: effectiveBaseUrl,
); );
} on MediaServerUrlException { } on MediaServerUrlException {
@@ -160,7 +171,7 @@ class JellyfinEndpointDiscovery {
} }
} }
/// Races public Jellyfin probes and returns persistence-safe endpoints. /// Races public MediaBrowser server probes and returns persistence-safe endpoints.
/// ///
/// [baseUrlsToPersist] contains caller-selected persistence candidates. /// [baseUrlsToPersist] contains caller-selected persistence candidates.
/// Candidates that reported another machine are excluded; candidates that /// Candidates that reported another machine are excluded; candidates that
@@ -175,7 +186,7 @@ class JellyfinEndpointDiscovery {
}) async { }) async {
final urls = normalizeBaseUrls(baseUrls); final urls = normalizeBaseUrls(baseUrls);
if (urls.isEmpty) { if (urls.isEmpty) {
throw MediaServerUrlException('Enter at least one Jellyfin server URL'); throw MediaServerUrlException('Enter at least one ${dialect.productName} server URL');
} }
final persistUrls = baseUrlsToPersist == null ? urls : normalizeBaseUrls(baseUrlsToPersist); final persistUrls = baseUrlsToPersist == null ? urls : normalizeBaseUrls(baseUrlsToPersist);
@@ -199,7 +210,7 @@ class JellyfinEndpointDiscovery {
EndpointRaceSelection<JellyfinEndpointCandidate, JellyfinEndpointProbeResult>? bestSelection; EndpointRaceSelection<JellyfinEndpointCandidate, JellyfinEndpointProbeResult>? bestSelection;
await for (final selection in raceEndpointCandidates<JellyfinEndpointCandidate, JellyfinEndpointProbeResult>( await for (final selection in raceEndpointCandidates<JellyfinEndpointCandidate, JellyfinEndpointProbeResult>(
label: 'Jellyfin server URL', label: '${dialect.productName} server URL',
candidates: candidates, candidates: candidates,
preferredUrl: preferred, preferredUrl: preferred,
urlOf: (candidate) => candidate.url, urlOf: (candidate) => candidate.url,
@@ -232,7 +243,7 @@ class JellyfinEndpointDiscovery {
final selected = bestSelection ?? firstSelection; final selected = bestSelection ?? firstSelection;
if (selected == null || selected.result.serverInfo == null) { if (selected == null || selected.result.serverInfo == null) {
throw MediaServerUrlException('No reachable Jellyfin server found'); throw MediaServerUrlException('No reachable ${dialect.productName} server found');
} }
final Map<JellyfinEndpointCandidate, JellyfinEndpointProbeResult> successfulResults = final Map<JellyfinEndpointCandidate, JellyfinEndpointProbeResult> successfulResults =
@@ -256,7 +267,7 @@ class JellyfinEndpointDiscovery {
final selectedInfo = selectedResult.serverInfo; final selectedInfo = selectedResult.serverInfo;
if (selectedInfo == null) { if (selectedInfo == null) {
throw MediaServerUrlException('No reachable Jellyfin server found'); throw MediaServerUrlException('No reachable ${dialect.productName} server found');
} }
final expected = hasExpectedMachineId ? expectedMachineIdTrimmed! : selectedInfo.machineId; final expected = hasExpectedMachineId ? expectedMachineIdTrimmed! : selectedInfo.machineId;
@@ -270,7 +281,7 @@ class JellyfinEndpointDiscovery {
final candidate = _selectValidationCandidate(groupResults, expectedMachineId: expectedMachineIdTrimmed); final candidate = _selectValidationCandidate(groupResults, expectedMachineId: expectedMachineIdTrimmed);
final info = candidate == null ? null : groupResults[candidate]?.serverInfo; final info = candidate == null ? null : groupResults[candidate]?.serverInfo;
if (info != null && info.machineId != expected) { if (info != null && info.machineId != expected) {
throw MediaServerUrlException('The URLs point to different Jellyfin servers'); throw MediaServerUrlException('The URLs point to different ${dialect.productName} servers');
} }
} }
} }
@@ -279,13 +290,13 @@ class JellyfinEndpointDiscovery {
if (!validateUrlSet.contains(entry.key.url)) continue; if (!validateUrlSet.contains(entry.key.url)) continue;
final info = entry.value.serverInfo; final info = entry.value.serverInfo;
if (info != null && info.machineId != expected) { if (info != null && info.machineId != expected) {
throw MediaServerUrlException('The URLs point to different Jellyfin servers'); throw MediaServerUrlException('The URLs point to different ${dialect.productName} servers');
} }
} }
} }
if (selectedInfo.machineId != expected) { if (selectedInfo.machineId != expected) {
throw MediaServerUrlException('The URL does not match this Jellyfin server'); throw MediaServerUrlException('The URL does not match this ${dialect.productName} server');
} }
final effectiveUrls = <String, String>{}; final effectiveUrls = <String, String>{};
@@ -392,7 +403,7 @@ class JellyfinEndpointDiscovery {
return _selectLowestLatencyCandidate(results); return _selectLowestLatencyCandidate(results);
} }
static String _resolveEffectiveBaseUrl(String requestedBaseUrl, MediaServerResponse response) { String _resolveEffectiveBaseUrl(String requestedBaseUrl, MediaServerResponse response) {
final requestedUri = response.requestUri; final requestedUri = response.requestUri;
final effectiveUri = response.effectiveUri; final effectiveUri = response.effectiveUri;
if (requestedUri == null || effectiveUri == null || effectiveUri == requestedUri) { if (requestedUri == null || effectiveUri == null || effectiveUri == requestedUri) {
@@ -407,7 +418,9 @@ class JellyfinEndpointDiscovery {
throw MediaServerUrlException('Server redirected to an unsupported URL'); throw MediaServerUrlException('Server redirected to an unsupported URL');
} }
if (requestedBaseUri.host.toLowerCase() != effectiveUri.host.toLowerCase()) { if (requestedBaseUri.host.toLowerCase() != effectiveUri.host.toLowerCase()) {
throw MediaServerUrlException('Server redirected to a different host. Enter the final Jellyfin URL directly'); throw MediaServerUrlException(
'Server redirected to a different host. Enter the final ${dialect.productName} URL directly',
);
} }
if (requestedBaseUri.scheme.toLowerCase() == 'https' && effectiveScheme != 'https') { if (requestedBaseUri.scheme.toLowerCase() == 'https' && effectiveScheme != 'https') {
throw MediaServerUrlException('Server redirected from HTTPS to an insecure URL'); throw MediaServerUrlException('Server redirected from HTTPS to an insecure URL');
@@ -415,18 +428,23 @@ class JellyfinEndpointDiscovery {
const publicInfoPath = '/System/Info/Public'; const publicInfoPath = '/System/Info/Public';
if (!effectiveUri.path.endsWith(publicInfoPath)) { if (!effectiveUri.path.endsWith(publicInfoPath)) {
throw MediaServerUrlException('Server redirected to an unsupported URL. Enter the final Jellyfin URL directly'); throw MediaServerUrlException(
'Server redirected to an unsupported URL. Enter the final ${dialect.productName} URL directly',
);
} }
final basePath = effectiveUri.path.substring(0, effectiveUri.path.length - publicInfoPath.length); final basePath = effectiveUri.path.substring(0, effectiveUri.path.length - publicInfoPath.length);
return normalizeBaseUrl(effectiveUri.replace(path: basePath, query: null, fragment: null).toString()); return normalizeBaseUrl(effectiveUri.replace(path: basePath, query: null, fragment: null).toString());
} }
/// Normalizes a concrete Jellyfin base URL without inventing a scheme or port. /// Normalizes a concrete MediaBrowser base URL without inventing a scheme or port.
static String normalizeBaseUrl(String input) => canonicalizeBaseUrl(input); static String normalizeBaseUrl(String input) => canonicalizeBaseUrl(input);
/// Expands a user-typed add/edit form entry into temporary probe candidates. /// Expands a user-typed add/edit form entry into temporary probe candidates.
/// These guesses are for discovery only; failed guesses should not be stored. /// These guesses are for discovery only; failed guesses should not be stored.
static List<String> expandInputToBaseUrls(String input) { static List<String> expandInputToBaseUrls(
String input, {
MediaBrowserDialect dialect = MediaBrowserDialect.jellyfin,
}) {
final trimmed = canonicalizeBaseUrl(input); final trimmed = canonicalizeBaseUrl(input);
if (trimmed.isEmpty) return const []; if (trimmed.isEmpty) return const [];
if (_hasScheme(trimmed)) return [trimmed]; if (_hasScheme(trimmed)) return [trimmed];
@@ -448,7 +466,9 @@ class JellyfinEndpointDiscovery {
} else { } else {
add(parsed.replace(scheme: 'http', port: defaultPort)); add(parsed.replace(scheme: 'http', port: defaultPort));
add(parsed.replace(scheme: 'https')); add(parsed.replace(scheme: 'https'));
add(parsed.replace(scheme: 'https', port: defaultPort)); for (final port in dialect.httpsPortGuesses) {
add(parsed.replace(scheme: 'https', port: port));
}
add(parsed.replace(scheme: 'http')); add(parsed.replace(scheme: 'http'));
} }
return List.unmodifiable(result); return List.unmodifiable(result);
@@ -460,7 +480,10 @@ class JellyfinEndpointDiscovery {
return raw.split(RegExp(r'[\n,]+')).map((url) => url.trim()).where((url) => url.isNotEmpty).toList(growable: false); return raw.split(RegExp(r'[\n,]+')).map((url) => url.trim()).where((url) => url.isNotEmpty).toList(growable: false);
} }
static JellyfinEndpointUserInputCandidates buildUserInputCandidates(Iterable<String> input) { static JellyfinEndpointUserInputCandidates buildUserInputCandidates(
Iterable<String> input, {
MediaBrowserDialect dialect = MediaBrowserDialect.jellyfin,
}) {
final probeBaseUrls = <String>[]; final probeBaseUrls = <String>[];
final explicitBaseUrls = <String>[]; final explicitBaseUrls = <String>[];
final validationBaseUrlGroups = <List<String>>[]; final validationBaseUrlGroups = <List<String>>[];
@@ -488,7 +511,7 @@ class JellyfinEndpointDiscovery {
validationBaseUrlGroups.add([normalized]); validationBaseUrlGroups.add([normalized]);
} else { } else {
final group = <String>[]; final group = <String>[];
for (final candidate in expandInputToBaseUrls(normalized)) { for (final candidate in expandInputToBaseUrls(normalized, dialect: dialect)) {
addProbe(candidate); addProbe(candidate);
group.add(candidate); group.add(candidate);
} }
@@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import '../media/media_browser_dialect.dart';
import '../utils/app_logger.dart'; import '../utils/app_logger.dart';
import '../utils/udp_broadcast_sockets.dart'; import '../utils/udp_broadcast_sockets.dart';
import 'jellyfin_endpoint_discovery.dart'; import 'jellyfin_endpoint_discovery.dart';
@@ -10,17 +11,19 @@ class DiscoveredJellyfinServer {
final String address; final String address;
final String id; final String id;
final String name; final String name;
final MediaBrowserDialect dialect;
DiscoveredJellyfinServer({required this.address, required this.id, required this.name}); DiscoveredJellyfinServer({required this.address, required this.id, required this.name, required this.dialect});
} }
class JellyfinLanDiscoveryService { class JellyfinLanDiscoveryService {
static const int discoveryPort = 7359; static const int discoveryPort = 7359;
static const String discoveryMessage = 'who is JellyfinServer?';
/// Sends two discovery packets 350 ms apart, then listens for /// Sends the selected dialect's discovery packet twice, 350 ms apart, then
/// [responseWindow] after the second packet. /// listens for [responseWindow] after the second packet. Jellyfin and Emby
/// answer only their own payload, while both use the same response shape.
Future<List<DiscoveredJellyfinServer>> discover({ Future<List<DiscoveredJellyfinServer>> discover({
required MediaBrowserDialect dialect,
Duration responseWindow = const Duration(seconds: 2), Duration responseWindow = const Duration(seconds: 2),
InternetAddress? broadcastAddress, InternetAddress? broadcastAddress,
}) async { }) async {
@@ -29,19 +32,19 @@ class JellyfinLanDiscoveryService {
try { try {
socketSet = await UdpBroadcastSockets.bind(); socketSet = await UdpBroadcastSockets.bind();
socketSet.listen((datagram) { socketSet.listen((datagram) {
final server = parseDiscoveryResponse(datagram.data); final server = parseDiscoveryResponse(datagram.data, dialect: dialect);
if (server == null) return; if (server == null) return;
discovered.putIfAbsent(server.id, () => server); discovered.putIfAbsent(server.id, () => server);
}, debugLabel: 'Jellyfin LAN discovery'); }, debugLabel: '${dialect.productName} LAN discovery');
final data = utf8.encode(discoveryMessage); final data = utf8.encode(dialect.lanDiscoveryMessage);
final target = broadcastAddress ?? UdpBroadcastSockets.limitedBroadcastAddress; final target = broadcastAddress ?? UdpBroadcastSockets.limitedBroadcastAddress;
socketSet.send(data, target, discoveryPort); socketSet.send(data, target, discoveryPort);
await Future<void>.delayed(const Duration(milliseconds: 350)); await Future<void>.delayed(const Duration(milliseconds: 350));
socketSet.send(data, target, discoveryPort); socketSet.send(data, target, discoveryPort);
await Future<void>.delayed(responseWindow); await Future<void>.delayed(responseWindow);
} catch (e, st) { } catch (e, st) {
appLogger.w('Jellyfin LAN discovery failed', error: e, stackTrace: st); appLogger.w('${dialect.productName} LAN discovery failed', error: e, stackTrace: st);
} finally { } finally {
await socketSet?.close(); await socketSet?.close();
} }
@@ -56,12 +59,14 @@ class JellyfinLanDiscoveryService {
if (name != 0) return name; if (name != 0) return name;
final address = a.address.compareTo(b.address); final address = a.address.compareTo(b.address);
if (address != 0) return address; if (address != 0) return address;
return a.id.compareTo(b.id); final id = a.id.compareTo(b.id);
if (id != 0) return id;
return a.dialect.id.compareTo(b.dialect.id);
}); });
return List.unmodifiable(sorted); return List.unmodifiable(sorted);
} }
static DiscoveredJellyfinServer? parseDiscoveryResponse(List<int> data) { static DiscoveredJellyfinServer? parseDiscoveryResponse(List<int> data, {required MediaBrowserDialect dialect}) {
try { try {
final decoded = jsonDecode(utf8.decode(data)); final decoded = jsonDecode(utf8.decode(data));
if (decoded is! Map<String, dynamic>) return null; if (decoded is! Map<String, dynamic>) return null;
@@ -73,7 +78,7 @@ class JellyfinLanDiscoveryService {
final normalized = JellyfinEndpointDiscovery.normalizeBaseUrl(address); final normalized = JellyfinEndpointDiscovery.normalizeBaseUrl(address);
if (normalized.isEmpty || id.trim().isEmpty || name.trim().isEmpty) return null; if (normalized.isEmpty || id.trim().isEmpty || name.trim().isEmpty) return null;
return DiscoveredJellyfinServer(address: normalized, id: id.trim(), name: name.trim()); return DiscoveredJellyfinServer(address: normalized, id: id.trim(), name: name.trim(), dialect: dialect);
} catch (_) { } catch (_) {
return null; return null;
} }
+37 -7
View File
@@ -1,4 +1,4 @@
import '../media/media_backend.dart'; import '../media/media_browser_dialect.dart';
import '../media/ids.dart'; import '../media/ids.dart';
import '../media/media_hub.dart'; import '../media/media_hub.dart';
import '../media/media_item.dart'; import '../media/media_item.dart';
@@ -180,17 +180,22 @@ class JellyfinMappers {
return '/Items/${_segment(id)}/Images/$type$indexPart$tagPart'; return '/Items/${_segment(id)}/Images/$type$indexPart$tagPart';
} }
/// Map a Jellyfin `BaseItemDto` (the `Items[]` shape returned by most /// Map a MediaBrowser `BaseItemDto` (the `Items[]` shape returned by most
/// browse endpoints) into a [MediaItem]. Returns `null` when the server /// browse endpoints) into a [MediaItem]. Returns `null` when the server
/// payload is missing `Id` — the mapped item would otherwise carry an /// payload is missing `Id` — the mapped item would otherwise carry an
/// empty-string id that breaks cache keys and image URLs (e.g. /// empty-string id that breaks cache keys and image URLs (e.g.
/// `/Items//Images/Primary`). Callers should filter nulls with /// `/Items//Images/Primary`). Callers should filter nulls with
/// `.whereType<MediaItem>()`. /// `.whereType<MediaItem>()`.
///
/// [dialect] stamps the produced item so downstream UI resolves the right
/// backend badge/label. Jellyfin and Emby DTOs are field-identical, so the
/// mapping itself is shared.
static MediaItem? mediaItem( static MediaItem? mediaItem(
Map<String, dynamic> item, { Map<String, dynamic> item, {
required ServerId serverId, required ServerId serverId,
String? serverName, String? serverName,
required JellyfinImageAbsolutizer? absolutizer, required JellyfinImageAbsolutizer? absolutizer,
MediaBrowserDialect dialect = MediaBrowserDialect.jellyfin,
}) { }) {
final id = item['Id'] as String?; final id = item['Id'] as String?;
if (id == null || id.isEmpty) return null; if (id == null || id.isEmpty) return null;
@@ -212,6 +217,7 @@ class JellyfinMappers {
: <String>[seriesBackdropPath]; : <String>[seriesBackdropPath];
final mapped = JellyfinMediaItem( final mapped = JellyfinMediaItem(
dialect: dialect,
id: id, id: id,
kind: kind, kind: kind,
guid: id, guid: id,
@@ -273,13 +279,13 @@ class JellyfinMappers {
rating: (item['CommunityRating'] as num?)?.toDouble(), rating: (item['CommunityRating'] as num?)?.toDouble(),
ratings: _ratingSources(item, kind), ratings: _ratingSources(item, kind),
isFavorite: _userData(item)?['IsFavorite'] as bool?, isFavorite: _userData(item)?['IsFavorite'] as bool?,
genres: _stringList(item['Genres']), genres: _stringListOrNamePairs(item['Genres'], item['GenreItems']),
directors: _peopleByType(item['People'], 'Director'), directors: _peopleByType(item['People'], 'Director'),
writers: _peopleByType(item['People'], 'Writer'), writers: _peopleByType(item['People'], 'Writer'),
producers: _peopleByType(item['People'], 'Producer'), producers: _peopleByType(item['People'], 'Producer'),
countries: _stringList(item['ProductionLocations']), countries: _stringList(item['ProductionLocations']),
collections: null, collections: null,
labels: _stringList(item['Tags']), labels: _stringListOrNamePairs(item['Tags'], item['TagItems']),
styles: null, styles: null,
moods: null, moods: null,
roles: _actors(item['People']), roles: _actors(item['People']),
@@ -297,18 +303,23 @@ class JellyfinMappers {
return absolutizer == null ? mapped : absolutizer.applyTo(mapped); return absolutizer == null ? mapped : absolutizer.applyTo(mapped);
} }
/// Map a Jellyfin "view" (returned by `/Users/{userId}/Views`) into a /// Map a MediaBrowser "view" (returned by `/Users/{userId}/Views`) into a
/// [MediaLibrary]. The CollectionType field maps onto [MediaKind] roughly. /// [MediaLibrary]. The CollectionType field maps onto [MediaKind] roughly.
/// Returns `null` when the view is missing `Id` — same rationale as /// Returns `null` when the view is missing `Id` — same rationale as
/// [mediaItem]. /// [mediaItem].
static MediaLibrary? library(Map<String, dynamic> view, {required ServerId serverId, String? serverName}) { static MediaLibrary? library(
Map<String, dynamic> view, {
required ServerId serverId,
String? serverName,
MediaBrowserDialect dialect = MediaBrowserDialect.jellyfin,
}) {
final id = view['Id'] as String?; final id = view['Id'] as String?;
if (id == null || id.isEmpty) return null; if (id == null || id.isEmpty) return null;
final collectionType = view['CollectionType'] as String?; final collectionType = view['CollectionType'] as String?;
final type = view['Type'] as String?; final type = view['Type'] as String?;
return MediaLibrary( return MediaLibrary(
id: id, id: id,
backend: MediaBackend.jellyfin, backend: dialect.backend,
title: view['Name'] as String? ?? t.libraries.fallbackTitle, title: view['Name'] as String? ?? t.libraries.fallbackTitle,
kind: _libraryKindFromCollectionType(collectionType, type), kind: _libraryKindFromCollectionType(collectionType, type),
defaultBrowseKinds: _defaultBrowseKindsFromCollectionType(collectionType, type), defaultBrowseKinds: _defaultBrowseKindsFromCollectionType(collectionType, type),
@@ -451,6 +462,25 @@ class JellyfinMappers {
return stringListFromRaw(list); return stringListFromRaw(list);
} }
/// A `Genres`/`Tags` style list, falling back to its `…Items` name-pair
/// sibling when the plain array is absent.
///
/// Emby never returns the plain `Tags` array on an item DTO — only
/// `TagItems` — whatever `Fields` the request asks for (measured on Emby
/// 4.9.5), so reading the plain key alone silently drops every tag.
static List<String>? _stringListOrNamePairs(Object? plain, Object? namePairs) {
final direct = stringListFromRaw(plain);
if (direct != null && direct.isNotEmpty) return direct;
if (namePairs is! List) return direct;
final result = <String>[];
for (final entry in namePairs) {
if (entry is! Map<String, dynamic>) continue;
final name = entry['Name'];
if (name is String && name.trim().isNotEmpty) result.add(name.trim());
}
return nullIfEmptyList(result) ?? direct;
}
static List<String>? _peopleByType(Object? list, String type) { static List<String>? _peopleByType(Object? list, String type) {
if (list is! List) return null; if (list is! List) return null;
final result = <String>[]; final result = <String>[];
+15 -10
View File
@@ -17,14 +17,18 @@ import 'jellyfin_client.dart';
import 'media_list_playback_launcher.dart'; import 'media_list_playback_launcher.dart';
import 'playlist_items_loader.dart'; import 'playlist_items_loader.dart';
/// Backend-neutral launcher for Jellyfin collections, playlists, and folders. /// Backend-neutral launcher for MediaBrowser collections, playlists, and
/// folders.
/// ///
/// Jellyfin has no server-side queue resource — the client fetches /// Jellyfin and Emby have no server-side queue resource — the client fetches
/// children (collection) or playlist items, applies shuffle locally, /// children (collection) or playlist items, applies shuffle locally, and hands
/// and hands the flat list to [PlaybackStateProvider] via /// the flat list to [PlaybackStateProvider] via
/// [PlaybackStateProvider.setPlaybackFromLocalQueue] which the player /// [PlaybackStateProvider.setPlaybackFromLocalQueue], which the player already
/// already consumes (mirrors the path /// consumes (mirrors the path [EpisodeNavigationService] uses for episode
/// [EpisodeNavigationService] uses for episode windows). /// windows).
///
/// The persisted `jellyfin:` queue-id prefix predates Emby support and covers
/// both dialects. It must remain stable so existing saved queues keep working.
class JellyfinSequentialLauncher extends MediaListPlaybackLauncher { class JellyfinSequentialLauncher extends MediaListPlaybackLauncher {
final BuildContext context; final BuildContext context;
@@ -99,9 +103,10 @@ class JellyfinSequentialLauncher extends MediaListPlaybackLauncher {
); );
} }
/// Launch playback from a Jellyfin folder row. Jellyfin has no server-side /// Launch playback from a MediaBrowser folder row. Neither dialect has a
/// queue resource, so folders use the same local queue path as collections. /// server-side queue resource, so folders use the same local queue path as
/// The client query is video-only; music-only folders return [PlayQueueEmpty]. /// collections. The client query is video-only; music-only folders return
/// [PlayQueueEmpty].
@override @override
Future<PlayQueueResult> launchFromFolder({ Future<PlayQueueResult> launchFromFolder({
required MediaItem folder, required MediaItem folder,
+64
View File
@@ -0,0 +1,64 @@
import '../media/media_browser_dialect.dart';
/// Route builders for the endpoints where the Jellyfin and Emby dialects of the
/// MediaBrowser API diverge.
///
/// Jellyfin 10.9 renamed a batch of user-scoped routes to unprefixed forms
/// (`/Users/{uid}/PlayedItems/{id}` → `/UserPlayedItems/{id}`) and introduced
/// `/Users/Me`. Emby only ever shipped the user-scoped spellings and fails on
/// the new ones — `/Users/Me` returns 500 `Unrecognized Guid format` because
/// `Me` is bound as a user id, and the rest 404. Keeping every divergent route
/// here lets the client parts stay dialect-agnostic.
///
/// Routes that are identical on both dialects (`/Items`, `/Users/{uid}/Views`,
/// `/Users/{uid}/Items/{id}`, `/Users/{uid}/Items/Latest`, `/Shows/*`,
/// `/Sessions/Playing*`, `/Items/{id}/PlaybackInfo`, image and stream routes)
/// deliberately do not appear here.
class MediaBrowserPaths {
const MediaBrowserPaths({required this.dialect, required this.userId});
final MediaBrowserDialect dialect;
final String userId;
String get _user => '/Users/${Uri.encodeComponent(userId)}';
static String _id(String itemId) => Uri.encodeComponent(itemId);
/// The authenticated user's own DTO — health probe and user-preference read.
String get currentUser => dialect.requiresUserScopedItemRoutes ? _user : '/Users/Me';
/// Continue Watching / resumable items.
String get resumeItems => dialect.requiresUserScopedItemRoutes ? '$_user/Items/Resume' : '/UserItems/Resume';
/// Played flag write route (`POST` to mark, `DELETE` to unmark).
String playedItem(String itemId) =>
dialect.requiresUserScopedItemRoutes ? '$_user/PlayedItems/${_id(itemId)}' : '/UserPlayedItems/${_id(itemId)}';
/// Favourite flag write route (`POST` to add, `DELETE` to remove).
String favoriteItem(String itemId) => dialect.requiresUserScopedItemRoutes
? '$_user/FavoriteItems/${_id(itemId)}'
: '/UserFavoriteItems/${_id(itemId)}';
/// Thumbs-up/down write route (`POST ?Likes=`, `DELETE` to clear).
String itemRating(String itemId) =>
dialect.requiresUserScopedItemRoutes ? '$_user/Items/${_id(itemId)}/Rating' : '/UserItems/${_id(itemId)}/Rating';
/// Trailers attached to a movie/series.
String localTrailers(String itemId) => dialect.requiresUserScopedItemRoutes
? '$_user/Items/${_id(itemId)}/LocalTrailers'
: '/Items/${_id(itemId)}/LocalTrailers';
/// Extras/behind-the-scenes children.
String specialFeatures(String itemId) => dialect.requiresUserScopedItemRoutes
? '$_user/Items/${_id(itemId)}/SpecialFeatures'
: '/Items/${_id(itemId)}/SpecialFeatures';
/// Hide an item from Continue Watching without touching its playback
/// position (`?Hide=true` to hide, `?Hide=false` to restore).
///
/// Emby-only: Jellyfin 10.11 has no equivalent under either spelling
/// (measured 404 for both `/UserItems/{id}/HideFromResume` and the
/// user-scoped form), which is why [ServerCapabilities.jellyfin] leaves
/// `continueWatchingRemoval` false while [ServerCapabilities.emby] sets it.
String hideFromResume(String itemId) => '$_user/Items/${_id(itemId)}/HideFromResume';
}
+12 -12
View File
@@ -46,10 +46,10 @@ class PlayQueueError extends PlayQueueResult {
/// Backend-neutral playback launcher for collections and playlists. /// Backend-neutral playback launcher for collections and playlists.
/// ///
/// Plex uses server-side `/playQueues` (one round trip, server tracks /// Plex uses server-side `/playQueues` (one round trip, server tracks
/// queue state). Jellyfin has no equivalent — the concrete Jellyfin launcher /// queue state). MediaBrowser servers have no equivalent — the concrete
/// builds an in-memory queue from playable descendants or playlist items. /// [JellyfinSequentialLauncher] builds an in-memory queue from playable
/// [MediaListPlaybackLauncher.forItem] picks the implementation by inspecting /// descendants or playlist items. [MediaListPlaybackLauncher.forItem] picks
/// the item's backend. /// the implementation by inspecting the item's backend.
abstract class MediaListPlaybackLauncher { abstract class MediaListPlaybackLauncher {
/// Launch playback from a collection (a [MediaItem] with /// Launch playback from a collection (a [MediaItem] with
/// `kind == MediaKind.collection`) or a [MediaPlaylist]. /// `kind == MediaKind.collection`) or a [MediaPlaylist].
@@ -57,7 +57,7 @@ abstract class MediaListPlaybackLauncher {
/// [startItem] (optional) starts playback at that item rather than the head /// [startItem] (optional) starts playback at that item rather than the head
/// of the queue — used by the playlist detail screen's "tap an item to /// of the queue — used by the playlist detail screen's "tap an item to
/// start here" interaction. Plex passes it as `key` to `/playQueues`; /// start here" interaction. Plex passes it as `key` to `/playQueues`;
/// Jellyfin rotates the locally-built queue. Ignored when [shuffle] is /// MediaBrowser rotates the locally-built queue. Ignored when [shuffle] is
/// true. /// true.
Future<PlayQueueResult> launchFromCollectionOrPlaylist({ Future<PlayQueueResult> launchFromCollectionOrPlaylist({
required Object item, required Object item,
@@ -67,17 +67,17 @@ abstract class MediaListPlaybackLauncher {
}); });
/// Launch shuffled playback for a show or season. Plex builds a server-side /// Launch shuffled playback for a show or season. Plex builds a server-side
/// `/playQueues` with `shuffle=1`; Jellyfin fetches the full episode list /// `/playQueues` with `shuffle=1`; MediaBrowser fetches the full episode
/// via `fetchClientSideEpisodeQueue`, shuffles locally, and publishes /// list via `fetchClientSideEpisodeQueue`, shuffles locally, and publishes
/// through `setPlaybackFromLocalQueue` (same path as the sequential /// through `setPlaybackFromLocalQueue` (same path as the sequential queue
/// queue from `EpisodeNavigationService`). /// from `EpisodeNavigationService`).
Future<PlayQueueResult> launchShuffledShow({required MediaItem metadata, bool showLoadingIndicator = true}); Future<PlayQueueResult> launchShuffledShow({required MediaItem metadata, bool showLoadingIndicator = true});
/// Launch playback from a folder row of the library tree. Everything each /// Launch playback from a folder row of the library tree. Everything each
/// backend needs is stamped onto [folder]: Plex builds a server-side /// backend needs is stamped onto [folder]: Plex builds a server-side
/// `/playQueues` from [MediaItem.backendFolderKey] (returning a /// `/playQueues` from [MediaItem.backendFolderKey] (returning a
/// [PlayQueueError] when the row carries none), Jellyfin fetches the /// [PlayQueueError] when the row carries none), while MediaBrowser fetches
/// folder's playable descendants and publishes a local queue. /// the folder's playable descendants and publishes a local queue.
Future<PlayQueueResult> launchFromFolder({ Future<PlayQueueResult> launchFromFolder({
required MediaItem folder, required MediaItem folder,
required bool shuffle, required bool shuffle,
@@ -88,7 +88,7 @@ abstract class MediaListPlaybackLauncher {
/// [MediaItem.backend] / [MediaPlaylist.backend]. /// [MediaItem.backend] / [MediaPlaylist.backend].
static MediaListPlaybackLauncher forItem(BuildContext context, Object item) { static MediaListPlaybackLauncher forItem(BuildContext context, Object item) {
final backend = _backendOf(item); final backend = _backendOf(item);
if (backend == MediaBackend.jellyfin) { if (backend.usesMediaBrowserApi) {
return JellyfinSequentialLauncher(context: context); return JellyfinSequentialLauncher(context: context);
} }
return PlexPlayQueueLauncher.forContext(context, item); return PlexPlayQueueLauncher.forContext(context, item);
+52 -40
View File
@@ -43,7 +43,7 @@ bool _isMediaServerAuthFailure(Object error) =>
/// The internal map and public accessors are typed against the /// The internal map and public accessors are typed against the
/// [MediaServerClient] interface so consumers don't depend on the concrete /// [MediaServerClient] interface so consumers don't depend on the concrete
/// backend. Onboarding helpers branch on backend (Plex `PlexServer`, /// backend. Onboarding helpers branch on backend (Plex `PlexServer`,
/// Jellyfin `JellyfinConnection`) and instantiate the matching client. /// MediaBrowser `JellyfinConnection`) and instantiate the matching client.
class MultiServerManager { class MultiServerManager {
MultiServerManager({ MultiServerManager({
PlexClientFactory plexClientFactory = PlexClient.create, PlexClientFactory plexClientFactory = PlexClient.create,
@@ -126,16 +126,18 @@ class MultiServerManager {
} }
/// Whether [compoundId] is still the client bound as the active user for /// Whether [compoundId] is still the client bound as the active user for
/// [machineId]. Async Jellyfin work must re-check this before publishing a /// [machineId]. Async MediaBrowser work must re-check this before publishing
/// result — a profile switch can rebind the machine mid-probe. /// a result — a profile switch can rebind the machine mid-probe.
bool _isActiveJellyfin(String machineId, String compoundId) => _activeJellyfinMachine[machineId] == compoundId; bool _isActiveJellyfin(String machineId, String compoundId) => _activeJellyfinMachine[machineId] == compoundId;
/// All Jellyfin clients ever added, keyed by the compound connection id /// All MediaBrowser clients ever added, keyed by the compound connection id
/// (`{serverMachineId}/{userId}`). Lets two users on the same Jellyfin /// (`{serverMachineId}/{userId}`). This lets users and dialects coexist
/// server coexist — adding the second user's client won't tear down the /// without tearing down another connection's in-flight operations. [_clients]
/// first user's in-flight operations. [_clients] holds the currently /// holds the currently "active" entry per machineId for consumers that pass
/// "active" entry per machineId for everyone-pass-machineId-as-serverId /// the public machine id as the server id.
/// consumers (cache resolver, visibility filter, MediaItem.serverId). ///
/// These private members retain their Jellyfin-era names because one
/// [JellyfinClient] implements both the Jellyfin and Emby dialects.
final Map<String, JellyfinClient> _jellyfinByCompoundId = {}; final Map<String, JellyfinClient> _jellyfinByCompoundId = {};
final Map<String, String> _activeJellyfinMachine = {}; final Map<String, String> _activeJellyfinMachine = {};
final Map<String, HealthStatus> _jellyfinHealthByCompoundId = {}; final Map<String, HealthStatus> _jellyfinHealthByCompoundId = {};
@@ -159,15 +161,15 @@ class MultiServerManager {
/// Debounce timer for connectivity events — collapses rapid network flapping /// Debounce timer for connectivity events — collapses rapid network flapping
Timer? _connectivityDebounce; Timer? _connectivityDebounce;
/// Get all registered server IDs (Plex + Jellyfin). /// Get all registered server IDs (Plex + MediaBrowser).
/// ///
/// Sourced from [_clients] rather than [_plexServers] because /// Sourced from [_clients] rather than [_plexServers] because
/// [_plexServers] only holds the Plex-specific [PlexServer] structs /// [_plexServers] only holds the Plex-specific [PlexServer] structs
/// (host/port metadata used for connection-racing). Jellyfin connections /// (host/port metadata used for connection-racing). MediaBrowser connections
/// are registered as clients only — falling back to [_plexServers] would /// are registered as clients only — falling back to [_plexServers] would
/// silently exclude them and callers (the active-profile binder, library /// silently exclude them and callers (the active-profile binder, library
/// refresh gates) would behave as if the manager were empty for /// refresh gates) would behave as if the manager were empty for
/// Jellyfin-only profiles. /// MediaBrowser-only profiles.
List<String> get serverIds => _clients.keys.toList(); List<String> get serverIds => _clients.keys.toList();
List<String> get onlineServerIds => _serverStatus.entries.where((e) => e.value).map((e) => e.key).toList(); List<String> get onlineServerIds => _serverStatus.entries.where((e) => e.value).map((e) => e.key).toList();
@@ -212,10 +214,9 @@ class MultiServerManager {
return getClient(serverId); return getClient(serverId);
} }
/// Get the [PlexClient] for a server, or `null` if the server is Jellyfin /// Get the [PlexClient] for a server, or `null` if the server uses the
/// (or not registered). Use for Plex-only flows (Live TV, server prefs, /// MediaBrowser API (or is not registered). Use for Plex-only flows that
/// endpoint optimization) that don't yet have a backend-neutral /// don't yet have a backend-neutral equivalent on [MediaServerClient].
/// equivalent on [MediaServerClient].
PlexClient? getPlexClient(ServerId serverId) { PlexClient? getPlexClient(ServerId serverId) {
final client = _clients[serverId]; final client = _clients[serverId];
return client is PlexClient ? client : null; return client is PlexClient ? client : null;
@@ -607,23 +608,23 @@ class MultiServerManager {
return bound; return bound;
} }
/// Add a Jellyfin server backed by an authenticated [JellyfinConnection]. /// Add a MediaBrowser server backed by an authenticated
/// Returns true on success. /// [JellyfinConnection]. Returns true on success.
/// ///
/// When a live client already exists for the same compound id and the /// When a live client already exists for the same compound id and the
/// connection is equivalent (see [canReuseJellyfinClient]), that client is /// connection is equivalent (see [canReuseJellyfinClient]), that client is
/// reused instead of recreated — profile rebinds re-add unchanged /// reused instead of recreated — profile rebinds re-add unchanged
/// connections routinely, and tearing the client down would abort its /// connections routinely, and tearing the client down would abort its
/// in-flight requests. A material change (token, deviceId, URL set) still /// in-flight requests. A material change (dialect, token, deviceId, URL set)
/// replaces the client. This mirrors the Plex rebind path, where /// still replaces the client. This mirrors the Plex rebind path, where
/// [refreshTokensForProfile] reuses the online client via an in-place /// [refreshTokensForProfile] reuses the online client via an in-place
/// token update. /// token update.
/// ///
/// Jellyfin clients use the shared endpoint-racing flow when multiple URLs /// MediaBrowser clients use the shared endpoint-racing flow when multiple
/// are configured, then instantiate the client against the lowest-latency /// URLs are configured, then instantiate the client against the
/// reachable URL. /// lowest-latency reachable URL.
/// ///
/// Two users on the same Jellyfin server are tracked separately in /// Two users on the same MediaBrowser server are tracked separately in
/// [_jellyfinByCompoundId]; only one is "active" per machineId at a time. /// [_jellyfinByCompoundId]; only one is "active" per machineId at a time.
/// Adding the second user's connection doesn't close the first user's /// Adding the second user's connection doesn't close the first user's
/// client (preserves any in-flight operations on the prior profile). /// client (preserves any in-flight operations on the prior profile).
@@ -640,7 +641,7 @@ class MultiServerManager {
var endpointSelectionValidated = false; var endpointSelectionValidated = false;
if (connection.baseUrls.length > 1) { if (connection.baseUrls.length > 1) {
try { try {
final endpoint = await JellyfinEndpointDiscovery().raceEndpoints( final endpoint = await JellyfinEndpointDiscovery(dialect: connection.dialect).raceEndpoints(
connection.baseUrls, connection.baseUrls,
preferredUrl: connection.baseUrl, preferredUrl: connection.baseUrl,
expectedMachineId: connection.serverMachineId, expectedMachineId: connection.serverMachineId,
@@ -657,7 +658,7 @@ class MultiServerManager {
endpointSelectionValidated = true; endpointSelectionValidated = true;
} catch (e, st) { } catch (e, st) {
appLogger.w( appLogger.w(
'Jellyfin endpoint race failed; using only the stored active endpoint', '${connection.dialect.productName} endpoint race failed; using only the stored active endpoint',
error: e.runtimeType, error: e.runtimeType,
stackTrace: st, stackTrace: st,
); );
@@ -709,13 +710,20 @@ class MultiServerManager {
_jellyfinHealthByCompoundId[compoundId] = health; _jellyfinHealthByCompoundId[compoundId] = health;
_applyHealth(ServerId(machineId), health); _applyHealth(ServerId(machineId), health);
appLogger.i('Added Jellyfin server: ${resolvedConnection.serverName}${healthy ? '' : ' (unhealthy)'}'); appLogger.i(
'Added ${resolvedConnection.dialect.productName} server: '
'${resolvedConnection.serverName}${healthy ? '' : ' (unhealthy)'}',
);
if (_connectivitySubscription == null && healthy) { if (_connectivitySubscription == null && healthy) {
_startNetworkMonitoring(); _startNetworkMonitoring();
} }
return healthy; return healthy;
} catch (e, stackTrace) { } catch (e, stackTrace) {
appLogger.e('Failed to add Jellyfin server ${connection.serverName}', error: e, stackTrace: stackTrace); appLogger.e(
'Failed to add ${connection.dialect.productName} server ${connection.serverName}',
error: e,
stackTrace: stackTrace,
);
return false; return false;
} }
} }
@@ -723,6 +731,7 @@ class MultiServerManager {
/// Whether the live client bound to [live] can serve [incoming] without /// Whether the live client bound to [live] can serve [incoming] without
/// being recreated. Recreation is required when a field baked into the /// being recreated. Recreation is required when a field baked into the
/// client at construction time changes: /// client at construction time changes:
/// - `dialect` controls route and capability behavior;
/// - `accessToken` / `deviceId` are embedded in the auth headers when the /// - `accessToken` / `deviceId` are embedded in the auth headers when the
/// HTTP client is built; /// HTTP client is built;
/// - `baseUrls` fixes the failover candidate set. Compared as a set: both /// - `baseUrls` fixes the failover candidate set. Compared as a set: both
@@ -736,7 +745,8 @@ class MultiServerManager {
/// precedes this check. /// precedes this check.
@visibleForTesting @visibleForTesting
static bool canReuseJellyfinClient({required JellyfinConnection live, required JellyfinConnection incoming}) { static bool canReuseJellyfinClient({required JellyfinConnection live, required JellyfinConnection incoming}) {
return live.accessToken == incoming.accessToken && return live.dialect == incoming.dialect &&
live.accessToken == incoming.accessToken &&
live.deviceId == incoming.deviceId && live.deviceId == incoming.deviceId &&
setEquals(live.baseUrls.toSet(), incoming.baseUrls.toSet()); setEquals(live.baseUrls.toSet(), incoming.baseUrls.toSet());
} }
@@ -857,8 +867,8 @@ class MultiServerManager {
} }
/// Test connection health for all servers. The probe is backend-defined: /// Test connection health for all servers. The probe is backend-defined:
/// Plex hits `/identity` (HTTP 200), Jellyfin hits `/Users/Me` (auth-required) /// Plex hits `/identity`; MediaBrowser uses the dialect's current-user route.
/// so a server with a revoked token is correctly reported as offline. /// Both are auth-required so a revoked token is reported as offline.
Future<void> checkServerHealth() async { Future<void> checkServerHealth() async {
// Coalesce concurrent calls — return the in-flight future if one exists // Coalesce concurrent calls — return the in-flight future if one exists
if (_activeHealthCheck != null) return _activeHealthCheck!; if (_activeHealthCheck != null) return _activeHealthCheck!;
@@ -1104,17 +1114,19 @@ class MultiServerManager {
} }
} }
/// Attempt reconnection for a single offline Jellyfin server. /// Attempt reconnection for a single offline MediaBrowser server.
/// ///
/// Jellyfin has a single fixed base URL — there's no connection-racing to /// Reuse the existing [JellyfinClient], whose request-level failover owns its
/// run, just a health round-trip. The existing [JellyfinClient] is reused /// endpoint set, and perform an authenticated health round-trip. On success,
/// (the access token persists in [JellyfinConnection]); on success we flip /// flip the machine slot back to online so MediaServer-aware UI un-greys the
/// the machine slot back to online so MediaServer-aware UI un-greys the
/// entry. /// entry.
Future<void> _reconnectJellyfinServer(String machineId, JellyfinClient client) async { Future<void> _reconnectJellyfinServer(String machineId, JellyfinClient client) async {
final expectedCompoundId = client.connection.id; final expectedCompoundId = client.connection.id;
try { try {
appLogger.d('Attempting reconnection for Jellyfin server ${client.connection.serverName}'); appLogger.d(
'Attempting reconnection for ${client.connection.dialect.productName} server '
'${client.connection.serverName}',
);
final status = await client.checkHealth(); final status = await client.checkHealth();
_jellyfinHealthByCompoundId[expectedCompoundId] = status; _jellyfinHealthByCompoundId[expectedCompoundId] = status;
if (!_isActiveJellyfin(machineId, expectedCompoundId)) { if (!_isActiveJellyfin(machineId, expectedCompoundId)) {
@@ -1211,9 +1223,9 @@ class MultiServerManager {
}); });
} }
/// Fire-and-forget safe: both backends' `checkHealth` catch every failure /// Fire-and-forget safe: both concrete clients' `checkHealth` implementations
/// and fold it into a [HealthStatus], and the scheduled reconnection guards /// catch every failure and fold it into a [HealthStatus], and the scheduled
/// its own errors — this future must never complete with one. /// reconnection guards its own errors — this future must never complete with one.
Future<void> _verifyServerEndpointsExhausted(ServerId serverId) async { Future<void> _verifyServerEndpointsExhausted(ServerId serverId) async {
final client = _clients[serverId]; final client = _clients[serverId];
if (client == null || !_endpointHealthChecks.add(serverId)) return; if (client == null || !_endpointHealthChecks.add(serverId)) return;

Some files were not shown because too many files have changed in this diff Show More