From 24fc137e4f06341d3438ff748fb772805959ad76 Mon Sep 17 00:00:00 2001 From: foxyz Date: Fri, 9 Oct 2026 17:23:34 +0300 Subject: [PATCH] No buffering rings at the end of short videos; links never guess an AniList show Co-Authored-By: Claude Opus 5.5 --- apps/desktop/lib/src/session/media_art.dart | 3 ++- .../lib/src/session/session_coordinator.dart | 3 +++ apps/desktop/test/schedule_anilist_test.dart | 2 ++ docs/ARCHITECTURE.md | 4 ++-- packages/core/lib/src/protocol.dart | 9 +++++++-- packages/core/test/core_test.dart | 17 +++++++++++++++++ 6 files changed, 33 insertions(+), 5 deletions(-) diff --git a/apps/desktop/lib/src/session/media_art.dart b/apps/desktop/lib/src/session/media_art.dart index de535ef..ea123ed 100644 --- a/apps/desktop/lib/src/session/media_art.dart +++ b/apps/desktop/lib/src/session/media_art.dart @@ -51,7 +51,8 @@ Future jellyfinArt(JellyfinClient client, String itemId, Future aniListArt(AniListClient aniList, String name, {Future Function(Uri url, Map headers, int maxBytes) fetch = fetchImage}) async { final series = parseReleaseName(name).series.trim(); - if (series.isEmpty) return null; + // "1", "2026-10-09": nothing a show could be called. + if (!RegExp(r'\p{L}', unicode: true).hasMatch(series)) return null; final match = await aniList.searchMedia(series); if (match == null) return null; Future get(String? url, int maxBytes) async { diff --git a/apps/desktop/lib/src/session/session_coordinator.dart b/apps/desktop/lib/src/session/session_coordinator.dart index 6a55781..44b398d 100644 --- a/apps/desktop/lib/src/session/session_coordinator.dart +++ b/apps/desktop/lib/src/session/session_coordinator.dart @@ -1059,6 +1059,9 @@ class SessionCoordinator { final ArtBytes? bytes; if (ref != null && client != null) { bytes = await jellyfinArt(client, ref.itemId); + } else if (ref == null && (media.url ?? '').isNotEmpty) { + // A web link: its name is a URL tail ("1"), not a show; never guess. + bytes = null; } else { final name = ref == null ? media.name : jellyfinTitle(ref.title, ref.subtitle).$1; bytes = await aniListArt(_aniList ??= AniListClient(), name); diff --git a/apps/desktop/test/schedule_anilist_test.dart b/apps/desktop/test/schedule_anilist_test.dart index fb3a076..1526c88 100644 --- a/apps/desktop/test/schedule_anilist_test.dart +++ b/apps/desktop/test/schedule_anilist_test.dart @@ -208,6 +208,8 @@ void main() { expect(art.banner, jpeg); expect(asked, unorderedEquals(['https://img/xl.jpg', 'https://img/banner.jpg', 'https://img/l.jpg'])); expect(await aniListArt(c, 'Nothing Like It.mkv', fetch: (_, _, _) async => jpeg), isNull); + // A link tail like ".../video/1" names no show: no AniList search at all. + expect(await aniListArt(c, '1', fetch: (_, _, _) async => jpeg), isNull); c.close(); }); diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index b67066a..961389c 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -662,8 +662,8 @@ always scrolls the page). Space never presses a focused button the controls: `playerStatusFor` (pure) picks the most pressing of removed, reconnecting (Cancel), a warn/error health message (Retry), connecting (only after 2 s), finished, someone loading (below - `StateBroadcast.bufferNeed`, the gate's own need: less near the end, - none at finished; their avatar with the progress round it), you not + `StateBroadcast.bufferNeed`, the gate's own need at the current + position: less near the end, none at finished; their avatar with the progress round it), you not ready (a click marks you ready) and who everyone waits for (avatars, "2/3 ready"); nothing otherwise, info never. 34 px avatars, a main line and a muted detail line. Frosted glass, colour only for problems; it fades through diff --git a/packages/core/lib/src/protocol.dart b/packages/core/lib/src/protocol.dart index 38e4fdf..47c1f00 100644 --- a/packages/core/lib/src/protocol.dart +++ b/packages/core/lib/src/protocol.dart @@ -559,12 +559,17 @@ class StateBroadcast { /// Buffer an in-set member needs before playback can go on, as the /// server's gate counts it: [RelayConstants.minBufferSecs], less near the /// end (mpv cannot buffer past it), none once finished. - double get bufferNeed { + double get bufferNeed => bufferNeedAt(DateTime.now().toUtc()); + + /// [bufferNeed] at [now]: while playing the position has moved on since + /// [serverTime] (the local clock is close enough for this). + double bufferNeedAt(DateTime now) { if (transport == 'finished') return 0; final d = (media?['duration'] as num?)?.toDouble() ?? 0; const full = RelayConstants.minBufferSecs; if (d <= 0) return full; - return (d - position - RelayConstants.endSlackSecs).clamp(0.0, full).toDouble(); + final moved = transport == 'playing' ? now.difference(serverTime).inMilliseconds / 1000 : 0; + return (d - (position + moved) - RelayConstants.endSlackSecs).clamp(0.0, full).toDouble(); } final int eventId; diff --git a/packages/core/test/core_test.dart b/packages/core/test/core_test.dart index 02f2ec2..59fa22e 100644 --- a/packages/core/test/core_test.dart +++ b/packages/core/test/core_test.dart @@ -193,4 +193,21 @@ void main() { expect((r.duration, r.mediaId), (0.0, 0)); }); }); + + test('bufferNeed counts the time played since the last event', () { + final t0 = DateTime.utc(2026, 1, 1); + final s = StateBroadcast( + eventId: 1, + transport: 'playing', + position: 0, + serverTime: t0, + mediaId: 1, + media: const {'name': 'clip', 'duration': 6.0}, + members: const [], + ); + expect(s.bufferNeedAt(t0), 4.0); + // 3 s in: 3 s left, minus the 2 s end slack. + expect(s.bufferNeedAt(t0.add(const Duration(seconds: 3))), closeTo(1.0, 1e-9)); + expect(s.bufferNeedAt(t0.add(const Duration(seconds: 5))), 0); + }); }