A Jellypod podcast listed its episodes alphabetically. The store pinned SortBy=SortName onto every drill-down, which overrode the order the channel plugin returns — and since Jellypod prefixes played episodes with "[Played]", the name sort also clumped every heard episode at the top. Which order a container's children take is domain knowledge, so it moves to Rust: the caller names the container (GetItemsOptions.parentKind) and default_listing_sort answers with the sort. A channel folder is PremiereDate descending, every other container keeps SortName ascending, and a caller naming no container still gets no SortBy, so the paths that rely on the server's own order keep it. An explicit sort always wins. ChannelFolderItem with is_folder now maps to MediaKind::ChannelFolder instead of collapsing into Folder — while both were Folder there was nothing to key the rule on. The offline leg of the cache/server race applies the same order, so the cached list no longer flashes in name order before the server's arrives. TRACES: UR-007 | DR-257 | UT-229, UT-230, UT-231
100 lines
3.9 KiB
Rust
100 lines
3.9 KiB
Rust
//! 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 *container* inside a channel — a Jellyfin `ChannelFolderItem` that is
|
|
/// itself a folder, e.g. one podcast within a podcast channel. Distinct
|
|
/// from `Folder` because its children are plugin content with an order of
|
|
/// their own (newest episode first), which a folder's name order silently
|
|
/// overrode.
|
|
///
|
|
/// TRACES: UR-007 | DR-257
|
|
ChannelFolder,
|
|
/// 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
|
|
)
|
|
}
|
|
}
|