feat: update media filter to support multiple series names and enhance library item fetching

This commit is contained in:
2026-03-12 03:12:59 +01:00
parent bf07a65dcd
commit f028b1be98
10 changed files with 173 additions and 93 deletions

View File

@@ -50,14 +50,13 @@ impl JellyfinMediaProvider {
}
}
#[async_trait]
impl IMediaProvider for JellyfinMediaProvider {
/// Fetch items matching `filter` from the Jellyfin library.
///
/// `MediaFilter.collections` maps to Jellyfin `ParentId` (library/folder UUID).
/// Multiple collections are not supported in a single call; the first entry wins.
/// Decades are mapped to Jellyfin's `MinYear`/`MaxYear`.
async fn fetch_items(&self, filter: &MediaFilter) -> DomainResult<Vec<MediaItem>> {
impl JellyfinMediaProvider {
/// Inner fetch: applies all filter fields plus an optional series name override.
async fn fetch_items_for_series(
&self,
filter: &MediaFilter,
series_name: Option<&str>,
) -> DomainResult<Vec<MediaItem>> {
let url = format!(
"{}/Users/{}/Items",
self.config.base_url, self.config.user_id
@@ -97,8 +96,12 @@ impl IMediaProvider for JellyfinMediaProvider {
params.push(("ParentId", parent_id.clone()));
}
if let Some(series_name) = &filter.series_name {
params.push(("SeriesName", series_name.clone()));
if let Some(name) = series_name {
params.push(("SeriesName", name.to_string()));
// Return episodes in chronological order when a specific series is
// requested — season first, then episode within the season.
params.push(("SortBy", "ParentIndexNumber,IndexNumber".into()));
params.push(("SortOrder", "Ascending".into()));
}
if let Some(q) = &filter.search_term {
@@ -127,7 +130,52 @@ impl IMediaProvider for JellyfinMediaProvider {
DomainError::InfrastructureError(format!("Failed to parse Jellyfin response: {}", e))
})?;
Ok(body.items.into_iter().filter_map(map_jellyfin_item).collect())
// Jellyfin's SeriesName query param is not a strict filter — it can
// bleed items from other shows. Post-filter in Rust to guarantee that
// only the requested series is returned.
let items = body.items.into_iter().filter_map(map_jellyfin_item);
let items: Vec<MediaItem> = if let Some(name) = series_name {
items
.filter(|item| {
item.series_name
.as_deref()
.map(|s| s.eq_ignore_ascii_case(name))
.unwrap_or(false)
})
.collect()
} else {
items.collect()
};
Ok(items)
}
}
#[async_trait]
impl IMediaProvider for JellyfinMediaProvider {
/// Fetch items matching `filter` from the Jellyfin library.
///
/// When `series_names` has more than one entry the results from each series
/// are fetched sequentially and concatenated (Jellyfin only supports one
/// `SeriesName` param per request).
async fn fetch_items(&self, filter: &MediaFilter) -> DomainResult<Vec<MediaItem>> {
match filter.series_names.len() {
0 | 1 => {
let series = filter.series_names.first().map(String::as_str);
self.fetch_items_for_series(filter, series).await
}
_ => {
let mut all = Vec::new();
for series_name in &filter.series_names {
let items = self
.fetch_items_for_series(filter, Some(series_name.as_str()))
.await?;
all.extend(items);
}
Ok(all)
}
}
}
/// Fetch a single item by its opaque ID.