feat(library): add media library browsing functionality

- Introduced new `library` module in the API routes to handle media library requests.
- Enhanced `AppState` to include a media provider for library interactions.
- Defined new `IMediaProvider` trait methods for listing collections, series, and genres.
- Implemented Jellyfin media provider methods for fetching collections and series.
- Added frontend components for selecting series and displaying filter previews.
- Created hooks for fetching collections, series, and genres from the library.
- Updated media filter to support series name and search term.
- Enhanced API client to handle new library-related endpoints.
This commit is contained in:
2026-03-12 02:54:30 +01:00
parent f069376136
commit bf07a65dcd
14 changed files with 1005 additions and 86 deletions

View File

@@ -13,7 +13,7 @@ pub mod value_objects;
// Re-export commonly used types
pub use entities::*;
pub use errors::{DomainError, DomainResult};
pub use ports::IMediaProvider;
pub use ports::{Collection, IMediaProvider, SeriesSummary};
pub use repositories::*;
pub use services::{ChannelService, ScheduleEngineService, UserService};
pub use value_objects::*;

View File

@@ -6,15 +6,56 @@
//! these traits for each concrete source.
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use crate::entities::{MediaItem};
use crate::errors::DomainResult;
use crate::value_objects::{MediaFilter, MediaItemId};
use crate::entities::MediaItem;
use crate::errors::{DomainError, DomainResult};
use crate::value_objects::{ContentType, MediaFilter, MediaItemId};
// ============================================================================
// Library browsing types
// ============================================================================
/// A top-level media collection / library exposed by a provider.
///
/// In Jellyfin this maps to a virtual library (Movies, TV Shows, …).
/// In Plex it maps to a section. The `id` is provider-specific and is used
/// as the value for `MediaFilter::collections`.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Collection {
pub id: String,
pub name: String,
/// Provider-specific type hint, e.g. "movies", "tvshows". `None` when the
/// provider does not expose this information.
pub collection_type: Option<String>,
}
/// Lightweight summary of a TV series available in the provider's library.
/// Returned by `IMediaProvider::list_series` for the dashboard browser.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SeriesSummary {
/// Provider-specific series ID (opaque — used for ParentId filtering).
pub id: String,
pub name: String,
/// Total number of episodes across all seasons, if the provider exposes it.
pub episode_count: u32,
pub genres: Vec<String>,
pub year: Option<u16>,
}
// ============================================================================
// Port trait
// ============================================================================
/// Port for reading media content from an external provider.
///
/// Implementations live in the infra layer. One adapter per provider type
/// (e.g. `JellyfinMediaProvider`, `PlexMediaProvider`, `LocalFileProvider`).
///
/// The three browsing methods (`list_collections`, `list_series`, `list_genres`)
/// have default implementations that return an `InfrastructureError`. Adapters
/// that support library browsing override them; those that don't (e.g. the
/// `NoopMediaProvider`) inherit the default and return a clear error.
#[async_trait]
pub trait IMediaProvider: Send + Sync {
/// Fetch metadata for all items matching `filter` from this provider.
@@ -36,4 +77,38 @@ pub trait IMediaProvider: Send + Sync {
/// URLs are intentionally *not* stored in the schedule because they may be
/// short-lived (signed URLs, session tokens) or depend on client context.
async fn get_stream_url(&self, item_id: &MediaItemId) -> DomainResult<String>;
/// List top-level collections (libraries/sections) available in this provider.
///
/// Used by the dashboard to populate the collections picker so users don't
/// need to know provider-internal IDs.
async fn list_collections(&self) -> DomainResult<Vec<Collection>> {
Err(DomainError::InfrastructureError(
"list_collections is not supported by this provider".into(),
))
}
/// List TV series available in an optional collection.
///
/// `collection_id` corresponds to `Collection::id` returned by
/// `list_collections`. Pass `None` to search across all libraries.
async fn list_series(&self, collection_id: Option<&str>) -> DomainResult<Vec<SeriesSummary>> {
let _ = collection_id;
Err(DomainError::InfrastructureError(
"list_series is not supported by this provider".into(),
))
}
/// List all genres available for a given content type.
///
/// Pass `None` to return genres across all content types.
async fn list_genres(
&self,
content_type: Option<&ContentType>,
) -> DomainResult<Vec<String>> {
let _ = content_type;
Err(DomainError::InfrastructureError(
"list_genres is not supported by this provider".into(),
))
}
}

View File

@@ -606,6 +606,12 @@ pub struct MediaFilter {
/// Abstract groupings interpreted by each provider (Jellyfin library, Plex section,
/// filesystem path, etc.). An empty list means "all available content".
pub collections: Vec<String>,
/// Filter by TV series name. Use with `content_type: Episode` and
/// `strategy: Sequential` for ordered series playback (e.g. "iCarly").
pub series_name: Option<String>,
/// Free-text search term. Intended for library browsing; typically omitted
/// during schedule generation.
pub search_term: Option<String>,
}
/// How the scheduling engine fills a time block with selected media items.