feat(library): genre sliders, artist links, and navigation utils
🏗️ Build and Test JellyTau / Run Tests (pull_request) Successful in 3m49s
Traceability Validation / Check Requirement Traces (pull_request) Successful in 19s
🏗️ Build and Test JellyTau / Build Android APK (pull_request) Successful in 18m24s

- music landing: diverse per-genre album sliders (online counts /
  offline wide-probe fallback) and home-screen library shortcuts
- add ArtistLinks component and shared navigation/genreDiversity utils
- player/playback-mode refinements across Rust and frontend
This commit is contained in:
2026-06-25 19:18:06 +02:00
parent 62874564ff
commit 1836615dc0
38 changed files with 1009 additions and 207 deletions
+1 -1
View File
@@ -864,7 +864,7 @@ impl MediaRepository for OfflineRepository {
let genres = genre_set
.into_iter()
.map(|name| Genre { id: name.clone(), name })
.map(|name| Genre { id: name.clone(), name, album_count: None })
.collect();
Ok(genres)
+25 -3
View File
@@ -811,7 +811,12 @@ impl MediaRepository for OnlineRepository {
}
async fn get_genres(&self, parent_id: Option<&str>) -> Result<Vec<Genre>, RepoError> {
let mut endpoint = format!("/Genres?UserId={}", self.user_id);
// Ask Jellyfin to scope counts to albums and include them, so the
// frontend can rank genres by popularity without probing each one.
let mut endpoint = format!(
"/Genres?UserId={}&IncludeItemTypes=MusicAlbum&Recursive=true&Fields=ItemCounts",
self.user_id
);
if let Some(pid) = parent_id {
endpoint.push_str(&format!("&ParentId={}", pid));
@@ -828,17 +833,34 @@ impl MediaRepository for OnlineRepository {
struct JellyfinGenre {
id: String,
name: String,
// Which count field Jellyfin populates for a genre under
// Fields=ItemCounts varies by server/version: scoped queries may
// fill AlbumCount, others only ChildCount. Read whichever is
// present so ranking still works. Absent on servers that ignore
// Fields=ItemCounts entirely, so all stay optional.
album_count: Option<u32>,
child_count: Option<u32>,
}
let response: GenresResponse = self.get_json(&endpoint).await?;
Ok(response
let genres: Vec<Genre> = response
.items
.into_iter()
.map(|g| Genre {
id: g.id,
name: g.name,
album_count: g.album_count.or(g.child_count),
})
.collect())
.collect();
let with_counts = genres.iter().filter(|g| g.album_count.is_some()).count();
log::debug!(
"get_genres: {} genres, {} carry counts (AlbumCount/ChildCount)",
genres.len(),
with_counts
);
Ok(genres)
}
async fn search(
+4
View File
@@ -255,6 +255,10 @@ pub struct PlaybackInfo {
pub struct Genre {
pub id: String,
pub name: String,
/// Number of albums tagged with this genre, when the backend can supply it
/// (online only). Lets the frontend rank/pick genres without probing each
/// one. `None` when unknown (e.g. offline).
pub album_count: Option<u32>,
}
/// Image type