Restructure step 4: local library and media identity
ci / core (push) Successful in 29s
ci / server (push) Successful in 1m8s
ci / updater-windows (push) Failing after 6m2s

- L1 (core, offline): release names with kinds (episode, movie, extra:
  OP/ED/Opening/Ending/NC/PV/OVA…), folder hints (season and name from
  parentheticals, ranges), one show across folders, gaps and next with
  skipped numbers, junk ignored, stable keys.
- L2 (app, device-owned cache): AniList match with synonyms and
  sequel walking, episode mapping across cours (AnimeFlow runs when
  signed in), art and episode titles, "Wrong show?" pins per show or
  folder, episode offsets; lazy, spaced requests, works offline.
- Files tab as a library: poster cards, show pages with banner, gap
  prompt, extras, movies, Wrong show? and Episode numbers.
- media.ref produced for local files, Jellyfin (provider ids) and links,
  carried in swaps and Up next; receivers name media from it.
- Hosting relays each Jellyfin item through its own server's login.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
foxyzandClaude Opus 5.5 committed 2026-10-15 23:34:39 +03:00
1 parent 4d00b7455e
commit bf9fc193b3
44 files changed
+4350 -891

No files matched your search

+26 -19
View File
@@ -130,7 +130,7 @@ class SyncServer {
mountArtRoute(_app, hub);
mountSessionLogRoutes(_app, hub, () => _sessionLog);
mountJellyfinRelayRoute(
_app, sessions, () => hub.jellyfin, () => _relayClient ??= HttpClient(),
_app, sessions, hub.loginFor, () => _relayClient ??= HttpClient(),
next: () => hub.nextJellyfinRef, onLog: logRequest);
// advertiseHost is only an explicit override; otherwise echo what the client dialled.
String authority(String dialledHost) => config.advertiseHost.isNotEmpty
@@ -193,32 +193,39 @@ class SyncServer {
}
/// A configured credential without a ServerId gets it from
/// `/System/Info/Public` (unauthenticated). On failure the relay stays off.
/// `/System/Info/Public` (unauthenticated). On failure that login stays
/// unused.
Future<void> _resolveJellyfinServerId() async {
final cred = hub.jellyfin;
if (cred == null) return;
if (cred.serverId.isEmpty) {
final client = JellyfinClient(baseUrl: cred.baseUrl, clientInfo: cred.clientInfo);
try {
final info = await client.publicInfo().timeout(const Duration(seconds: 10));
hub.jellyfin = cred.withServerId(info.id);
} catch (e) {
_log.warning('jellyfin relay off: server id lookup failed: $e');
return;
} finally {
client.close();
final logins = <JellyfinCredential>[];
for (final cred in hub.jellyfinLogins) {
var c = cred;
if (c.serverId.isEmpty) {
final client = JellyfinClient(baseUrl: c.baseUrl, clientInfo: c.clientInfo);
try {
final info = await client.publicInfo().timeout(const Duration(seconds: 10));
c = c.withServerId(info.id);
} catch (e) {
_log.warning('jellyfin relay off for ${c.baseUrl}: server id lookup failed: $e');
} finally {
client.close();
}
}
if (c.serverId.isNotEmpty && !logins.any((l) => l.serverId == c.serverId)) {
logins.add(c);
_log.info('jellyfin relay on for server ${c.serverId}');
}
}
_log.info('jellyfin relay on for server ${hub.jellyfin!.serverId}');
hub.jellyfinLogins = logins;
}
/// Opens the running server to the network: a second listener on
/// every IPv4 interface at [port] (0: a free one), the password
/// [secret] ('' : open) and the Jellyfin relay login [jellyfin]. Members
/// [secret] ('' : open) and the Jellyfin relay logins [jellyfin] (one
/// per Jellyfin server; null keeps the ones it has). Members
/// already in (on the first listener) stay connected; nothing restarts.
/// Shared already: the listener moves to [port] when it differs. Returns
/// the bound port; throws when it cannot bind.
Future<int> share({required int port, required String secret, JellyfinCredential? jellyfin}) async {
Future<int> share({required int port, required String secret, List<JellyfinCredential>? jellyfin}) async {
if (_server == null) throw StateError('the server is not running');
final was = _shared;
if (was != null && (port == 0 || port == was.port)) {
@@ -247,9 +254,9 @@ class SyncServer {
return listener.port;
}
void _applyShareSettings(String secret, JellyfinCredential? jellyfin) {
void _applyShareSettings(String secret, List<JellyfinCredential>? jellyfin) {
session.secret = secret;
if (jellyfin != null) hub.jellyfin = jellyfin;
if (jellyfin != null) hub.jellyfinLogins = [...jellyfin];
}
/// Closes the network listener [share] opened: everyone but the host