ba8d427890
* feat(artwork): add KindRadioArtwork and EntityRadio constant * feat(model): add UploadedImage field and artwork methods to Radio * feat(model): add Radio to GetEntityByID lookup chain * feat(db): add uploaded_image column to radio table * feat(artwork): add radio artwork reader with uploaded image fallback * feat(api): add radio image upload/delete endpoints * feat(ui): add radio artwork ID prefix to getCoverArtUrl * feat(ui): add cover art display and upload to RadioEdit * feat(ui): add cover art thumbnails to radio list * feat(ui): prefer artwork URL in radio player helper * refactor: remove redundant code in radio artwork - Remove duplicate Avatar rendering in RadioList by reusing CoverArtField - Remove redundant UpdatedAt assignment in radio image handlers (already set by repository Put) * refactor(ui): extract shared useImageLoadingState hook Move image loading/error/lightbox state management into a shared useImageLoadingState hook in common/. Consolidates duplicated logic from AlbumDetails, PlaylistDetails, RadioEdit, and artist detail views. * feat(ui): use radio placeholder icon when no uploaded image Remove album placeholder fallback from radio artwork reader so radios without an uploaded image return ErrUnavailable. On the frontend, show the internet-radio-icon.svg placeholder instead of requesting server artwork when no image is uploaded, allowing favicon fallback in the player. * refactor(ui): update defaultOff fields in useSelectedFields for RadioList Signed-off-by: Deluan <deluan@navidrome.org> * fix: address code review feedback - Add missing alt attribute to CardMedia in RadioEdit for accessibility - Fix UpdateInternetRadio to preserve UploadedImage field by fetching existing radio before updating (prevents Subsonic API from clearing custom artwork) - Add Reader() level tests to verify ErrUnavailable is returned when radio has no uploaded image * refactor: add colsToUpdate to RadioRepository.Put Use the base sqlRepository.put with column filtering instead of hand-rolled SQL. UpdateInternetRadio now specifies only the Subsonic API fields, preventing UploadedImage from being cleared. Image upload/delete handlers specify only UploadedImage. * fix: ensure UpdatedAt is included in colsToUpdate for radio Put --------- Signed-off-by: Deluan <deluan@navidrome.org>
153 lines
3.8 KiB
JavaScript
153 lines
3.8 KiB
JavaScript
import { baseUrl } from '../utils'
|
|
import { httpClient } from '../dataProvider'
|
|
|
|
const url = (command, id, options) => {
|
|
const username = localStorage.getItem('username')
|
|
const token = localStorage.getItem('subsonic-token')
|
|
const salt = localStorage.getItem('subsonic-salt')
|
|
if (!username || !token || !salt) {
|
|
return ''
|
|
}
|
|
|
|
const params = new URLSearchParams()
|
|
params.append('u', username)
|
|
params.append('t', token)
|
|
params.append('s', salt)
|
|
params.append('f', 'json')
|
|
params.append('v', '1.8.0')
|
|
params.append('c', 'NavidromeUI')
|
|
id && params.append('id', id)
|
|
if (options) {
|
|
if (options.ts) {
|
|
options['_'] = new Date().getTime()
|
|
delete options.ts
|
|
}
|
|
Object.keys(options).forEach((k) => {
|
|
const value = options[k]
|
|
// Handle array parameters by appending each value separately
|
|
if (Array.isArray(value)) {
|
|
value.forEach((v) => params.append(k, v))
|
|
} else {
|
|
params.append(k, value)
|
|
}
|
|
})
|
|
}
|
|
return `/rest/${command}?${params.toString()}`
|
|
}
|
|
|
|
const ping = () => httpClient(url('ping'))
|
|
|
|
const scrobble = (id, time, submission = true, position = null) =>
|
|
httpClient(
|
|
url('scrobble', id, {
|
|
...(submission && time && { time }),
|
|
submission,
|
|
...(!submission && position !== null && { position }),
|
|
}),
|
|
)
|
|
|
|
const nowPlaying = (id, position = null) => scrobble(id, null, false, position)
|
|
|
|
const star = (id) => httpClient(url('star', id))
|
|
|
|
const unstar = (id) => httpClient(url('unstar', id))
|
|
|
|
const setRating = (id, rating) => httpClient(url('setRating', id, { rating }))
|
|
|
|
const download = (id, format = 'raw', bitrate = '0') =>
|
|
(window.location.href = baseUrl(url('download', id, { format, bitrate })))
|
|
|
|
const startScan = (options) => httpClient(url('startScan', null, options))
|
|
|
|
const getScanStatus = () => httpClient(url('getScanStatus'))
|
|
|
|
const getNowPlaying = () => httpClient(url('getNowPlaying'))
|
|
|
|
const getAvatarUrl = (username, size) =>
|
|
baseUrl(
|
|
url('getAvatar', null, {
|
|
username,
|
|
...(size && { size }),
|
|
}),
|
|
)
|
|
|
|
const getCoverArtUrl = (record, size, square) => {
|
|
const options = {
|
|
...(record.updatedAt && { _: record.updatedAt }),
|
|
...(size && { size }),
|
|
...(square && { square }),
|
|
}
|
|
|
|
// TODO Move this logic to server
|
|
if (record.album) {
|
|
return baseUrl(url('getCoverArt', 'mf-' + record.id, options))
|
|
} else if (record.albumArtist) {
|
|
return baseUrl(url('getCoverArt', 'al-' + record.id, options))
|
|
} else if (record.sync !== undefined) {
|
|
// This is a playlist
|
|
return baseUrl(url('getCoverArt', 'pl-' + record.id, options))
|
|
} else if (record.streamUrl !== undefined) {
|
|
// This is a radio station
|
|
return baseUrl(url('getCoverArt', 'ra-' + record.id, options))
|
|
} else {
|
|
return baseUrl(url('getCoverArt', 'ar-' + record.id, options))
|
|
}
|
|
}
|
|
|
|
const getDiscCoverArtUrl = (albumId, discNumber, updatedAt, size) => {
|
|
const options = {
|
|
...(updatedAt && { _: updatedAt }),
|
|
...(size && { size }),
|
|
}
|
|
return baseUrl(
|
|
url('getCoverArt', 'dc-' + albumId + ':' + discNumber, options),
|
|
)
|
|
}
|
|
|
|
const getArtistInfo = (id) => {
|
|
return httpClient(url('getArtistInfo', id))
|
|
}
|
|
|
|
const getAlbumInfo = (id) => {
|
|
return httpClient(url('getAlbumInfo', id))
|
|
}
|
|
|
|
const getSimilarSongs2 = (id, count = 100) => {
|
|
return httpClient(url('getSimilarSongs2', id, { count }))
|
|
}
|
|
|
|
const getTopSongs = (artist, count = 50) => {
|
|
return httpClient(url('getTopSongs', null, { artist, count }))
|
|
}
|
|
|
|
const streamUrl = (id, options) => {
|
|
return baseUrl(
|
|
url('stream', id, {
|
|
ts: true,
|
|
...options,
|
|
}),
|
|
)
|
|
}
|
|
|
|
export default {
|
|
url,
|
|
ping,
|
|
scrobble,
|
|
nowPlaying,
|
|
download,
|
|
star,
|
|
unstar,
|
|
setRating,
|
|
startScan,
|
|
getScanStatus,
|
|
getNowPlaying,
|
|
getCoverArtUrl,
|
|
getDiscCoverArtUrl,
|
|
getAvatarUrl,
|
|
streamUrl,
|
|
getAlbumInfo,
|
|
getArtistInfo,
|
|
getTopSongs,
|
|
getSimilarSongs2,
|
|
}
|