From cb396f3dba3084ab3d581c5f7c89c353291d5d44 Mon Sep 17 00:00:00 2001 From: Deluan Date: Sun, 22 Mar 2026 14:54:28 -0400 Subject: [PATCH] feat(ui): increase cover art size to 600px and use CatmullRom scaling Increased the UI cover art request size from 300px to 600px for sharper images on high-DPI displays. Replaced BiLinear with CatmullRom (bicubic) interpolation for higher quality image resizing. Extracted the hardcoded size into a COVER_ART_SIZE constant in the frontend and consolidated backend sizes into a CacheWarmerImageSizes slice. Removed the unused UIThumbnailSize constant. Signed-off-by: Deluan --- consts/consts.go | 8 ++++++-- core/artwork/cache_warmer.go | 9 ++++----- core/artwork/cache_warmer_test.go | 4 ++-- core/artwork/reader_playlist.go | 2 +- core/artwork/reader_resized.go | 2 +- ui/src/album/AlbumDetails.jsx | 3 ++- ui/src/album/AlbumGridView.jsx | 4 ++-- ui/src/artist/DesktopArtistDetails.jsx | 3 ++- ui/src/artist/MobileArtistDetails.jsx | 3 ++- ui/src/common/CoverArtAvatar.jsx | 3 ++- ui/src/consts.js | 2 ++ ui/src/playlist/PlaylistDetails.jsx | 3 ++- ui/src/radio/RadioEdit.jsx | 4 ++-- ui/src/radio/helper.jsx | 4 ++-- ui/src/subsonic/index.test.js | 21 +++++++++++---------- 15 files changed, 43 insertions(+), 32 deletions(-) diff --git a/consts/consts.go b/consts/consts.go index 6fb6c5da..f1010a87 100644 --- a/consts/consts.go +++ b/consts/consts.go @@ -70,8 +70,6 @@ const ( PlaceholderArtistArt = "artist-placeholder.webp" PlaceholderAlbumArt = "album-placeholder.webp" PlaceholderAvatar = "logo-192x192.png" - UICoverArtSize = 300 - UIThumbnailSize = 80 DefaultUIVolume = 100 DefaultUISearchDebounceMs = 200 @@ -86,6 +84,12 @@ const ( Zwsp = string('\u200b') ) +const ( + UICoverArtSize = 600 +) + +var CacheWarmerImageSizes = []int{UICoverArtSize} + // Prometheus options const ( PrometheusDefaultPath = "/metrics" diff --git a/core/artwork/cache_warmer.go b/core/artwork/cache_warmer.go index 83c98c80..bd1359b7 100644 --- a/core/artwork/cache_warmer.go +++ b/core/artwork/cache_warmer.go @@ -142,15 +142,14 @@ func (a *cacheWarmer) doCacheImage(ctx context.Context, id model.ArtworkID) erro ctx, cancel := context.WithTimeout(ctx, 10*time.Second) defer cancel() - for _, size := range []int{consts.UICoverArtSize, consts.UIThumbnailSize} { + for _, size := range consts.CacheWarmerImageSizes { r, _, err := a.artwork.Get(ctx, id, size, true) if err != nil { return fmt.Errorf("caching id='%s', size=%d: %w", id, size, err) } - defer r.Close() - if _, err = io.Copy(io.Discard, r); err != nil { - return err - } + _, err = io.Copy(io.Discard, r) + r.Close() + return err } return nil } diff --git a/core/artwork/cache_warmer_test.go b/core/artwork/cache_warmer_test.go index 6ddda00d..9798ea8d 100644 --- a/core/artwork/cache_warmer_test.go +++ b/core/artwork/cache_warmer_test.go @@ -176,13 +176,13 @@ var _ = Describe("CacheWarmer", func() { }).Should(Equal(0)) }) - It("pre-caches both UICoverArtSize and UIThumbnailSize", func() { + It("pre-caches UICoverArtSize", func() { cw := NewCacheWarmer(aw, fc).(*cacheWarmer) cw.PreCache(model.MustParseArtworkID("al-1")) Eventually(func() []int { return aw.getCachedSizes() - }).Should(ContainElements(consts.UICoverArtSize, consts.UIThumbnailSize)) + }).Should(ContainElements(consts.UICoverArtSize)) }) }) }) diff --git a/core/artwork/reader_playlist.go b/core/artwork/reader_playlist.go index 7919eead..09707843 100644 --- a/core/artwork/reader_playlist.go +++ b/core/artwork/reader_playlist.go @@ -264,6 +264,6 @@ func fillCenter(src image.Image, dstW, dstH int) image.Image { } dst := image.NewNRGBA(image.Rect(0, 0, dstW, dstH)) - xdraw.BiLinear.Scale(dst, dst.Bounds(), src, cropRect, draw.Src, nil) + xdraw.CatmullRom.Scale(dst, dst.Bounds(), src, cropRect, draw.Src, nil) return dst } diff --git a/core/artwork/reader_resized.go b/core/artwork/reader_resized.go index 7e90f10e..72baad43 100644 --- a/core/artwork/reader_resized.go +++ b/core/artwork/reader_resized.go @@ -155,7 +155,7 @@ func resizeStaticImage(data []byte, size int, square bool) (io.Reader, int, erro dst = image.NewNRGBA(image.Rect(0, 0, dstW, dstH)) dstRect = dst.Bounds() } - xdraw.BiLinear.Scale(dst, dstRect, original, bounds, draw.Src, nil) + xdraw.CatmullRom.Scale(dst, dstRect, original, bounds, draw.Src, nil) buf := bufPool.Get().(*bytes.Buffer) buf.Reset() diff --git a/ui/src/album/AlbumDetails.jsx b/ui/src/album/AlbumDetails.jsx index c5d9a7ac..2411b861 100644 --- a/ui/src/album/AlbumDetails.jsx +++ b/ui/src/album/AlbumDetails.jsx @@ -18,6 +18,7 @@ import { useTranslate, } from 'react-admin' import Lightbox from 'react-image-lightbox' +import { COVER_ART_SIZE } from '../consts' import 'react-image-lightbox/style.css' import subsonic from '../subsonic' import { @@ -254,7 +255,7 @@ const AlbumDetails = (props) => { }) }, [record]) - const imageUrl = subsonic.getCoverArtUrl(record, 300) + const imageUrl = subsonic.getCoverArtUrl(record, COVER_ART_SIZE) const fullImageUrl = subsonic.getCoverArtUrl(record) return ( diff --git a/ui/src/album/AlbumGridView.jsx b/ui/src/album/AlbumGridView.jsx index 6b44c5fe..e90e7a77 100644 --- a/ui/src/album/AlbumGridView.jsx +++ b/ui/src/album/AlbumGridView.jsx @@ -19,7 +19,7 @@ import { ArtistLinkField, OverflowTooltip, } from '../common' -import { DraggableTypes } from '../consts' +import { COVER_ART_SIZE, DraggableTypes } from '../consts' import clsx from 'clsx' import { AlbumDatesField } from './AlbumDatesField.jsx' @@ -157,7 +157,7 @@ const Cover = withContentRect('bounds')(({
{record.name} { { { handleCloseLightbox, } = useImageLoadingState(record.id) - const imageUrl = subsonic.getCoverArtUrl(record, 300, true) + const imageUrl = subsonic.getCoverArtUrl(record, COVER_ART_SIZE, true) const fullImageUrl = subsonic.getCoverArtUrl(record) return ( diff --git a/ui/src/radio/RadioEdit.jsx b/ui/src/radio/RadioEdit.jsx index 6b1d2df7..5f804535 100644 --- a/ui/src/radio/RadioEdit.jsx +++ b/ui/src/radio/RadioEdit.jsx @@ -11,7 +11,7 @@ import { makeStyles } from '@material-ui/core/styles' import { urlValidate } from '../utils/validations' import { Title, ImageUploadOverlay, useImageLoadingState } from '../common' import subsonic from '../subsonic' -import { RADIO_PLACEHOLDER_IMAGE } from '../consts' +import { COVER_ART_SIZE, RADIO_PLACEHOLDER_IMAGE } from '../consts' const useStyles = makeStyles({ coverParent: { @@ -83,7 +83,7 @@ const RadioCoverArt = ({ record }) => { {record.uploadedImage ? ( { @@ -30,10 +31,10 @@ describe('getCoverArtUrl', () => { updatedAt: '2023-01-01T00:00:00Z', } - const url = subsonic.getCoverArtUrl(playlistRecord, 300, true) + const url = subsonic.getCoverArtUrl(playlistRecord, COVER_ART_SIZE, true) expect(url).toContain('pl-playlist-123') - expect(url).toContain('size=300') + expect(url).toContain('size=600') expect(url).toContain('square=true') expect(url).toContain('_=2023-01-01T00%3A00%3A00Z') }) @@ -44,10 +45,10 @@ describe('getCoverArtUrl', () => { sync: true, } - const url = subsonic.getCoverArtUrl(playlistRecord, 300, true) + const url = subsonic.getCoverArtUrl(playlistRecord, COVER_ART_SIZE, true) expect(url).toContain('pl-playlist-123') - expect(url).toContain('size=300') + expect(url).toContain('size=600') expect(url).toContain('square=true') expect(url).not.toContain('_=') }) @@ -59,10 +60,10 @@ describe('getCoverArtUrl', () => { updatedAt: '2023-01-01T00:00:00Z', } - const url = subsonic.getCoverArtUrl(albumRecord, 300, true) + const url = subsonic.getCoverArtUrl(albumRecord, COVER_ART_SIZE, true) expect(url).toContain('al-album-123') - expect(url).toContain('size=300') + expect(url).toContain('size=600') expect(url).toContain('square=true') }) @@ -73,10 +74,10 @@ describe('getCoverArtUrl', () => { updatedAt: '2023-01-01T00:00:00Z', } - const url = subsonic.getCoverArtUrl(songRecord, 300, true) + const url = subsonic.getCoverArtUrl(songRecord, COVER_ART_SIZE, true) expect(url).toContain('mf-song-123') - expect(url).toContain('size=300') + expect(url).toContain('size=600') expect(url).toContain('square=true') }) @@ -86,10 +87,10 @@ describe('getCoverArtUrl', () => { updatedAt: '2023-01-01T00:00:00Z', } - const url = subsonic.getCoverArtUrl(artistRecord, 300, true) + const url = subsonic.getCoverArtUrl(artistRecord, COVER_ART_SIZE, true) expect(url).toContain('ar-artist-123') - expect(url).toContain('size=300') + expect(url).toContain('size=600') expect(url).toContain('square=true') })