mirror of
https://github.com/navidrome/navidrome.git
synced 2026-11-09 11:48:41 +01:00
feat(artist): add Share and Download actions to the Artist detail page (#5944)
* feat(ui): add Share button to artist detail page * feat(ui): add Download button to artist detail page * fix(ui): scope artist share/download to album-artist content Gate the artist Share/Download actions on album-artist stats and show the album-artist size, since ZipArtist and the share query only cover album_artist_id songs. Previously the total (role-inclusive) size was shown and guest-only artists could produce an empty archive. Applies to the artist toolbar, the shared context menu, and the download dialog title. * fix: match artist download/share to album-artist participation ZipArtist and the artist share query filtered the deprecated album_artist_id column, which only stores the first album artist of a track. Secondary album-artists (co-credited but not first) got an empty download/share even though the UI offered it. Filter by the album-artist role participation instead, matching the artist's album-artist stats used to gate the actions. Also cover the artist-specific size branch of the download dialog. * fix(share): scope artist shares to the owner's libraries The artist share query broadened to album-artist participation, which could pull a secondary album artist's tracks from libraries the (non-admin) share owner cannot access into the public share. Load the artist share as the owner so their library access is applied, mirroring how playlist shares already work. Adds a repository test covering co-album-artist inclusion and library scoping. * test(share): assert album participation branch of artist shares Link the co-album-artist fixtures to albums and assert share.Albums (used by Subsonic getShares) includes the accessible album and excludes the one in a library the owner cannot access, so the album participation + scoping branch is covered too. * fix: exclude missing files from artist download/share actions An artist's stats still count files that went missing, so the toolbar/context menu could offer Download/Share for an artist whose files are all gone, while the share query (missing=false) returns nothing and downloads open dead paths. Hide the actions when the artist is missing and exclude missing files from ZipArtist, matching the share semantics. * refactor: dedupe artist download-size and share-owner lookups Extract the 'album-artist download size (or none when missing)' rule into a single artistDownloadSize() helper shared by the toolbar, context menu, and download dialog, and factor the duplicated share-owner context lookup into a shareRepository.ownerContext() method used by both the artist and playlist share cases. * refactor(ui): move artistDownloadSize helper to common utils is for domain-agnostic, potentially portable code; this helper is Navidrome-specific (artist stats shape), so it belongs in common. Consumers import it directly from common/artist to avoid pulling in the common barrel.
This commit is contained in:
parent
8978c7b9fa
commit
752b38609c
13 changed files with 411 additions and 19 deletions
|
|
@ -8,8 +8,9 @@ import {
|
|||
DialogTitle,
|
||||
} from '@material-ui/core'
|
||||
import subsonic from '../subsonic'
|
||||
import { closeDownloadMenu } from '../actions'
|
||||
import { closeDownloadMenu, DOWNLOAD_MENU_ARTIST } from '../actions'
|
||||
import { formatBytes } from '../utils'
|
||||
import { artistDownloadSize } from '../common/artist'
|
||||
import { useTranscodingOptions } from './useTranscodingOptions'
|
||||
|
||||
const DownloadMenuDialog = () => {
|
||||
|
|
@ -22,6 +23,12 @@ const DownloadMenuDialog = () => {
|
|||
const { TranscodingOptionsInput, format, maxBitRate, originalFormat } =
|
||||
useTranscodingOptions()
|
||||
|
||||
// Artist downloads only include album-artist songs, so show that size
|
||||
const downloadSize =
|
||||
recordType === DOWNLOAD_MENU_ARTIST
|
||||
? artistDownloadSize(record)
|
||||
: record?.size
|
||||
|
||||
const handleClose = (e) => {
|
||||
dispatch(closeDownloadMenu())
|
||||
e.stopPropagation()
|
||||
|
|
@ -55,7 +62,7 @@ const DownloadMenuDialog = () => {
|
|||
smart_count: 1,
|
||||
}).toLocaleLowerCase(),
|
||||
name: record?.name || record?.title,
|
||||
size: formatBytes(record?.size),
|
||||
size: formatBytes(downloadSize),
|
||||
})}
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
|
|
|
|||
58
ui/src/dialogs/DownloadMenuDialog.test.jsx
Normal file
58
ui/src/dialogs/DownloadMenuDialog.test.jsx
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import * as React from 'react'
|
||||
import { TestContext } from 'ra-test'
|
||||
import { render, screen, cleanup } from '@testing-library/react'
|
||||
import { describe, afterEach, it, expect, vi } from 'vitest'
|
||||
import DownloadMenuDialog from './DownloadMenuDialog'
|
||||
import { DOWNLOAD_MENU_ALBUM, DOWNLOAD_MENU_ARTIST } from '../actions'
|
||||
|
||||
vi.mock('./useTranscodingOptions', () => ({
|
||||
useTranscodingOptions: () => ({
|
||||
TranscodingOptionsInput: () => null,
|
||||
format: '',
|
||||
maxBitRate: 0,
|
||||
originalFormat: true,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('react-admin', async (importOriginal) => {
|
||||
const actual = await importOriginal()
|
||||
return {
|
||||
...actual,
|
||||
useTranslate: () => (key, opts) =>
|
||||
opts?.size ? `${key}:${opts.name}:${opts.size}` : key,
|
||||
}
|
||||
})
|
||||
|
||||
const renderDialog = (record, recordType) =>
|
||||
render(
|
||||
<TestContext
|
||||
initialState={{ downloadMenuDialog: { open: true, record, recordType } }}
|
||||
>
|
||||
<DownloadMenuDialog />
|
||||
</TestContext>,
|
||||
)
|
||||
|
||||
describe('DownloadMenuDialog', () => {
|
||||
afterEach(cleanup)
|
||||
|
||||
it('shows the album-artist size (not the total) for an artist download', () => {
|
||||
renderDialog(
|
||||
{
|
||||
id: 'ar1',
|
||||
name: 'Artist',
|
||||
size: 999999999,
|
||||
stats: { albumartist: { size: 1024 } },
|
||||
},
|
||||
DOWNLOAD_MENU_ARTIST,
|
||||
)
|
||||
expect(screen.getByText(/:Artist:1 KB$/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows the total size for an album download', () => {
|
||||
renderDialog(
|
||||
{ id: 'al1', name: 'Album', size: 1024 * 1024 },
|
||||
DOWNLOAD_MENU_ALBUM,
|
||||
)
|
||||
expect(screen.getByText(/:Album:1 MB$/)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue