domain: introduce provider-neutral media model (phase 1)

Establish src-tauri/src/domain/ as the single source of truth for the
media model, with all Jellyfin translation isolated in from_jellyfin.rs.
Adds MediaKind enum and neutral duration_ms/image_id fields to MediaItem
as additive, defaulted dual-carry alongside the legacy Jellyfin-named
fields, so nothing breaks while the frontend migrates off them.

- domain/media.rs: canonical MediaKind (closed enum, replaces stringly
  item_type), Default = Other so unknown/defaulted items are inert.
- domain/from_jellyfin.rs: total, panic-free item_type -> MediaKind
  classification (all audited types + person subroles) and ticks->ms.
- MediaItem gains kind/duration_ms/image_id, populated at both mapping
  seams (online to_media_item, offline cached_item_to_media_item) and
  the synthesized-album/person sites.
- Regenerated bindings.ts: frontend now HAS the neutral model available.

Phase 1 of docs/specs/frontend-domain-model.md. No frontend behaviour
change yet; wire shape is a superset of before.

Rust 456 tests, frontend 644 tests, check + check:boundary all green.
This commit is contained in:
2026-07-23 20:53:47 +02:00
parent f89b241ad6
commit 55fa26377a
11 changed files with 448 additions and 27 deletions
+68
View File
@@ -0,0 +1,68 @@
//! Canonical, provider-neutral media domain model.
//!
//! This is the *single source of truth* for what a media item is across the
//! whole app. Rust (repositories, player, downloads) uses these types directly;
//! the frontend consumes the tauri-specta-generated projection in
//! `src/lib/api/bindings.ts`. There is no second hand-written copy in either
//! language, so the model cannot drift.
//!
//! No provider (Jellyfin) vocabulary belongs in this file. Translation from a
//! provider's wire shape lives beside it in `from_jellyfin.rs` and is the only
//! place provider terms touch the domain type.
//!
//! Spec: docs/specs/frontend-domain-model.md
use serde::{Deserialize, Serialize};
/// The kind of a media item — provider-neutral classification.
///
/// Replaces the stringly-typed `item_type` that carried Jellyfin's vocabulary
/// (`"Audio"`, `"MusicAlbum"`, …) across the boundary. A closed enum means a
/// typo or an unhandled kind is a compile error on the frontend, not a silent
/// runtime miss across ~127 comparison sites.
#[derive(specta::Type, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub enum MediaKind {
// Music
Track,
Album,
Artist,
Playlist,
// Video
Movie,
Series,
Season,
Episode,
// Cast/crew
Person,
// Containers / live TV
Channel,
Folder,
/// A kind we do not model explicitly. Reached only for provider item types
/// that map to nothing meaningful; consumers treat it like an opaque
/// container. The mapping must be *total* — it never panics — so this is the
/// safe sink for unknown strings. Also the `Default`, so a defaulted
/// `MediaItem` (see the dual-carry migration) is inert rather than a lie.
#[default]
Other,
}
impl MediaKind {
/// True for kinds that are containers/collections rather than playable leaves.
/// Presentation-neutral helper the backend can use for e.g. drill-vs-play.
// Consumed by later migration phases (drill-vs-play routing); kept now so the
// domain surface is complete alongside the type it describes.
#[allow(dead_code)]
pub fn is_container(self) -> bool {
matches!(
self,
MediaKind::Album
| MediaKind::Artist
| MediaKind::Series
| MediaKind::Season
| MediaKind::Playlist
| MediaKind::Channel
| MediaKind::Folder
)
}
}