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 = () => {