mirror of
https://github.com/navidrome/navidrome.git
synced 2026-04-28 03:19:38 +00:00
feat: add configurable UICoverArtSize option
Converted the hardcoded UICoverArtSize constant (600px) into a configurable option, allowing users to reduce the cover art size requested by the UI to mitigate slow image encoding. The value is served to the frontend via the app config and used by all components that request cover art. Also simplified the cache warmer by removing a single-iteration loop in favor of direct code.
This commit is contained in:
parent
6cd689dfbf
commit
d8abe80d2c
20 changed files with 42 additions and 42 deletions
|
|
@ -88,6 +88,7 @@ type configOptions struct {
|
|||
DefaultLanguage string
|
||||
DefaultUIVolume int
|
||||
UISearchDebounceMs int
|
||||
UICoverArtSize int
|
||||
EnableReplayGain bool
|
||||
EnableCoverAnimation bool
|
||||
EnableNowPlaying bool
|
||||
|
|
@ -729,6 +730,7 @@ func setViperDefaults() {
|
|||
viper.SetDefault("defaultlanguage", "")
|
||||
viper.SetDefault("defaultuivolume", consts.DefaultUIVolume)
|
||||
viper.SetDefault("uisearchdebouncems", consts.DefaultUISearchDebounceMs)
|
||||
viper.SetDefault("uicoverartsize", consts.DefaultUICoverArtSize)
|
||||
viper.SetDefault("enablereplaygain", true)
|
||||
viper.SetDefault("enablecoveranimation", true)
|
||||
viper.SetDefault("enablenowplaying", true)
|
||||
|
|
|
|||
|
|
@ -85,11 +85,9 @@ const (
|
|||
)
|
||||
|
||||
const (
|
||||
UICoverArtSize = 600
|
||||
DefaultUICoverArtSize = 600
|
||||
)
|
||||
|
||||
var CacheWarmerImageSizes = []int{UICoverArtSize}
|
||||
|
||||
// Prometheus options
|
||||
const (
|
||||
PrometheusDefaultPath = "/metrics"
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/navidrome/navidrome/conf"
|
||||
"github.com/navidrome/navidrome/consts"
|
||||
"github.com/navidrome/navidrome/log"
|
||||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/navidrome/navidrome/model/request"
|
||||
|
|
@ -24,7 +23,7 @@ type CacheWarmer interface {
|
|||
|
||||
// NewCacheWarmer creates a new CacheWarmer instance. The CacheWarmer will pre-cache Artwork images in the background
|
||||
// to speed up the response time when the image is requested by the UI. The cache is pre-populated with the original
|
||||
// image size, as well as the size defined in the UICoverArtSize constant.
|
||||
// image size, as well as the size defined by the UICoverArtSize config option.
|
||||
func NewCacheWarmer(artwork Artwork, cache cache.FileCache) CacheWarmer {
|
||||
// If image cache is disabled, return a NOOP implementation
|
||||
if conf.Server.ImageCacheSize == "0" || !conf.Server.EnableArtworkPrecache {
|
||||
|
|
@ -142,16 +141,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 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)
|
||||
}
|
||||
_, err = io.Copy(io.Discard, r)
|
||||
r.Close()
|
||||
return err
|
||||
size := conf.Server.UICoverArtSize
|
||||
r, _, err := a.artwork.Get(ctx, id, size, true)
|
||||
if err != nil {
|
||||
return fmt.Errorf("caching id='%s', size=%d: %w", id, size, err)
|
||||
}
|
||||
return nil
|
||||
_, err = io.Copy(io.Discard, r)
|
||||
r.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
func NoopCacheWarmer() CacheWarmer {
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ import (
|
|||
|
||||
"github.com/navidrome/navidrome/conf"
|
||||
"github.com/navidrome/navidrome/conf/configtest"
|
||||
"github.com/navidrome/navidrome/consts"
|
||||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/navidrome/navidrome/utils/cache"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
|
|
@ -182,7 +181,7 @@ var _ = Describe("CacheWarmer", func() {
|
|||
|
||||
Eventually(func() []int {
|
||||
return aw.getCachedSizes()
|
||||
}).Should(ContainElements(consts.UICoverArtSize))
|
||||
}).Should(ContainElements(conf.Server.UICoverArtSize))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -196,6 +196,7 @@ var staticData = sync.OnceValue(func() insights.Data {
|
|||
data.Config.EnableArtworkUpload = conf.Server.EnableArtworkUpload
|
||||
data.Config.CoverArtQuality = conf.Server.CoverArtQuality
|
||||
data.Config.EnableWebPEncoding = conf.Server.EnableWebPEncoding
|
||||
data.Config.UICoverArtSize = conf.Server.UICoverArtSize
|
||||
data.Config.EnableCoverAnimation = conf.Server.EnableCoverAnimation
|
||||
data.Config.EnableNowPlaying = conf.Server.EnableNowPlaying
|
||||
data.Config.EnableDownloads = conf.Server.EnableDownloads
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@ type Data struct {
|
|||
EnableArtworkUpload bool `json:"enableArtworkUpload,omitempty"`
|
||||
CoverArtQuality int `json:"coverArtQuality,omitempty"`
|
||||
EnableWebPEncoding bool `json:"enableWebPEncoding,omitempty"`
|
||||
UICoverArtSize int `json:"uiCoverArtSize,omitempty"`
|
||||
EnableCoverAnimation bool `json:"enableCoverAnimation,omitempty"`
|
||||
EnableNowPlaying bool `json:"enableNowPlaying,omitempty"`
|
||||
SessionTimeout uint64 `json:"sessionTimeout,omitempty"`
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import (
|
|||
"net/http"
|
||||
"path"
|
||||
|
||||
"github.com/navidrome/navidrome/conf"
|
||||
"github.com/navidrome/navidrome/consts"
|
||||
"github.com/navidrome/navidrome/core/auth"
|
||||
"github.com/navidrome/navidrome/core/publicurl"
|
||||
|
|
@ -81,7 +82,7 @@ func checkShareError(ctx context.Context, w http.ResponseWriter, err error, id s
|
|||
|
||||
func (pub *Router) mapShareInfo(r *http.Request, s model.Share) *model.Share {
|
||||
s.URL = ShareURL(r, s.ID)
|
||||
s.ImageURL = publicurl.ImageURL(r, s.CoverArtID(), consts.UICoverArtSize)
|
||||
s.ImageURL = publicurl.ImageURL(r, s.CoverArtID(), conf.Server.UICoverArtSize)
|
||||
for i := range s.Tracks {
|
||||
s.Tracks[i].ID = encodeMediafileShare(s, s.Tracks[i].ID)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ func serveIndex(ds model.DataStore, fs fs.FS, shareInfo *model.Share) http.Handl
|
|||
"defaultLanguage": conf.Server.DefaultLanguage,
|
||||
"defaultUIVolume": conf.Server.DefaultUIVolume,
|
||||
"uiSearchDebounceMs": conf.Server.UISearchDebounceMs,
|
||||
"uiCoverArtSize": conf.Server.UICoverArtSize,
|
||||
"enableCoverAnimation": conf.Server.EnableCoverAnimation,
|
||||
"enableNowPlaying": conf.Server.EnableNowPlaying,
|
||||
"gaTrackingId": conf.Server.GATrackingID,
|
||||
|
|
|
|||
|
|
@ -86,6 +86,7 @@ var _ = Describe("serveIndex", func() {
|
|||
Entry("defaultLanguage", func() { conf.Server.DefaultLanguage = "pt" }, "defaultLanguage", "pt"),
|
||||
Entry("defaultUIVolume", func() { conf.Server.DefaultUIVolume = 45 }, "defaultUIVolume", float64(45)),
|
||||
Entry("uiSearchDebounceMs", func() { conf.Server.UISearchDebounceMs = 500 }, "uiSearchDebounceMs", float64(500)),
|
||||
Entry("uiCoverArtSize", func() { conf.Server.UICoverArtSize = 300 }, "uiCoverArtSize", float64(300)),
|
||||
Entry("enableCoverAnimation", func() { conf.Server.EnableCoverAnimation = true }, "enableCoverAnimation", true),
|
||||
Entry("enableNowPlaying", func() { conf.Server.EnableNowPlaying = true }, "enableNowPlaying", true),
|
||||
Entry("gaTrackingId", func() { conf.Server.GATrackingID = "UA-12345" }, "gaTrackingId", "UA-12345"),
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ import {
|
|||
useTranslate,
|
||||
} from 'react-admin'
|
||||
import Lightbox from 'react-image-lightbox'
|
||||
import { COVER_ART_SIZE } from '../consts'
|
||||
import config from '../config'
|
||||
import 'react-image-lightbox/style.css'
|
||||
import subsonic from '../subsonic'
|
||||
import {
|
||||
|
|
@ -32,7 +32,6 @@ import {
|
|||
useAlbumsPerPage,
|
||||
useImageLoadingState,
|
||||
} from '../common'
|
||||
import config from '../config'
|
||||
import { formatFullDate, intersperse } from '../utils'
|
||||
import AlbumExternalLinks from './AlbumExternalLinks'
|
||||
import { SafeHTML } from '../common/SafeHTML'
|
||||
|
|
@ -255,7 +254,7 @@ const AlbumDetails = (props) => {
|
|||
})
|
||||
}, [record])
|
||||
|
||||
const imageUrl = subsonic.getCoverArtUrl(record, COVER_ART_SIZE)
|
||||
const imageUrl = subsonic.getCoverArtUrl(record, config.uiCoverArtSize)
|
||||
const fullImageUrl = subsonic.getCoverArtUrl(record)
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -20,7 +20,8 @@ import {
|
|||
OverflowTooltip,
|
||||
useImageUrl,
|
||||
} from '../common'
|
||||
import { COVER_ART_SIZE, DraggableTypes } from '../consts'
|
||||
import config from '../config'
|
||||
import { DraggableTypes } from '../consts'
|
||||
import clsx from 'clsx'
|
||||
import { AlbumDatesField } from './AlbumDatesField.jsx'
|
||||
|
||||
|
|
@ -135,7 +136,7 @@ const Cover = withContentRect('bounds')(({
|
|||
[record],
|
||||
)
|
||||
|
||||
const url = subsonic.getCoverArtUrl(record, COVER_ART_SIZE, true)
|
||||
const url = subsonic.getCoverArtUrl(record, config.uiCoverArtSize, true)
|
||||
const { imgUrl, loading: imageLoading } = useImageUrl(url)
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ import {
|
|||
import Lightbox from 'react-image-lightbox'
|
||||
import ExpandInfoDialog from '../dialogs/ExpandInfoDialog'
|
||||
import AlbumInfo from '../album/AlbumInfo'
|
||||
import { COVER_ART_SIZE } from '../consts'
|
||||
import subsonic from '../subsonic'
|
||||
import { SafeHTML } from '../common/SafeHTML'
|
||||
|
||||
|
|
@ -110,7 +109,7 @@ const DesktopArtistDetails = ({ artistInfo, record, biography }) => {
|
|||
<CardMedia
|
||||
key={record.id}
|
||||
component="img"
|
||||
src={subsonic.getCoverArtUrl(record, COVER_ART_SIZE)}
|
||||
src={subsonic.getCoverArtUrl(record, config.uiCoverArtSize)}
|
||||
className={`${classes.cover} ${imageLoading ? classes.coverLoading : ''}`}
|
||||
onClick={handleOpenLightbox}
|
||||
onLoad={handleImageLoad}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ import {
|
|||
useImageLoadingState,
|
||||
} from '../common'
|
||||
import Lightbox from 'react-image-lightbox'
|
||||
import { COVER_ART_SIZE } from '../consts'
|
||||
import subsonic from '../subsonic'
|
||||
import { SafeHTML } from '../common/SafeHTML'
|
||||
|
||||
|
|
@ -113,7 +112,7 @@ const MobileArtistDetails = ({ artistInfo, biography, record }) => {
|
|||
<CardMedia
|
||||
key={record.id}
|
||||
component="img"
|
||||
src={subsonic.getCoverArtUrl(record, COVER_ART_SIZE)}
|
||||
src={subsonic.getCoverArtUrl(record, config.uiCoverArtSize)}
|
||||
className={`${classes.cover} ${imageLoading ? classes.coverLoading : ''}`}
|
||||
onClick={handleOpenLightbox}
|
||||
onLoad={handleImageLoad}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { useRecordContext } from 'react-admin'
|
|||
import { Avatar } from '@material-ui/core'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import clsx from 'clsx'
|
||||
import { COVER_ART_SIZE } from '../consts'
|
||||
import config from '../config'
|
||||
import subsonic from '../subsonic'
|
||||
import { useImageUrl } from './useImageUrl'
|
||||
|
||||
|
|
@ -28,7 +28,7 @@ export const CoverArtAvatar = ({
|
|||
const record = recordProp || recordContext
|
||||
const square = variant !== 'circular'
|
||||
const url = record
|
||||
? subsonic.getCoverArtUrl(record, COVER_ART_SIZE, square)
|
||||
? subsonic.getCoverArtUrl(record, config.uiCoverArtSize, square)
|
||||
: null
|
||||
const { imgUrl } = useImageUrl(url)
|
||||
if (!record) return null
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ const defaultConfig = {
|
|||
defaultLanguage: '',
|
||||
defaultUIVolume: 100,
|
||||
uiSearchDebounceMs: 200,
|
||||
uiCoverArtSize: 600,
|
||||
enableUserEditing: true,
|
||||
enableArtworkUpload: true,
|
||||
enableSharing: true,
|
||||
|
|
|
|||
|
|
@ -26,8 +26,6 @@ DraggableTypes.ALL.push(
|
|||
|
||||
export const RADIO_PLACEHOLDER_IMAGE = 'internet-radio-icon.svg'
|
||||
|
||||
export const COVER_ART_SIZE = 600
|
||||
|
||||
export const DEFAULT_SHARE_BITRATE = 128
|
||||
|
||||
export const BITRATE_CHOICES = [
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ import {
|
|||
OverflowTooltip,
|
||||
useImageLoadingState,
|
||||
} from '../common'
|
||||
import { COVER_ART_SIZE } from '../consts'
|
||||
import config from '../config'
|
||||
import subsonic from '../subsonic'
|
||||
|
||||
const useStyles = makeStyles(
|
||||
|
|
@ -107,7 +107,7 @@ const PlaylistDetails = (props) => {
|
|||
handleCloseLightbox,
|
||||
} = useImageLoadingState(record.id)
|
||||
|
||||
const imageUrl = subsonic.getCoverArtUrl(record, COVER_ART_SIZE, true)
|
||||
const imageUrl = subsonic.getCoverArtUrl(record, config.uiCoverArtSize, true)
|
||||
const fullImageUrl = subsonic.getCoverArtUrl(record)
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -11,7 +11,8 @@ import { makeStyles } from '@material-ui/core/styles'
|
|||
import { urlValidate } from '../utils/validations'
|
||||
import { Title, ImageUploadOverlay, useImageLoadingState } from '../common'
|
||||
import subsonic from '../subsonic'
|
||||
import { COVER_ART_SIZE, RADIO_PLACEHOLDER_IMAGE } from '../consts'
|
||||
import config from '../config'
|
||||
import { RADIO_PLACEHOLDER_IMAGE } from '../consts'
|
||||
|
||||
const useStyles = makeStyles({
|
||||
coverParent: {
|
||||
|
|
@ -83,7 +84,7 @@ const RadioCoverArt = ({ record }) => {
|
|||
{record.uploadedImage ? (
|
||||
<CardMedia
|
||||
component="img"
|
||||
src={subsonic.getCoverArtUrl(record, COVER_ART_SIZE, true)}
|
||||
src={subsonic.getCoverArtUrl(record, config.uiCoverArtSize, true)}
|
||||
className={`${classes.cover} ${imageLoading ? classes.coverLoading : ''}`}
|
||||
onLoad={handleImageLoad}
|
||||
onError={handleImageError}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import subsonic from '../subsonic'
|
||||
import { COVER_ART_SIZE, RADIO_PLACEHOLDER_IMAGE } from '../consts'
|
||||
import config from '../config'
|
||||
import { RADIO_PLACEHOLDER_IMAGE } from '../consts'
|
||||
|
||||
export async function songFromRadio(radio) {
|
||||
if (!radio) {
|
||||
|
|
@ -8,7 +9,7 @@ export async function songFromRadio(radio) {
|
|||
|
||||
let cover = RADIO_PLACEHOLDER_IMAGE
|
||||
if (radio.uploadedImage) {
|
||||
cover = subsonic.getCoverArtUrl(radio, COVER_ART_SIZE, true)
|
||||
cover = subsonic.getCoverArtUrl(radio, config.uiCoverArtSize, true)
|
||||
} else {
|
||||
// Try favicon as fallback
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { vi } from 'vitest'
|
||||
import { COVER_ART_SIZE } from '../consts'
|
||||
import config from '../config'
|
||||
import subsonic from './index'
|
||||
|
||||
describe('getCoverArtUrl', () => {
|
||||
|
|
@ -31,7 +31,7 @@ describe('getCoverArtUrl', () => {
|
|||
updatedAt: '2023-01-01T00:00:00Z',
|
||||
}
|
||||
|
||||
const url = subsonic.getCoverArtUrl(playlistRecord, COVER_ART_SIZE, true)
|
||||
const url = subsonic.getCoverArtUrl(playlistRecord, config.uiCoverArtSize, true)
|
||||
|
||||
expect(url).toContain('pl-playlist-123')
|
||||
expect(url).toContain('size=600')
|
||||
|
|
@ -45,7 +45,7 @@ describe('getCoverArtUrl', () => {
|
|||
sync: true,
|
||||
}
|
||||
|
||||
const url = subsonic.getCoverArtUrl(playlistRecord, COVER_ART_SIZE, true)
|
||||
const url = subsonic.getCoverArtUrl(playlistRecord, config.uiCoverArtSize, true)
|
||||
|
||||
expect(url).toContain('pl-playlist-123')
|
||||
expect(url).toContain('size=600')
|
||||
|
|
@ -60,7 +60,7 @@ describe('getCoverArtUrl', () => {
|
|||
updatedAt: '2023-01-01T00:00:00Z',
|
||||
}
|
||||
|
||||
const url = subsonic.getCoverArtUrl(albumRecord, COVER_ART_SIZE, true)
|
||||
const url = subsonic.getCoverArtUrl(albumRecord, config.uiCoverArtSize, true)
|
||||
|
||||
expect(url).toContain('al-album-123')
|
||||
expect(url).toContain('size=600')
|
||||
|
|
@ -74,7 +74,7 @@ describe('getCoverArtUrl', () => {
|
|||
updatedAt: '2023-01-01T00:00:00Z',
|
||||
}
|
||||
|
||||
const url = subsonic.getCoverArtUrl(songRecord, COVER_ART_SIZE, true)
|
||||
const url = subsonic.getCoverArtUrl(songRecord, config.uiCoverArtSize, true)
|
||||
|
||||
expect(url).toContain('mf-song-123')
|
||||
expect(url).toContain('size=600')
|
||||
|
|
@ -87,7 +87,7 @@ describe('getCoverArtUrl', () => {
|
|||
updatedAt: '2023-01-01T00:00:00Z',
|
||||
}
|
||||
|
||||
const url = subsonic.getCoverArtUrl(artistRecord, COVER_ART_SIZE, true)
|
||||
const url = subsonic.getCoverArtUrl(artistRecord, config.uiCoverArtSize, true)
|
||||
|
||||
expect(url).toContain('ar-artist-123')
|
||||
expect(url).toContain('size=600')
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue