Restructure step 4: local library and media identity
- 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:
1 parent
4d00b7455e
commit
bf9fc193b3
44 files changed
+4350
-891
No files matched your search
@@ -0,0 +1,80 @@
|
||||
import 'package:sync_relay_core/core.dart';
|
||||
|
||||
// `media.ref` (PROTOCOL.md "Media identity") for each source: what a media
|
||||
// is, independent of where its bytes come from. Local files get theirs
|
||||
// from the library (`ShowMatcher.refFor`, L1 + L2); these are the other
|
||||
// two: Jellyfin items by their provider ids, links best effort from their
|
||||
// name.
|
||||
|
||||
/// A Jellyfin episode, movie or video: kind, series (or movie) title,
|
||||
/// season and episode as Jellyfin numbers them, and the AniList / TVDB ids
|
||||
/// its metadata plugins set ([series]' when given: an episode's own ids
|
||||
/// are the episode's, not the show's). Null for anything that is not
|
||||
/// played (series, seasons, folders).
|
||||
MediaRef? jellyfinMediaRef(JellyfinItem item, {JellyfinItem? series}) {
|
||||
final kind = switch (item.kind) {
|
||||
JellyfinItemKind.episode => 'episode',
|
||||
JellyfinItemKind.movie || JellyfinItemKind.video => 'movie',
|
||||
_ => null,
|
||||
};
|
||||
if (kind == null) return null;
|
||||
final episode = kind == 'episode';
|
||||
// An episode's Tvdb id names the episode; the show's comes from the
|
||||
// series item. AniList ids are only ever set on shows and movies.
|
||||
final owner = episode ? series : item;
|
||||
final special = episode && item.parentIndexNumber == 0;
|
||||
return MediaRef(
|
||||
kind: special ? 'extra' : kind,
|
||||
title: episode ? (item.seriesName ?? series?.name ?? item.name) : item.name,
|
||||
anilistId: owner?.providerId('AniList'),
|
||||
tvdbId: owner?.providerId('Tvdb'),
|
||||
season: episode && !special ? item.parentIndexNumber : null,
|
||||
ep: episode && !special ? item.indexNumber : null,
|
||||
);
|
||||
}
|
||||
|
||||
final _sxe = RegExp(r'S(\d{1,3})\s?E(\d{1,4})$', caseSensitive: false);
|
||||
|
||||
/// A picked Jellyfin item from its card's lines alone ([title], and
|
||||
/// [subtitle] "Series · S1E6" for episodes, the year for movies): kind,
|
||||
/// title and numbers, no ids.
|
||||
MediaRef jellyfinPickRef(String title, String subtitle) {
|
||||
final sub = subtitle.trim();
|
||||
final cut = sub.lastIndexOf(' · ');
|
||||
final label = cut < 0 ? sub : sub.substring(cut + 3).trim();
|
||||
final m = _sxe.firstMatch(label);
|
||||
if (m == null) return MediaRef(kind: 'movie', title: title);
|
||||
final season = int.parse(m.group(1)!);
|
||||
return MediaRef(
|
||||
kind: season == 0 ? 'extra' : 'episode',
|
||||
title: cut < 0 ? title : sub.substring(0, cut).trim(),
|
||||
season: season == 0 ? null : season,
|
||||
ep: season == 0 ? null : int.parse(m.group(2)!),
|
||||
);
|
||||
}
|
||||
|
||||
/// A link, best effort: its name read like a release name (`Show - 05`
|
||||
/// is an episode, `OP1` an extra); anything else a movie of that title.
|
||||
MediaRef linkMediaRef(String name) {
|
||||
final p = parseReleaseName(name);
|
||||
final title = p.series.trim().isEmpty ? name.trim() : p.series.trim();
|
||||
return switch (p.kind) {
|
||||
ReleaseKind.episode => MediaRef(
|
||||
kind: 'episode',
|
||||
title: title,
|
||||
season: p.season,
|
||||
ep: p.season == null ? null : p.episode,
|
||||
absolute: p.season == null ? p.episode : null),
|
||||
ReleaseKind.extra => MediaRef(kind: 'extra', title: title, season: p.season),
|
||||
ReleaseKind.movie || ReleaseKind.other => MediaRef(kind: 'movie', title: title),
|
||||
};
|
||||
}
|
||||
|
||||
/// The episode line a ref gives (`S3 E13`, `E108`), or null (movies;
|
||||
/// extras keep the name's own marker).
|
||||
String? refEpisodeLine(MediaRef ref) {
|
||||
if (ref.kind != 'episode') return null;
|
||||
if (ref.season != null && ref.ep != null) return 'S${ref.season} E${ref.ep}';
|
||||
final n = ref.absolute ?? ref.ep ?? ref.anilistEp;
|
||||
return n == null ? null : 'E$n';
|
||||
}
|
||||
Reference in new issue
Block a user