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:
@@ -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 |
@@ -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? ?? '',
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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'];
|
||||||
|
|||||||
@@ -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).
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 n’a pas pu être sélectionné',
|
'videoControls.subtitleDownloadedNotApplied' => 'Le sous-titre a été téléchargé, mais n’a 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 d’accès peut empêcher le fonctionnement de Regarder ensemble.',
|
'watchTogether.relayUnreachable' => 'Serveur relais inaccessible. Un blocage par le fournisseur d’accè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 l’hôte regarde actuellement',
|
'watchTogether.joinCurrentPlaybackDescription' => 'Reprendre le contenu que l’hô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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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();
|
||||||
|
|||||||
@@ -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,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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._();
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|||||||
@@ -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) ...[
|
||||||
|
|||||||
@@ -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)),
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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
@@ -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!;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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?>[];
|
||||||
|
|||||||
@@ -459,18 +459,19 @@ 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) {
|
||||||
|
final persisted = await JellyfinCacheResolver(_database).findProfileScopeId(serverId, activeProfileId);
|
||||||
|
return persisted ?? activeClientScopeIdForServer(serverId);
|
||||||
}
|
}
|
||||||
if (backend != MediaBackend.jellyfin) return null;
|
return buildPlexProfileScopeId(serverId: serverId, profileId: activeProfileId);
|
||||||
final persisted = await JellyfinCacheResolver(_database).findProfileScopeId(serverId, activeProfileId);
|
|
||||||
return persisted ?? activeClientScopeIdForServer(serverId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Bulk-load pinned metadata. Profile-visible hydration reads only exact
|
/// Bulk-load pinned metadata. Profile-visible hydration reads only exact
|
||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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('"')) {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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(',')}';
|
||||||
|
}
|
||||||
|
|
||||||
|
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 Jellyfin server.
|
/// [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;
|
||||||
|
|||||||
@@ -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,23 +1904,31 @@ mixin _JellyfinBrowseMethods on _JellyfinClientInternals {
|
|||||||
abort: abort,
|
abort: abort,
|
||||||
);
|
);
|
||||||
case 'nextup':
|
case 'nextup':
|
||||||
return _safeFetchMediaPage(
|
final nextUpQuery = <String, dynamic>{
|
||||||
'/Shows/NextUp',
|
'userId': connection.userId,
|
||||||
{
|
'StartIndex': offset.toString(),
|
||||||
'userId': connection.userId,
|
'Limit': effectiveLimit,
|
||||||
'StartIndex': offset.toString(),
|
'Fields': _hubRowFields,
|
||||||
'Limit': effectiveLimit,
|
'ParentId': ?parentId,
|
||||||
'Fields': _hubRowFields,
|
'EnableResumable': 'false',
|
||||||
'ParentId': ?parentId,
|
'NextUpDateCutoff': _nextUpDateCutoff(),
|
||||||
'EnableResumable': 'false',
|
'EnableTotalRecordCount': 'true',
|
||||||
'NextUpDateCutoff': _nextUpDateCutoff(),
|
...jellyfinImageQueryParameters,
|
||||||
'EnableTotalRecordCount': 'true',
|
};
|
||||||
...jellyfinImageQueryParameters,
|
if (dialect.supportsGlobalNextUp) {
|
||||||
},
|
return _safeFetchMediaPage(
|
||||||
offset: offset,
|
'/Shows/NextUp',
|
||||||
requestedSize: pageSize,
|
nextUpQuery,
|
||||||
abort: abort,
|
offset: offset,
|
||||||
);
|
requestedSize: pageSize,
|
||||||
|
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.
|
||||||
|
|||||||
@@ -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});
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>[];
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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';
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
Reference in New Issue
Block a user