From 6595c146a3e37858b3a8bbc03c7859d61a90a018 Mon Sep 17 00:00:00 2001 From: Frank Stellmacher <171614930+Psychotoxical@users.noreply.github.com> Date: Sun, 17 May 2026 15:29:29 +0200 Subject: [PATCH] feat(album): show OpenSubsonic disc subtitles after the CD heading (#753) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(album): show OpenSubsonic disc subtitles after the CD heading Multi-disc albums in OpenSubsonic / Navidrome carry a per-disc subtitle (`discTitles`) — e.g. "Sessions" on CD 3 of a deluxe edition. AlbumTrackList only rendered "CD N" and dropped the subtitle, so users couldn't tell two discs apart unless they read the track names. * `SubsonicAlbum.discTitles` typed; `getAlbum` forwards it as-is. * `AlbumTrackList` and `AlbumTrackListMobile` take a discTitleByNum map and render the subtitle in the disc separator after "CD N". * Heading bumped slightly (13 → 15 px, icon 16 → 18 px) so the disc separator stays legible next to the new subtitle. * docs(changelog): note disc subtitles after CD heading (#753) --- CHANGELOG.md | 6 ++++++ src/api/subsonicTypes.ts | 7 +++++++ src/components/AlbumTrackList.tsx | 10 ++++++++++ .../albumTrackList/AlbumTrackListMobile.tsx | 5 +++++ src/pages/AlbumDetail.tsx | 1 + src/styles/components/tracklist.css | 16 ++++++++++++++-- 6 files changed, 43 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 71b54820..de2c9a04 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -216,6 +216,12 @@ Foundational work: faster reviews, narrower diffs, and a safety net under the pa * The Queue side panel's ETA label and the sleep-timer preview both go through **`formatClockTime`**, which just delegates to **`toLocaleTimeString`** — on en-US that meant AM/PM with no in-app override. **Settings → System → App Behavior** now exposes a tri-state **Clock Format** select: **`Auto`** (default — keeps existing locale-driven behaviour, so first launch after the update is a no-op for everyone), **`24h`**, and **`12h`**, the explicit values forcing **`hour12`** everywhere `formatClockTime` is used. Wired through `authStore` (`clockFormat` / `setClockFormat`) and consumed by both surfaces; all nine bundled locales ship the four new strings. +### Album page — OpenSubsonic disc subtitles after the CD heading + +**By [@Psychotoxical](https://github.com/Psychotoxical), thanks to zunoz for the report on the Psysonic Discord, PR [#753](https://github.com/Psychotoxical/psysonic/pull/753)** + +* Multi-disc albums in OpenSubsonic / Navidrome can carry a per-disc subtitle (`discTitles`, e.g. **"Sessions"** on CD 3 of a deluxe edition). The album tracklist previously dropped it and only showed **`CD N`**, so adjacent discs of a reissue read the same in the header. The disc separator now renders **`CD N — Subtitle`** in both desktop and mobile track lists, and the heading is bumped slightly so the subtitle stays legible next to the disc number. + ## Changed ### Backend — Cargo workspace with 5 domain crates (Rust refactor) diff --git a/src/api/subsonicTypes.ts b/src/api/subsonicTypes.ts index e0b83c4a..1c6f5b82 100644 --- a/src/api/subsonicTypes.ts +++ b/src/api/subsonicTypes.ts @@ -22,6 +22,13 @@ export interface SubsonicAlbum { releaseTypes?: string[]; /** OpenSubsonic: album-level credits (Navidrome may attach on album and/or child songs). */ albumArtists?: SubsonicOpenArtistRef[]; + /** OpenSubsonic: per-disc subtitles (e.g. "Sessions" on CD 3 of a deluxe edition). */ + discTitles?: SubsonicDiscTitle[]; +} + +export interface SubsonicDiscTitle { + disc: number; + title: string; } /** OpenSubsonic `artists` / `albumArtists` entries on a child song (may include `userRating`). */ diff --git a/src/components/AlbumTrackList.tsx b/src/components/AlbumTrackList.tsx index e5fefe7f..0048c7e8 100644 --- a/src/components/AlbumTrackList.tsx +++ b/src/components/AlbumTrackList.tsx @@ -20,6 +20,8 @@ export type { SortKey } from '../utils/componentHelpers/albumTrackListHelpers'; interface AlbumTrackListProps { songs: SubsonicSong[]; + /** Per-disc subtitles from the album payload, rendered after "CD N". */ + discTitles?: { disc: number; title: string }[]; sorted?: boolean; hasVariousArtists: boolean; currentTrack: Track | null; @@ -42,6 +44,7 @@ interface AlbumTrackListProps { export default function AlbumTrackList({ songs, + discTitles, sorted, hasVariousArtists: _hasVariousArtists, currentTrack, @@ -90,6 +93,9 @@ export default function AlbumTrackList({ } const discNums = sorted ? [1] : Array.from(discs.keys()).sort((a, b) => a - b); const isMultiDisc = !sorted && discNums.length > 1; + const discTitleByNum = new Map( + (discTitles ?? []).filter(d => d.title?.trim()).map(d => [d.disc, d.title.trim()]), + ); const currentTrackId = currentTrack?.id ?? null; @@ -98,6 +104,7 @@ export default function AlbumTrackList({ 💿 CD {discNum} + {discTitleByNum.get(discNum) && ( + {discTitleByNum.get(discNum)} + )} )} {discs.get(discNum)!.map(song => { diff --git a/src/components/albumTrackList/AlbumTrackListMobile.tsx b/src/components/albumTrackList/AlbumTrackListMobile.tsx index 81159b6f..9aac94dc 100644 --- a/src/components/albumTrackList/AlbumTrackListMobile.tsx +++ b/src/components/albumTrackList/AlbumTrackListMobile.tsx @@ -8,6 +8,7 @@ import { formatLongDuration } from '../../utils/format/formatDuration'; interface Props { discNums: number[]; discs: Map; + discTitleByNum: Map; isMultiDisc: boolean; currentTrackId: string | null; isPlaying: boolean; @@ -31,6 +32,7 @@ interface Props { export function AlbumTrackListMobile({ discNums, discs, + discTitleByNum, isMultiDisc, currentTrackId, isPlaying, @@ -46,6 +48,9 @@ export function AlbumTrackListMobile({ {isMultiDisc && (
💿 CD {discNum} + {discTitleByNum.get(discNum) && ( + {discTitleByNum.get(discNum)} + )}
)} {discs.get(discNum)!.map(song => { diff --git a/src/pages/AlbumDetail.tsx b/src/pages/AlbumDetail.tsx index 1386dd34..97e4a168 100644 --- a/src/pages/AlbumDetail.tsx +++ b/src/pages/AlbumDetail.tsx @@ -341,6 +341,7 @@ const handleShuffleAll = () => {