jellytau_lib/domain/media.rs
1//! Canonical, provider-neutral media domain model.
2//!
3//! This is the *single source of truth* for what a media item is across the
4//! whole app. Rust (repositories, player, downloads) uses these types directly;
5//! the frontend consumes the tauri-specta-generated projection in
6//! `src/lib/api/bindings.ts`. There is no second hand-written copy in either
7//! language, so the model cannot drift.
8//!
9//! No provider (Jellyfin) vocabulary belongs in this file. Translation from a
10//! provider's wire shape lives beside it in `from_jellyfin.rs` and is the only
11//! place provider terms touch the domain type.
12//!
13//! Spec: docs/specs/frontend-domain-model.md
14
15use serde::{Deserialize, Serialize};
16
17/// The kind of a media item — provider-neutral classification.
18///
19/// Replaces the stringly-typed `item_type` that carried Jellyfin's vocabulary
20/// (`"Audio"`, `"MusicAlbum"`, …) across the boundary. A closed enum means a
21/// typo or an unhandled kind is a compile error on the frontend, not a silent
22/// runtime miss across ~127 comparison sites.
23#[derive(specta::Type, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
24#[serde(rename_all = "camelCase")]
25pub enum MediaKind {
26 // Music
27 Track,
28 Album,
29 Artist,
30 Playlist,
31 // Video
32 Movie,
33 Series,
34 Season,
35 Episode,
36 // Cast/crew
37 Person,
38 // Containers / live TV
39 /// A channel *container* the user drills into (Jellyfin `Channel`).
40 Channel,
41 Folder,
42 /// A live TV channel — playable, but a live stream with no seekable
43 /// timeline (no resume/seek). Jellyfin `TvChannel`/`LiveTvChannel`.
44 LiveChannel,
45 /// A playable leaf inside a channel (Jellyfin `ChannelFolderItem` that is
46 /// not itself a folder) — e.g. a plugin-channel VOD item that has no
47 /// dedicated item type but carries its own media streams. Playable and
48 /// seekable, unlike `LiveChannel`. Distinct from `Channel` (the container)
49 /// and from `Other` so the UI can route it to playback.
50 ChannelItem,
51 /// A *container* inside a channel — a Jellyfin `ChannelFolderItem` that is
52 /// itself a folder, e.g. one podcast within a podcast channel. Distinct
53 /// from `Folder` because its children are plugin content with an order of
54 /// their own (newest episode first), which a folder's name order silently
55 /// overrode.
56 ///
57 /// TRACES: UR-007 | DR-257
58 ChannelFolder,
59 /// A kind we do not model explicitly. Reached only for provider item types
60 /// that map to nothing meaningful; consumers treat it like an opaque
61 /// container. The mapping must be *total* — it never panics — so this is the
62 /// safe sink for unknown strings. Also the `Default`, so a defaulted
63 /// `MediaItem` (see the dual-carry migration) is inert rather than a lie.
64 #[default]
65 Other,
66}
67
68/// The kind of a media stream within an item (audio track, video track,
69/// subtitle, …) — provider-neutral, replacing the stringly Jellyfin stream type.
70#[derive(specta::Type, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
71#[serde(rename_all = "camelCase")]
72pub enum StreamKind {
73 Audio,
74 Video,
75 Subtitle,
76 /// Any stream kind we do not model explicitly (e.g. embedded image, data).
77 #[default]
78 Other,
79}
80
81impl MediaKind {
82 /// True for kinds that are containers/collections rather than playable leaves.
83 /// Presentation-neutral helper the backend can use for e.g. drill-vs-play.
84 // Consumed by later migration phases (drill-vs-play routing); kept now so the
85 // domain surface is complete alongside the type it describes.
86 #[allow(dead_code)]
87 pub fn is_container(self) -> bool {
88 matches!(
89 self,
90 MediaKind::Album
91 | MediaKind::Artist
92 | MediaKind::Series
93 | MediaKind::Season
94 | MediaKind::Playlist
95 | MediaKind::Channel
96 | MediaKind::Folder
97 )
98 }
99}