Compare commits

..

2 Commits

Author SHA1 Message Date
Psychotoxical 05f7ae0611 docs(changelog): add multi-disc separator cover entry (#1336) 2026-07-23 03:45:02 +02:00
Psychotoxical b0b7ff6a92 feat(album): show disc cover in the multi-disc separator
Replace the generic CD glyph next to "CD N" with the disc's cover. The cover is
resolved from the disc's own first track (`albumCoverRefForSong` with per-disc
resolution forced), so a release with distinct per-disc artwork shows each disc's
own cover; single-cover albums reuse the shared album art. Falls back to a
placeholder when no cover is available. Applies to the desktop and mobile track
lists; a new DiscHeaderCover composes existing cover-pipeline helpers only.
2026-07-23 03:43:12 +02:00
5 changed files with 65 additions and 4 deletions
+6
View File
@@ -43,6 +43,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* **Settings → System → Logging** and **PsyLab → Logs** offer basic and verbose debug-depth levels while Debug logging is enabled. Verbose mode adds structured multi-server scope, reachability, music-folder and New Releases diagnostics, with credentials and sensitive URL data redacted.
### Album details — disc covers in the multi-disc separator
**By [@Psychotoxical](https://github.com/Psychotoxical), PR [#1336](https://github.com/Psychotoxical/psysonic/pull/1336)**
* Multi-disc albums show each disc's cover next to "CD N" instead of a generic disc icon. Releases with distinct per-disc artwork show each disc's own cover; other albums fall back to the shared album art.
## Changed
### Library index — designed for several live servers at once
@@ -15,6 +15,7 @@ import { TrackRow } from '@/features/album/components/TrackRow';
import { AlbumTrackListMobile } from '@/features/album/components/AlbumTrackListMobile';
import { TracklistColumnPicker } from '@/ui/TracklistColumnPicker';
import { TracklistHeaderRow } from '@/features/album/components/TracklistHeaderRow';
import { DiscHeaderCover } from '@/features/album/components/DiscHeaderCover';
import { offlineActionPolicy, type OfflineActionPolicy } from '@/features/offline';
import { ownedEntityKey, ownedOverrideValue } from '@/lib/util/ownedEntityKey';
@@ -167,7 +168,7 @@ export default function AlbumTrackList({
<div key={discNum}>
{isMultiDisc && (
<div className="disc-header">
<span className="disc-icon">💿</span>
<DiscHeaderCover song={discs.get(discNum)![0]} />
CD {discNum}
{discTitleByNum.get(discNum) && (
<span className="disc-subtitle">{discTitleByNum.get(discNum)}</span>
@@ -6,6 +6,7 @@ import { songToTrack } from '@/lib/media/songToTrack';
import { formatLongDuration } from '@/lib/format/formatDuration';
import { ownedEntityKey } from '@/lib/util/ownedEntityKey';
import { sameQueueTrack } from '@/features/playback';
import { DiscHeaderCover } from '@/features/album/components/DiscHeaderCover';
interface Props {
discNums: number[];
@@ -49,7 +50,7 @@ export function AlbumTrackListMobile({
<div key={discNum}>
{isMultiDisc && (
<div className="disc-header">
<span className="disc-icon">💿</span> CD {discNum}
<DiscHeaderCover song={discs.get(discNum)![0]} /> CD {discNum}
{discTitleByNum.get(discNum) && (
<span className="disc-subtitle">{discTitleByNum.get(discNum)}</span>
)}
@@ -0,0 +1,36 @@
import type { SubsonicSong } from '@/lib/api/subsonicTypes';
import { CoverArtImage } from '@/cover/CoverArtImage';
import { albumCoverRefForSong } from '@/cover/ref';
import { coverServerScopeForServerId } from '@/cover/serverScope';
type DiscHeaderSong = Pick<SubsonicSong, 'id' | 'albumId' | 'coverArt' | 'discNumber' | 'serverId'>;
/**
* Cover shown next to a multi-disc separator ("CD N"), resolved from the disc's
* own first track rather than album-scoped.
*
* A disc's cover is that track's own art (`song.coverArt`). Servers surface
* embedded per-file art as per-track `mf-*` ids, and the album-scoped heuristic
* (`album_has_distinct_disc_covers`) deliberately rejects per-track ids to avoid a
* per-song cache explosion — which routes every disc to the shared album slot, so
* discs with genuinely different embedded art collide on the first disc's cover.
* Forcing per-disc resolution here is safe: the separator renders at most one
* cover per disc, so a dedicated per-track cache slot cannot explode. Genuine
* per-disc `dc-*` art resolves the same way; single-cover albums simply reuse the
* same bytes under a per-disc slot.
*/
export function DiscHeaderCover({ song }: { song: DiscHeaderSong }) {
const coverRef = albumCoverRefForSong(song, true, coverServerScopeForServerId(song.serverId));
if (!coverRef) return null;
return (
<CoverArtImage
coverRef={coverRef}
displayCssPx={40}
surface="dense"
alt=""
loading="lazy"
decoding="async"
className="track-row-cover-thumb"
/>
);
}
+19 -2
View File
@@ -626,8 +626,25 @@ html[data-track-previews-randommix="off"] [data-preview-loc="randomMix"] .pl
margin-top: 0;
}
.disc-icon {
font-size: 18px;
/* Album / per-disc cover in the multi-disc separator (replaces the old CD glyph).
Sized here because `.track-row-cover-thumb` only carries dimensions in
track-row / queue contexts; box sets with distinct disc art resolve per disc
via the library cover resolver. */
.disc-header .track-row-cover-thumb {
flex-shrink: 0;
width: 40px;
height: 40px;
border-radius: var(--radius-sm);
object-fit: cover;
background: var(--bg-hover);
}
.disc-header .track-row-cover-thumb--placeholder {
display: inline-flex;
align-items: center;
justify-content: center;
color: var(--text-muted);
opacity: 0.55;
}
/* Per-disc subtitle (OpenSubsonic `discTitles`), e.g. "Sessions" on a deluxe CD 3. */