Files
sync_replay_project/apps/desktop/lib/src/library/media_refs.dart
T
foxyzandClaude Opus 5.5 bf9fc193b3
ci / core (push) Successful in 29s
ci / server (push) Successful in 1m8s
ci / updater-windows (push) Failing after 6m2s
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>
2026-10-15 23:34:39 +03:00

81 lines
3.4 KiB
Dart

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';
}