From 3289ce0fbc22e38d67d81412035888b35edf5a3c Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Sun, 22 Feb 2026 04:34:36 +0100 Subject: [PATCH] feat: fallback skip markers from chapter titles --- lib/models/plex_media_info.dart | 47 +++++++++++++++++++ lib/services/plex_client.dart | 2 +- .../video_controls/video_controls.dart | 2 +- 3 files changed, 49 insertions(+), 2 deletions(-) diff --git a/lib/models/plex_media_info.dart b/lib/models/plex_media_info.dart index 70907874..5b130173 100644 --- a/lib/models/plex_media_info.dart +++ b/lib/models/plex_media_info.dart @@ -183,4 +183,51 @@ class PlaybackExtras { final List markers; PlaybackExtras({required this.chapters, required this.markers}); + + static final _introPattern = RegExp( + r'(?:^|\b)(?:intro(?:duction)?|opening)(?:\b|$)|^op(?:\s?\d+)?$', + caseSensitive: false, + ); + static final _creditsPattern = RegExp( + r'(?:^|\b)(?:outro|closing|credits?|ending)(?:\b|$)|^ed(?:\s?\d+)?$', + caseSensitive: false, + ); + + static String? _classifyChapterTitle(String title) { + if (_introPattern.hasMatch(title)) return 'intro'; + if (_creditsPattern.hasMatch(title)) return 'credits'; + return null; + } + + /// Returns [PlaybackExtras] using real markers when available, otherwise + /// synthesises markers from chapter titles matching intro/credits patterns. + factory PlaybackExtras.withChapterFallback({ + required List chapters, + required List markers, + }) { + if (markers.isNotEmpty) { + return PlaybackExtras(chapters: chapters, markers: markers); + } + + final synthetic = []; + for (var i = 0; i < chapters.length; i++) { + final ch = chapters[i]; + final title = ch.title; + if (title == null || title.isEmpty) continue; + + final type = _classifyChapterTitle(title); + if (type == null) continue; + + final start = ch.startTimeOffset; + if (start == null) continue; + + final end = ch.endTimeOffset ?? + (i + 1 < chapters.length ? chapters[i + 1].startTimeOffset : null); + if (end == null) continue; + + synthetic.add(PlexMarker(id: ch.id, type: type, startTimeOffset: start, endTimeOffset: end)); + } + + return PlaybackExtras(chapters: chapters, markers: synthetic); + } } diff --git a/lib/services/plex_client.dart b/lib/services/plex_client.dart index cdf33a56..60dc92a0 100644 --- a/lib/services/plex_client.dart +++ b/lib/services/plex_client.dart @@ -997,7 +997,7 @@ class PlexClient { } } - return PlaybackExtras(chapters: chapters, markers: markers); + return PlaybackExtras.withChapterFallback(chapters: chapters, markers: markers); } /// Get consolidated video playback data (URL, media info, versions, and markers) in a single API call. diff --git a/lib/widgets/video_controls/video_controls.dart b/lib/widgets/video_controls/video_controls.dart index c12ba842..be260fbb 100644 --- a/lib/widgets/video_controls/video_controls.dart +++ b/lib/widgets/video_controls/video_controls.dart @@ -885,7 +885,7 @@ class _PlexVideoControlsState extends State with WindowListen } } - return PlaybackExtras(chapters: chapters, markers: markers); + return PlaybackExtras.withChapterFallback(chapters: chapters, markers: markers); } Widget _buildTrackChapterControlsWidget() {