Skip to main content

#
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}