//! 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 /// A channel *container* the user drills into (Jellyfin `Channel`). Channel, Folder, /// A live TV channel — playable, but a live stream with no seekable /// timeline (no resume/seek). Jellyfin `TvChannel`/`LiveTvChannel`. LiveChannel, /// A playable leaf inside a channel (Jellyfin `ChannelFolderItem` that is /// not itself a folder) — e.g. a plugin-channel VOD item that has no /// dedicated item type but carries its own media streams. Playable and /// seekable, unlike `LiveChannel`. Distinct from `Channel` (the container) /// and from `Other` so the UI can route it to playback. ChannelItem, /// 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, } /// The kind of a media stream within an item (audio track, video track, /// subtitle, …) — provider-neutral, replacing the stringly Jellyfin stream type. #[derive(specta::Type, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] #[serde(rename_all = "camelCase")] pub enum StreamKind { Audio, Video, Subtitle, /// Any stream kind we do not model explicitly (e.g. embedded image, data). #[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 ) } }