2023-03-26 19:34:12 -04:00
|
|
|
package core_test
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"archive/zip"
|
|
|
|
|
"bytes"
|
|
|
|
|
"context"
|
feat(archiver): add folder cover image to downloaded zips (#6224)
* feat(archiver): add folder cover image to downloaded zips
Album, artist, playlist and share downloads now include the item's cover as
folder.<ext>, the image most players and car stereos show for the files next to
it. This helps users who copy transcoded downloads to offline devices, since
transcoding drops the embedded artwork (#5841).
Album folders get the album cover, the artist zip root gets the artist image,
and playlist and share zips get the playlist or shared item's cover at the root.
The image is the same one getCoverArt serves, resized to 500px (not square),
and named by its detected type. Items without artwork get no image, and a cover
that fails to load is logged and skipped so it never breaks the archive.
The archiver reads covers through a new core.CoverArtReader interface,
implemented by artwork.CoverArtReader, to avoid an import cycle between core
and core/artwork.
* refactor(archiver): read covers through artwork.Artwork directly
The archiver no longer needs a local CoverArtReader interface and adapter.
The only reason core/artwork imported core was a core.AbsolutePath call in
loadArtistFolder, which now reads the library path from the repository and
cleans it the same way. With the import cycle gone, the archiver takes
artwork.Artwork and treats ErrUnavailable and ErrNotFound as no cover.
Covers are now written after each album's tracks (and after all tracks for
the archive root), so a slow artwork lookup does not delay the first bytes
of the download.
* fix(archiver): read share covers as admin so private playlists keep theirs
Public share downloads run with an anonymous context, and the playlist
repository hides private playlists from anonymous users, so a zip of a shared
private playlist silently had no folder image. Like the public image handler,
the share itself is the authorization: the cover lookup now runs with an admin
user. Only the cover read is elevated; streaming keeps the anonymous context,
so the transcode limiter still keys public downloads the same way.
* style(archiver): trim comments
Shorten the comments added by the folder cover image change and drop the ones the names already explain.
* fix(archiver): add one cover per album folder in artist zips
Albums with the same name share a zip folder (pre-existing naming), so an
artist zip with two such albums wrote two folder.<ext> entries at the same
path. Keep the first cover and skip the rest for that folder.
2026-09-25 16:42:49 -04:00
|
|
|
"errors"
|
2023-03-26 19:34:12 -04:00
|
|
|
"io"
|
|
|
|
|
"strings"
|
|
|
|
|
|
|
|
|
|
"github.com/Masterminds/squirrel"
|
fix(archiver): give same-named albums their own folder in artist zips (#6225)
* refactor: add Tags.First and slice.GroupOrdered helpers
Tags.First returns the first value of a tag or an empty string, replacing the inline len-check-then-index pattern in FullTitle, FullAlbumName, Album.FullName and the Subsonic album version mapping.
slice.GroupOrdered is slice.Group returning the groups in first-seen order, for callers that need a deterministic order the map-based Group cannot give.
* fix(archiver): give same-named albums their own folder in artist zips
Artist zips put every album in a folder named after the album, so two albums with the same name (an original and a deluxe edition, or names that only differ in characters the sanitizer replaces) were merged into one folder, with tracks mixed together and duplicate zip entries when file names collided.
The folder is now named after FullAlbumName(), so with AppendAlbumVersion on (the default) the version is part of the name, matching what clients display. Albums whose sanitized names still clash get a " [suffix]" taken from the first field that has a distinct, non-empty value for all of them: album version, year, release type, record label, catalog number, then a short album id. This follows the shape of beets' %aunique{} path function.
Albums are also grouped with slice.GroupOrdered instead of a map, so the zip is deterministic.
* fix(archiver): use the release year to tell same-named albums apart
Taggers often write an edition's date to the Date tag next to an original date, and the scanner then stores the original year in Year and the edition's year in ReleaseYear. Reissues of the same album therefore share Year, so the year disambiguator could not tell them apart and they fell through to the album id suffix. Prefer ReleaseYear and fall back to Year when it is not set.
Found by downloading an artist zip from a live server built from this branch.
* fix(archiver): let one clashing album keep the plain folder name
A disambiguator was only accepted when every clashing album had a non-empty value, so an original and its deluxe edition (with the version not appended to the name) fell through to the album id suffix. Accept a field whose values are distinct across the group even when one of them is empty, as beets' %aunique{} does: that album keeps the plain name, which the suffixed folders cannot clash with. Two or more empty values still count as a tie.
* test(archiver): refactor tests for album naming conventions and query order
2026-09-25 16:19:30 -04:00
|
|
|
"github.com/navidrome/navidrome/conf"
|
|
|
|
|
"github.com/navidrome/navidrome/conf/configtest"
|
2023-03-26 19:34:12 -04:00
|
|
|
"github.com/navidrome/navidrome/core"
|
feat(archiver): add folder cover image to downloaded zips (#6224)
* feat(archiver): add folder cover image to downloaded zips
Album, artist, playlist and share downloads now include the item's cover as
folder.<ext>, the image most players and car stereos show for the files next to
it. This helps users who copy transcoded downloads to offline devices, since
transcoding drops the embedded artwork (#5841).
Album folders get the album cover, the artist zip root gets the artist image,
and playlist and share zips get the playlist or shared item's cover at the root.
The image is the same one getCoverArt serves, resized to 500px (not square),
and named by its detected type. Items without artwork get no image, and a cover
that fails to load is logged and skipped so it never breaks the archive.
The archiver reads covers through a new core.CoverArtReader interface,
implemented by artwork.CoverArtReader, to avoid an import cycle between core
and core/artwork.
* refactor(archiver): read covers through artwork.Artwork directly
The archiver no longer needs a local CoverArtReader interface and adapter.
The only reason core/artwork imported core was a core.AbsolutePath call in
loadArtistFolder, which now reads the library path from the repository and
cleans it the same way. With the import cycle gone, the archiver takes
artwork.Artwork and treats ErrUnavailable and ErrNotFound as no cover.
Covers are now written after each album's tracks (and after all tracks for
the archive root), so a slow artwork lookup does not delay the first bytes
of the download.
* fix(archiver): read share covers as admin so private playlists keep theirs
Public share downloads run with an anonymous context, and the playlist
repository hides private playlists from anonymous users, so a zip of a shared
private playlist silently had no folder image. Like the public image handler,
the share itself is the authorization: the cover lookup now runs with an admin
user. Only the cover read is elevated; streaming keeps the anonymous context,
so the transcode limiter still keys public downloads the same way.
* style(archiver): trim comments
Shorten the comments added by the folder cover image change and drop the ones the names already explain.
* fix(archiver): add one cover per album folder in artist zips
Albums with the same name share a zip folder (pre-existing naming), so an
artist zip with two such albums wrote two folder.<ext> entries at the same
path. Keep the first cover and skip the rest for that folder.
2026-09-25 16:42:49 -04:00
|
|
|
"github.com/navidrome/navidrome/core/artwork"
|
2026-03-09 22:22:58 -04:00
|
|
|
"github.com/navidrome/navidrome/core/stream"
|
2023-03-26 19:34:12 -04:00
|
|
|
"github.com/navidrome/navidrome/model"
|
feat(archiver): add folder cover image to downloaded zips (#6224)
* feat(archiver): add folder cover image to downloaded zips
Album, artist, playlist and share downloads now include the item's cover as
folder.<ext>, the image most players and car stereos show for the files next to
it. This helps users who copy transcoded downloads to offline devices, since
transcoding drops the embedded artwork (#5841).
Album folders get the album cover, the artist zip root gets the artist image,
and playlist and share zips get the playlist or shared item's cover at the root.
The image is the same one getCoverArt serves, resized to 500px (not square),
and named by its detected type. Items without artwork get no image, and a cover
that fails to load is logged and skipped so it never breaks the archive.
The archiver reads covers through a new core.CoverArtReader interface,
implemented by artwork.CoverArtReader, to avoid an import cycle between core
and core/artwork.
* refactor(archiver): read covers through artwork.Artwork directly
The archiver no longer needs a local CoverArtReader interface and adapter.
The only reason core/artwork imported core was a core.AbsolutePath call in
loadArtistFolder, which now reads the library path from the repository and
cleans it the same way. With the import cycle gone, the archiver takes
artwork.Artwork and treats ErrUnavailable and ErrNotFound as no cover.
Covers are now written after each album's tracks (and after all tracks for
the archive root), so a slow artwork lookup does not delay the first bytes
of the download.
* fix(archiver): read share covers as admin so private playlists keep theirs
Public share downloads run with an anonymous context, and the playlist
repository hides private playlists from anonymous users, so a zip of a shared
private playlist silently had no folder image. Like the public image handler,
the share itself is the authorization: the cover lookup now runs with an admin
user. Only the cover read is elevated; streaming keeps the anonymous context,
so the transcode limiter still keys public downloads the same way.
* style(archiver): trim comments
Shorten the comments added by the folder cover image change and drop the ones the names already explain.
* fix(archiver): add one cover per album folder in artist zips
Albums with the same name share a zip folder (pre-existing naming), so an
artist zip with two such albums wrote two folder.<ext> entries at the same
path. Keep the first cover and skip the rest for that folder.
2026-09-25 16:42:49 -04:00
|
|
|
"github.com/navidrome/navidrome/model/request"
|
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.
2026-08-12 11:59:56 -04:00
|
|
|
"github.com/navidrome/navidrome/persistence"
|
2023-03-26 19:34:12 -04:00
|
|
|
. "github.com/onsi/ginkgo/v2"
|
|
|
|
|
. "github.com/onsi/gomega"
|
|
|
|
|
"github.com/stretchr/testify/mock"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
var _ = Describe("Archiver", func() {
|
|
|
|
|
var (
|
|
|
|
|
arch core.Archiver
|
|
|
|
|
ms *mockMediaStreamer
|
fix(share): reuse cached transcodes for share streams and zip downloads (#6262)
* fix(share): reuse cached transcodes when streaming from share links
Public share streams built the stream request with only the share's format
and bit rate, leaving sample rate, bit depth and channels at zero. Regular
playback resolves those through the transcode decider (e.g. 48000 Hz for
Opus), and they are part of the transcoding cache key, so a track already
transcoded during normal playback was transcoded again into a separate,
identical cache entry when played through a share link.
The public router now resolves share stream requests with the same
TranscodeDecider.ResolveRequest used by the Subsonic stream endpoint, so
both paths produce the same request and share cache entries.
Fixes #6261
* fix(archiver): reuse cached transcodes when zipping downloads
Zip downloads (album, artist, playlist and share) built the stream request
with only the format and bit rate, leaving sample rate, bit depth and
channels at zero. Those are part of the transcoding cache key, so a track
already transcoded for playback was transcoded again into a separate cache
entry when downloaded in a zip, and vice versa.
The archiver now resolves each request with TranscodeDecider.ResolveRequest,
the same as single-song downloads and streams. This also applies the
decider's defaults, so a zip requested without a bit rate uses the target
format's default bit rate instead of leaving it to ffmpeg.
* fix(archiver): name zip entries after the resolved transcoding format
The transcode decider can pick a different format than the one requested
(for example a player's forced transcoding, or a fallback to the default
downsampling format when the requested one can't be produced). Zip entry
names and the playlist M3U were still built from the requested format, so
an entry could end in .mp3 or .flac while holding Opus data.
Each track's request is now resolved before its entry name is built, and
the name uses the resolved format.
2026-10-03 08:58:54 -07:00
|
|
|
dc *fakeDecider
|
2023-03-26 19:34:12 -04:00
|
|
|
ds *mockDataStore
|
|
|
|
|
sh *mockShare
|
feat(archiver): add folder cover image to downloaded zips (#6224)
* feat(archiver): add folder cover image to downloaded zips
Album, artist, playlist and share downloads now include the item's cover as
folder.<ext>, the image most players and car stereos show for the files next to
it. This helps users who copy transcoded downloads to offline devices, since
transcoding drops the embedded artwork (#5841).
Album folders get the album cover, the artist zip root gets the artist image,
and playlist and share zips get the playlist or shared item's cover at the root.
The image is the same one getCoverArt serves, resized to 500px (not square),
and named by its detected type. Items without artwork get no image, and a cover
that fails to load is logged and skipped so it never breaks the archive.
The archiver reads covers through a new core.CoverArtReader interface,
implemented by artwork.CoverArtReader, to avoid an import cycle between core
and core/artwork.
* refactor(archiver): read covers through artwork.Artwork directly
The archiver no longer needs a local CoverArtReader interface and adapter.
The only reason core/artwork imported core was a core.AbsolutePath call in
loadArtistFolder, which now reads the library path from the repository and
cleans it the same way. With the import cycle gone, the archiver takes
artwork.Artwork and treats ErrUnavailable and ErrNotFound as no cover.
Covers are now written after each album's tracks (and after all tracks for
the archive root), so a slow artwork lookup does not delay the first bytes
of the download.
* fix(archiver): read share covers as admin so private playlists keep theirs
Public share downloads run with an anonymous context, and the playlist
repository hides private playlists from anonymous users, so a zip of a shared
private playlist silently had no folder image. Like the public image handler,
the share itself is the authorization: the cover lookup now runs with an admin
user. Only the cover read is elevated; streaming keeps the anonymous context,
so the transcode limiter still keys public downloads the same way.
* style(archiver): trim comments
Shorten the comments added by the folder cover image change and drop the ones the names already explain.
* fix(archiver): add one cover per album folder in artist zips
Albums with the same name share a zip folder (pre-existing naming), so an
artist zip with two such albums wrote two folder.<ext> entries at the same
path. Keep the first cover and skip the rest for that folder.
2026-09-25 16:42:49 -04:00
|
|
|
ca *mockCoverArt
|
2023-03-26 19:34:12 -04:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
BeforeEach(func() {
|
|
|
|
|
ms = &mockMediaStreamer{}
|
fix(share): reuse cached transcodes for share streams and zip downloads (#6262)
* fix(share): reuse cached transcodes when streaming from share links
Public share streams built the stream request with only the share's format
and bit rate, leaving sample rate, bit depth and channels at zero. Regular
playback resolves those through the transcode decider (e.g. 48000 Hz for
Opus), and they are part of the transcoding cache key, so a track already
transcoded during normal playback was transcoded again into a separate,
identical cache entry when played through a share link.
The public router now resolves share stream requests with the same
TranscodeDecider.ResolveRequest used by the Subsonic stream endpoint, so
both paths produce the same request and share cache entries.
Fixes #6261
* fix(archiver): reuse cached transcodes when zipping downloads
Zip downloads (album, artist, playlist and share) built the stream request
with only the format and bit rate, leaving sample rate, bit depth and
channels at zero. Those are part of the transcoding cache key, so a track
already transcoded for playback was transcoded again into a separate cache
entry when downloaded in a zip, and vice versa.
The archiver now resolves each request with TranscodeDecider.ResolveRequest,
the same as single-song downloads and streams. This also applies the
decider's defaults, so a zip requested without a bit rate uses the target
format's default bit rate instead of leaving it to ffmpeg.
* fix(archiver): name zip entries after the resolved transcoding format
The transcode decider can pick a different format than the one requested
(for example a player's forced transcoding, or a fallback to the default
downsampling format when the requested one can't be produced). Zip entry
names and the playlist M3U were still built from the requested format, so
an entry could end in .mp3 or .flac while holding Opus data.
Each track's request is now resolved before its entry name is built, and
the name uses the resolved format.
2026-10-03 08:58:54 -07:00
|
|
|
dc = &fakeDecider{}
|
2023-03-26 19:34:12 -04:00
|
|
|
sh = &mockShare{}
|
feat(bfr): Big Refactor: new scanner, lots of new fields and tags, improvements and DB schema changes (#2709)
* fix(server): more race conditions when updating artist/album from external sources
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(scanner): add .gitignore syntax to .ndignore. Resolves #1394
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(ui): null
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(scanner): pass configfile option to child process
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(scanner): resume interrupted fullScans
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(scanner): remove old scanner code
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(scanner): rename old metadata package
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(scanner): move old metadata package
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: tests
Signed-off-by: Deluan <deluan@navidrome.org>
* chore(deps): update Go to 1.23.4
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: logs
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(test):
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: log level
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: remove log message
Signed-off-by: Deluan <deluan@navidrome.org>
* feat: add config for scanner watcher
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: children playlists
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: replace `interface{}` with `any`
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: smart playlists with genres
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: allow any tags in smart playlists
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: artist names in playlists
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: smart playlist's sort by tags
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(subsonic): add moods to child
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(subsonic): add moods to AlbumID3
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor(subsonic): use generic JSONArray for OS arrays
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor(subsonic): use https in test
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(subsonic): add releaseTypes to AlbumID3
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(subsonic): add recordLabels to AlbumID3
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor(subsonic): rename JSONArray to Array
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(subsonic): add artists to AlbumID3
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(subsonic): add artists to Child
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(scanner): do not pre-populate smart playlists
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(subsonic): implement a simplified version of ArtistID3.
See https://github.com/opensubsonic/open-subsonic-api/discussions/120
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(subsonic): add artists to album child
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(subsonic): add contributors to mediafile Child
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(subsonic): add albumArtists to mediafile Child
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(subsonic): add displayArtist and displayAlbumArtist
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(subsonic): add displayComposer to Child
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(subsonic): add roles to ArtistID3
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(subsonic): use " • " separator for displayComposer
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor:
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(subsonic):
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(subsonic): respect `PreferSortTags` config option
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor(subsonic):
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: optimize purging non-unused tags
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: don't run 'refresh artist stats' concurrently with other transactions
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor:
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: log message
Signed-off-by: Deluan <deluan@navidrome.org>
* feat: add Scanner.ScanOnStartup config option, default true
Signed-off-by: Deluan <deluan@navidrome.org>
* feat: better json parsing error msg when importing NSPs
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: don't update album's imported_time when updating external_metadata
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: handle interrupted scans and full scans after migrations
Signed-off-by: Deluan <deluan@navidrome.org>
* feat: run `analyze` when migration requires a full rescan
Signed-off-by: Deluan <deluan@navidrome.org>
* feat: run `PRAGMA optimize` at the end of the scan
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: don't update artist's updated_at when updating external_metadata
Signed-off-by: Deluan <deluan@navidrome.org>
* feat: handle multiple artists and roles in smart playlists
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(ui): dim missing tracks
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: album missing logic
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: error encoding in gob
Signed-off-by: Deluan <deluan@navidrome.org>
* feat: separate warnings from errors
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: mark albums as missing if they were contained in a deleted folder
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: add participant names to media_file and album tables
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: use participations in criteria, instead of m2m relationship
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: rename participations to participants
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(subsonic): add moods to album child
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: albumartist role case
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(scanner): run scanner as an external process by default
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(ui): show albumArtist names
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(ui): dim out missing albums
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: flaky test
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(server): scrobble buffer mapping. fix #3583
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: more participations renaming
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: listenbrainz scrobbling
Signed-off-by: Deluan <deluan@navidrome.org>
* feat: send release_group_mbid to listenbrainz
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(subsonic): implement OpenSubsonic explicitStatus field (#3597)
* feat: implement OpenSubsonic explicitStatus field
* fix(subsonic): fix failing snapshot tests
* refactor: create helper for setting explicitStatus
* fix: store smaller values for explicit-status on database
* test: ToAlbum explicitStatus
* refactor: rename explicitStatus helper function
---------
Co-authored-by: Deluan Quintão <deluan@navidrome.org>
* fix: handle album and track tags in the DB based on the mappings.yaml file
Signed-off-by: Deluan <deluan@navidrome.org>
* save similar artists as JSONB
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: getAlbumList byGenre
Signed-off-by: Deluan <deluan@navidrome.org>
* detect changes in PID configuration
Signed-off-by: Deluan <deluan@navidrome.org>
* set default album PID to legacy_pid
Signed-off-by: Deluan <deluan@navidrome.org>
* fix tests
Signed-off-by: Deluan <deluan@navidrome.org>
* fix SIGSEGV
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: don't lose album stars/ratings when migrating
Signed-off-by: Deluan <deluan@navidrome.org>
* store full PID conf in properties
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: keep album annotations when changing PID.Album config
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: reassign album annotations
Signed-off-by: Deluan <deluan@navidrome.org>
* feat: use (display) albumArtist and add links to each artist
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: not showing albums by albumartist
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: error msgs
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: hide PID from Native API
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: album cover art resolution
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: trim participant names
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: reduce watcher log spam
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: panic when initializing the watcher
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: various artists
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: don't store empty lyrics in the DB
Signed-off-by: Deluan <deluan@navidrome.org>
* remove unused methods
Signed-off-by: Deluan <deluan@navidrome.org>
* drop full_text indexes, as they are not being used by SQLite
Signed-off-by: Deluan <deluan@navidrome.org>
* keep album created_at when upgrading
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(ui): null pointer
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: album artwork cache
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: don't expose missing files in Subsonic API
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: searchable interface
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: filter out missing items from subsonic search
* fix: filter out missing items from playlists
* fix: filter out missing items from shares
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(ui): add filter by artist role
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(subsonic): only return albumartists in getIndexes and getArtists endpoints
Signed-off-by: Deluan <deluan@navidrome.org>
* sort roles alphabetically
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: artist playcounts
Signed-off-by: Deluan <deluan@navidrome.org>
* change default Album PID conf
Signed-off-by: Deluan <deluan@navidrome.org>
* fix albumartist link when it does not match any albumartists values
Signed-off-by: Deluan <deluan@navidrome.org>
* fix `Ignoring filter not whitelisted` (role) message
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: trim any names/titles being imported
Signed-off-by: Deluan <deluan@navidrome.org>
* remove unused genre code
Signed-off-by: Deluan <deluan@navidrome.org>
* serialize calls to Last.fm's getArtist
Signed-off-by: Deluan <deluan@navidrome.org>
xxx
Signed-off-by: Deluan <deluan@navidrome.org>
* add counters to genres
Signed-off-by: Deluan <deluan@navidrome.org>
* nit: fix migration `notice` message
Signed-off-by: Deluan <deluan@navidrome.org>
* optimize similar artists query
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: last.fm.getInfo when mbid does not exist
Signed-off-by: Deluan <deluan@navidrome.org>
* ui only show missing items for admins
Signed-off-by: Deluan <deluan@navidrome.org>
* don't allow interaction with missing items
Signed-off-by: Deluan <deluan@navidrome.org>
* Add Missing Files view (WIP)
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: merged tag_counts into tag table
Signed-off-by: Deluan <deluan@navidrome.org>
* add option to completely disable automatic scanner
Signed-off-by: Deluan <deluan@navidrome.org>
* add delete missing files functionality
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: playlists not showing for regular users
Signed-off-by: Deluan <deluan@navidrome.org>
* reduce updateLastAccess frequency to once every minute
Signed-off-by: Deluan <deluan@navidrome.org>
* reduce update player frequency to once every minute
Signed-off-by: Deluan <deluan@navidrome.org>
* add timeout when updating player
Signed-off-by: Deluan <deluan@navidrome.org>
* remove dead code
Signed-off-by: Deluan <deluan@navidrome.org>
* fix duplicated roles in stats
Signed-off-by: Deluan <deluan@navidrome.org>
* add `; ` to artist splitters
Signed-off-by: Deluan <deluan@navidrome.org>
* fix stats query
Signed-off-by: Deluan <deluan@navidrome.org>
* more logs
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: support legacy clients (DSub) by removing OpenSubsonic extra fields - WIP
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: support legacy clients (DSub) by removing OpenSubsonic extra fields - WIP
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: support legacy clients (DSub) by removing OpenSubsonic extra fields - WIP
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: support legacy clients (DSub) by removing OpenSubsonic extra fields - WIP
Signed-off-by: Deluan <deluan@navidrome.org>
* add record label filter
Signed-off-by: Deluan <deluan@navidrome.org>
* add release type filter
Signed-off-by: Deluan <deluan@navidrome.org>
* fix purgeUnused tags
Signed-off-by: Deluan <deluan@navidrome.org>
* add grouping filter to albums
Signed-off-by: Deluan <deluan@navidrome.org>
* allow any album tags to be used in as filters in the API
Signed-off-by: Deluan <deluan@navidrome.org>
* remove empty tags from album info
Signed-off-by: Deluan <deluan@navidrome.org>
* comments in the migration
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: Cannot read properties of undefined
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: listenbrainz scrobbling (#3640)
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: remove duplicated tag values
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: don't ignore the taglib folder!
Signed-off-by: Deluan <deluan@navidrome.org>
* feat: show track subtitle tag
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: show artists stats based on selected role
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: inspect
Signed-off-by: Deluan <deluan@navidrome.org>
* add media type to album info/filters
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: change format of subtitle in the UI
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: subtitle in Subsonic API and search
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: subtitle in UI's player
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: split strings should be case-insensitive
Signed-off-by: Deluan <deluan@navidrome.org>
* disable ScanSchedule
Signed-off-by: Deluan <deluan@navidrome.org>
* increase default sessiontimeout
Signed-off-by: Deluan <deluan@navidrome.org>
* add sqlite command line tool to docker image
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: resources override
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: album PID conf
Signed-off-by: Deluan <deluan@navidrome.org>
* change migration to mark current artists as albumArtists
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(ui): Allow filtering on multiple genres (#3679)
* feat(ui): Allow filtering on multiple genres
Signed-off-by: Henrik Nordvik <henrikno@gmail.com>
Signed-off-by: Deluan <deluan@navidrome.org>
* add multi-genre filter in Album list
Signed-off-by: Deluan <deluan@navidrome.org>
---------
Signed-off-by: Henrik Nordvik <henrikno@gmail.com>
Signed-off-by: Deluan <deluan@navidrome.org>
Co-authored-by: Henrik Nordvik <henrikno@gmail.com>
* add more multi-valued tag filters to Album and Song views
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(ui): unselect missing files after removing
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(ui): song filter
Signed-off-by: Deluan <deluan@navidrome.org>
* fix sharing tracks. fix #3687
Signed-off-by: Deluan <deluan@navidrome.org>
* use rowids when using search for sync (ex: Symfonium)
Signed-off-by: Deluan <deluan@navidrome.org>
* fix "Report Real Paths" option for subsonic clients
Signed-off-by: Deluan <deluan@navidrome.org>
* fix "Report Real Paths" option for subsonic clients for search
Signed-off-by: Deluan <deluan@navidrome.org>
* add libraryPath to Native API /songs endpoint
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(subsonic): add album version
Signed-off-by: Deluan <deluan@navidrome.org>
* made all tags lowercase as they are case-insensitive anyways.
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(ui): Show full paths, extended properties for album/song (#3691)
* feat(ui): Show full paths, extended properties for album/song
- uses library path + os separator + path
- show participants (album/song) and tags (song)
- make album/participant clickable in show info
* add source to path
* fix pathSeparator in UI
Signed-off-by: Deluan <deluan@navidrome.org>
* fix local artist artwork (#3695)
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: parse vorbis performers
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: clean function into smaller functions
Signed-off-by: Deluan <deluan@navidrome.org>
* fix translations for en and pt
Signed-off-by: Deluan <deluan@navidrome.org>
* add trace log to show annotations reassignment
Signed-off-by: Deluan <deluan@navidrome.org>
* add trace log to show annotations reassignment
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: allow performers without instrument/subrole
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: metadata clean function again
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: optimize split function
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: split function is now a method of TagConf
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: humanize Artist total size
Signed-off-by: Deluan <deluan@navidrome.org>
* add album version to album details
Signed-off-by: Deluan <deluan@navidrome.org>
* don't display album-level tags in SongInfo
Signed-off-by: Deluan <deluan@navidrome.org>
* fix genre clicking in Album Page
Signed-off-by: Deluan <deluan@navidrome.org>
* don't use mbids in Last.fm api calls.
From https://discord.com/channels/671335427726114836/704303730660737113/1337574018143879248:
With MBID:
```
GET https://ws.audioscrobbler.com/2.0/?api_key=XXXX&artist=Van+Morrison&format=json&lang=en&mbid=a41ac10f-0a56-4672-9161-b83f9b223559&method=artist.getInfo
{
artist: {
name: "Bee Gees",
mbid: "bf0f7e29-dfe1-416c-b5c6-f9ebc19ea810",
url: "https://www.last.fm/music/Bee+Gees",
}
```
Without MBID:
```
GET https://ws.audioscrobbler.com/2.0/?api_key=XXXX&artist=Van+Morrison&format=json&lang=en&method=artist.getInfo
{
artist: {
name: "Van Morrison",
mbid: "a41ac10f-0a56-4672-9161-b83f9b223559",
url: "https://www.last.fm/music/Van+Morrison",
}
```
Signed-off-by: Deluan <deluan@navidrome.org>
* better logging for when the artist folder is not found
Signed-off-by: Deluan <deluan@navidrome.org>
* fix various issues with artist image resolution
Signed-off-by: Deluan <deluan@navidrome.org>
* hide "Additional Tags" header if there are none.
Signed-off-by: Deluan <deluan@navidrome.org>
* simplify tag rendering
Signed-off-by: Deluan <deluan@navidrome.org>
* enhance logging for artist folder detection
Signed-off-by: Deluan <deluan@navidrome.org>
* make folderID consistent for relative and absolute folderPaths
Signed-off-by: Deluan <deluan@navidrome.org>
* handle more folder paths scenarios
Signed-off-by: Deluan <deluan@navidrome.org>
* filter out other roles when SubsonicArtistParticipations = true
Signed-off-by: Deluan <deluan@navidrome.org>
* fix "Cannot read properties of undefined"
Signed-off-by: Deluan <deluan@navidrome.org>
* fix lyrics and comments being truncated (#3701)
* fix lyrics and comments being truncated
* specifically test for lyrics and comment length
* reorder assertions
Signed-off-by: Deluan <deluan@navidrome.org>
---------
Signed-off-by: Deluan <deluan@navidrome.org>
Co-authored-by: Deluan <deluan@navidrome.org>
* fix(server): Expose library_path for playlist (#3705)
Allows showing absolute path for UI, and makes "report real path" work for playlists (Subsonic)
* fix BFR on Windows (#3704)
* fix potential reflected cross-site scripting vulnerability
Signed-off-by: Deluan <deluan@navidrome.org>
* hack to make it work on Windows
* ignore windows executables
* try fixing the pipeline
Signed-off-by: Deluan <deluan@navidrome.org>
* allow MusicFolder in other drives
* move windows local drive logic to local storage implementation
---------
Signed-off-by: Deluan <deluan@navidrome.org>
* increase pagination sizes for missing files
Signed-off-by: Deluan <deluan@navidrome.org>
* reduce level of "already scanning" watcher log message
Signed-off-by: Deluan <deluan@navidrome.org>
* only count folders with audio files in it
See https://github.com/navidrome/navidrome/discussions/3676#discussioncomment-11990930
Signed-off-by: Deluan <deluan@navidrome.org>
* add album version and catalog number to search
Signed-off-by: Deluan <deluan@navidrome.org>
* add `organization` alias for `recordlabel`
Signed-off-by: Deluan <deluan@navidrome.org>
* remove mbid from Last.fm agent
Signed-off-by: Deluan <deluan@navidrome.org>
* feat: support inspect in ui (#3726)
* inspect in ui
* address round 1
* add catalogNum to AlbumInfo
Signed-off-by: Deluan <deluan@navidrome.org>
* remove dependency on metadata_old (deprecated) package
Signed-off-by: Deluan <deluan@navidrome.org>
* add `RawTags` to model
Signed-off-by: Deluan <deluan@navidrome.org>
* support parsing MBIDs for roles (from the https://github.com/kgarner7/picard-all-mbids plugin) (#3698)
* parse standard roles, vorbis/m4a work for now
* fix djmixer
* working roles, use DJ-mix
* add performers to file
* map mbids
* add a few more tests
* add test
Signed-off-by: Deluan <deluan@navidrome.org>
* try to simplify the performers logic
Signed-off-by: Deluan <deluan@navidrome.org>
* stylistic changes
---------
Signed-off-by: Deluan <deluan@navidrome.org>
Co-authored-by: Deluan <deluan@navidrome.org>
* remove param mutation
Signed-off-by: Deluan <deluan@navidrome.org>
* run automated SQLite optimizations
Signed-off-by: Deluan <deluan@navidrome.org>
* fix playlists import/export on Windows
* fix import playlists
* fix export playlists
* better handling of Windows volumes
Signed-off-by: Deluan <deluan@navidrome.org>
* handle more album ID reassignments
Signed-off-by: Deluan <deluan@navidrome.org>
* allow adding/overriding tags in the config file
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(ui): Fix playlist track id, handle missing tracks better (#3734)
- Use `mediaFileId` instead of `id` for playlist tracks
- Only fetch if the file is not missing
- If extractor fails to get the file, also error (rather than panic)
* optimize DB after each scan.
Signed-off-by: Deluan <deluan@navidrome.org>
* remove sortable from AlbumSongs columns
Signed-off-by: Deluan <deluan@navidrome.org>
* simplify query to get missing tracks
Signed-off-by: Deluan <deluan@navidrome.org>
* mark Scanner.Extractor as deprecated
Signed-off-by: Deluan <deluan@navidrome.org>
---------
Signed-off-by: Deluan <deluan@navidrome.org>
Signed-off-by: Henrik Nordvik <henrikno@gmail.com>
Co-authored-by: Caio Cotts <caio@cotts.com.br>
Co-authored-by: Henrik Nordvik <henrikno@gmail.com>
Co-authored-by: Kendall Garner <17521368+kgarner7@users.noreply.github.com>
2025-02-19 17:35:17 -08:00
|
|
|
ds = &mockDataStore{}
|
feat(archiver): add folder cover image to downloaded zips (#6224)
* feat(archiver): add folder cover image to downloaded zips
Album, artist, playlist and share downloads now include the item's cover as
folder.<ext>, the image most players and car stereos show for the files next to
it. This helps users who copy transcoded downloads to offline devices, since
transcoding drops the embedded artwork (#5841).
Album folders get the album cover, the artist zip root gets the artist image,
and playlist and share zips get the playlist or shared item's cover at the root.
The image is the same one getCoverArt serves, resized to 500px (not square),
and named by its detected type. Items without artwork get no image, and a cover
that fails to load is logged and skipped so it never breaks the archive.
The archiver reads covers through a new core.CoverArtReader interface,
implemented by artwork.CoverArtReader, to avoid an import cycle between core
and core/artwork.
* refactor(archiver): read covers through artwork.Artwork directly
The archiver no longer needs a local CoverArtReader interface and adapter.
The only reason core/artwork imported core was a core.AbsolutePath call in
loadArtistFolder, which now reads the library path from the repository and
cleans it the same way. With the import cycle gone, the archiver takes
artwork.Artwork and treats ErrUnavailable and ErrNotFound as no cover.
Covers are now written after each album's tracks (and after all tracks for
the archive root), so a slow artwork lookup does not delay the first bytes
of the download.
* fix(archiver): read share covers as admin so private playlists keep theirs
Public share downloads run with an anonymous context, and the playlist
repository hides private playlists from anonymous users, so a zip of a shared
private playlist silently had no folder image. Like the public image handler,
the share itself is the authorization: the cover lookup now runs with an admin
user. Only the cover read is elevated; streaming keeps the anonymous context,
so the transcode limiter still keys public downloads the same way.
* style(archiver): trim comments
Shorten the comments added by the folder cover image change and drop the ones the names already explain.
* fix(archiver): add one cover per album folder in artist zips
Albums with the same name share a zip folder (pre-existing naming), so an
artist zip with two such albums wrote two folder.<ext> entries at the same
path. Keep the first cover and skip the rest for that folder.
2026-09-25 16:42:49 -04:00
|
|
|
ca = &mockCoverArt{images: map[string][]byte{}}
|
fix(share): reuse cached transcodes for share streams and zip downloads (#6262)
* fix(share): reuse cached transcodes when streaming from share links
Public share streams built the stream request with only the share's format
and bit rate, leaving sample rate, bit depth and channels at zero. Regular
playback resolves those through the transcode decider (e.g. 48000 Hz for
Opus), and they are part of the transcoding cache key, so a track already
transcoded during normal playback was transcoded again into a separate,
identical cache entry when played through a share link.
The public router now resolves share stream requests with the same
TranscodeDecider.ResolveRequest used by the Subsonic stream endpoint, so
both paths produce the same request and share cache entries.
Fixes #6261
* fix(archiver): reuse cached transcodes when zipping downloads
Zip downloads (album, artist, playlist and share) built the stream request
with only the format and bit rate, leaving sample rate, bit depth and
channels at zero. Those are part of the transcoding cache key, so a track
already transcoded for playback was transcoded again into a separate cache
entry when downloaded in a zip, and vice versa.
The archiver now resolves each request with TranscodeDecider.ResolveRequest,
the same as single-song downloads and streams. This also applies the
decider's defaults, so a zip requested without a bit rate uses the target
format's default bit rate instead of leaving it to ffmpeg.
* fix(archiver): name zip entries after the resolved transcoding format
The transcode decider can pick a different format than the one requested
(for example a player's forced transcoding, or a fallback to the default
downsampling format when the requested one can't be produced). Zip entry
names and the playlist M3U were still built from the requested format, so
an entry could end in .mp3 or .flac while holding Opus data.
Each track's request is now resolved before its entry name is built, and
the name uses the resolved format.
2026-10-03 08:58:54 -07:00
|
|
|
arch = core.NewArchiver(ms, dc, ds, sh, ca)
|
2023-03-26 19:34:12 -04:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
Context("ZipAlbum", func() {
|
|
|
|
|
It("zips an album correctly", func() {
|
|
|
|
|
mfs := model.MediaFiles{
|
2023-05-16 18:34:15 -04:00
|
|
|
{Path: "test_data/01 - track1.mp3", Suffix: "mp3", AlbumID: "1", Album: "Album/Promo", DiscNumber: 1},
|
|
|
|
|
{Path: "test_data/02 - track2.mp3", Suffix: "mp3", AlbumID: "1", Album: "Album/Promo", DiscNumber: 1},
|
2023-03-26 19:34:12 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
mfRepo := &mockMediaFileRepository{}
|
|
|
|
|
mfRepo.On("GetAll", []model.QueryOptions{{
|
|
|
|
|
Filters: squirrel.Eq{"album_id": "1"},
|
|
|
|
|
Sort: "album",
|
|
|
|
|
}}).Return(mfs, nil)
|
|
|
|
|
|
2026-09-25 18:06:10 -04:00
|
|
|
ds.On("MediaFile").Return(mfRepo)
|
2026-03-09 22:22:58 -04:00
|
|
|
ms.On("NewStream", mock.Anything, mock.Anything, stream.Request{Format: "mp3", BitRate: 128}).Return(io.NopCloser(strings.NewReader("test")), nil).Times(3)
|
2023-03-26 19:34:12 -04:00
|
|
|
|
|
|
|
|
out := new(bytes.Buffer)
|
|
|
|
|
err := arch.ZipAlbum(context.Background(), "1", "mp3", 128, out)
|
|
|
|
|
Expect(err).To(BeNil())
|
|
|
|
|
|
|
|
|
|
zr, err := zip.NewReader(bytes.NewReader(out.Bytes()), int64(out.Len()))
|
|
|
|
|
Expect(err).To(BeNil())
|
|
|
|
|
|
|
|
|
|
Expect(len(zr.File)).To(Equal(2))
|
2023-05-16 18:34:15 -04:00
|
|
|
Expect(zr.File[0].Name).To(Equal("Album_Promo/01 - track1.mp3"))
|
|
|
|
|
Expect(zr.File[1].Name).To(Equal("Album_Promo/02 - track2.mp3"))
|
2023-03-26 19:34:12 -04:00
|
|
|
})
|
fix(share): reuse cached transcodes for share streams and zip downloads (#6262)
* fix(share): reuse cached transcodes when streaming from share links
Public share streams built the stream request with only the share's format
and bit rate, leaving sample rate, bit depth and channels at zero. Regular
playback resolves those through the transcode decider (e.g. 48000 Hz for
Opus), and they are part of the transcoding cache key, so a track already
transcoded during normal playback was transcoded again into a separate,
identical cache entry when played through a share link.
The public router now resolves share stream requests with the same
TranscodeDecider.ResolveRequest used by the Subsonic stream endpoint, so
both paths produce the same request and share cache entries.
Fixes #6261
* fix(archiver): reuse cached transcodes when zipping downloads
Zip downloads (album, artist, playlist and share) built the stream request
with only the format and bit rate, leaving sample rate, bit depth and
channels at zero. Those are part of the transcoding cache key, so a track
already transcoded for playback was transcoded again into a separate cache
entry when downloaded in a zip, and vice versa.
The archiver now resolves each request with TranscodeDecider.ResolveRequest,
the same as single-song downloads and streams. This also applies the
decider's defaults, so a zip requested without a bit rate uses the target
format's default bit rate instead of leaving it to ffmpeg.
* fix(archiver): name zip entries after the resolved transcoding format
The transcode decider can pick a different format than the one requested
(for example a player's forced transcoding, or a fallback to the default
downsampling format when the requested one can't be produced). Zip entry
names and the playlist M3U were still built from the requested format, so
an entry could end in .mp3 or .flac while holding Opus data.
Each track's request is now resolved before its entry name is built, and
the name uses the resolved format.
2026-10-03 08:58:54 -07:00
|
|
|
|
|
|
|
|
It("streams the request resolved by the transcode decider and names the entry after its format", func() {
|
|
|
|
|
mfRepo := &mockMediaFileRepository{}
|
|
|
|
|
mfRepo.On("GetAll", mock.Anything).Return(model.MediaFiles{{Path: "test_data/01 - track1.flac", Suffix: "flac", AlbumID: "1"}}, nil)
|
|
|
|
|
ds.On("MediaFile").Return(mfRepo)
|
|
|
|
|
resolved := stream.Request{Format: "opus", BitRate: 128, SampleRate: 48000, Channels: 2}
|
|
|
|
|
dc.resolved = &resolved
|
|
|
|
|
ms.On("NewStream", mock.Anything, mock.Anything, resolved).Return(io.NopCloser(strings.NewReader("test")), nil).Once()
|
|
|
|
|
|
|
|
|
|
out := new(bytes.Buffer)
|
|
|
|
|
Expect(arch.ZipAlbum(GinkgoT().Context(), "1", "mp3", 128, out)).To(Succeed())
|
|
|
|
|
ms.AssertExpectations(GinkgoT())
|
|
|
|
|
|
|
|
|
|
zr, err := zip.NewReader(bytes.NewReader(out.Bytes()), int64(out.Len()))
|
|
|
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
|
Expect(zr.File[0].Name).To(HaveSuffix("01 - track1.opus"))
|
|
|
|
|
})
|
2023-03-26 19:34:12 -04:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
Context("ZipArtist", func() {
|
|
|
|
|
It("zips an artist's albums correctly", func() {
|
|
|
|
|
mfs := model.MediaFiles{
|
|
|
|
|
{Path: "test_data/01 - track1.mp3", Suffix: "mp3", AlbumArtistID: "1", AlbumID: "1", Album: "Album 1", DiscNumber: 1},
|
|
|
|
|
{Path: "test_data/02 - track2.mp3", Suffix: "mp3", AlbumArtistID: "1", AlbumID: "1", Album: "Album 1", DiscNumber: 1},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
mfRepo := &mockMediaFileRepository{}
|
|
|
|
|
mfRepo.On("GetAll", []model.QueryOptions{{
|
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.
2026-08-12 11:59:56 -04:00
|
|
|
Filters: squirrel.And{
|
|
|
|
|
persistence.ParticipantIDFilter("media_file", "1", model.RoleAlbumArtist),
|
|
|
|
|
squirrel.Eq{"missing": false},
|
|
|
|
|
},
|
|
|
|
|
Sort: "album",
|
2023-03-26 19:34:12 -04:00
|
|
|
}}).Return(mfs, nil)
|
|
|
|
|
|
2026-09-25 18:06:10 -04:00
|
|
|
ds.On("MediaFile").Return(mfRepo)
|
2026-03-09 22:22:58 -04:00
|
|
|
ms.On("NewStream", mock.Anything, mock.Anything, stream.Request{Format: "mp3", BitRate: 128}).Return(io.NopCloser(strings.NewReader("test")), nil).Times(2)
|
2023-03-26 19:34:12 -04:00
|
|
|
|
|
|
|
|
out := new(bytes.Buffer)
|
|
|
|
|
err := arch.ZipArtist(context.Background(), "1", "mp3", 128, out)
|
|
|
|
|
Expect(err).To(BeNil())
|
|
|
|
|
|
|
|
|
|
zr, err := zip.NewReader(bytes.NewReader(out.Bytes()), int64(out.Len()))
|
|
|
|
|
Expect(err).To(BeNil())
|
|
|
|
|
|
|
|
|
|
Expect(len(zr.File)).To(Equal(2))
|
|
|
|
|
Expect(zr.File[0].Name).To(Equal("Album 1/01 - track1.mp3"))
|
|
|
|
|
Expect(zr.File[1].Name).To(Equal("Album 1/02 - track2.mp3"))
|
|
|
|
|
})
|
fix(archiver): give same-named albums their own folder in artist zips (#6225)
* refactor: add Tags.First and slice.GroupOrdered helpers
Tags.First returns the first value of a tag or an empty string, replacing the inline len-check-then-index pattern in FullTitle, FullAlbumName, Album.FullName and the Subsonic album version mapping.
slice.GroupOrdered is slice.Group returning the groups in first-seen order, for callers that need a deterministic order the map-based Group cannot give.
* fix(archiver): give same-named albums their own folder in artist zips
Artist zips put every album in a folder named after the album, so two albums with the same name (an original and a deluxe edition, or names that only differ in characters the sanitizer replaces) were merged into one folder, with tracks mixed together and duplicate zip entries when file names collided.
The folder is now named after FullAlbumName(), so with AppendAlbumVersion on (the default) the version is part of the name, matching what clients display. Albums whose sanitized names still clash get a " [suffix]" taken from the first field that has a distinct, non-empty value for all of them: album version, year, release type, record label, catalog number, then a short album id. This follows the shape of beets' %aunique{} path function.
Albums are also grouped with slice.GroupOrdered instead of a map, so the zip is deterministic.
* fix(archiver): use the release year to tell same-named albums apart
Taggers often write an edition's date to the Date tag next to an original date, and the scanner then stores the original year in Year and the edition's year in ReleaseYear. Reissues of the same album therefore share Year, so the year disambiguator could not tell them apart and they fell through to the album id suffix. Prefer ReleaseYear and fall back to Year when it is not set.
Found by downloading an artist zip from a live server built from this branch.
* fix(archiver): let one clashing album keep the plain folder name
A disambiguator was only accepted when every clashing album had a non-empty value, so an original and its deluxe edition (with the version not appended to the name) fell through to the album id suffix. Accept a field whose values are distinct across the group even when one of them is empty, as beets' %aunique{} does: that album keeps the plain name, which the suffixed folders cannot clash with. Two or more empty values still count as a tie.
* test(archiver): refactor tests for album naming conventions and query order
2026-09-25 16:19:30 -04:00
|
|
|
|
|
|
|
|
When("albums that share a name", func() {
|
|
|
|
|
BeforeEach(func() {
|
|
|
|
|
DeferCleanup(configtest.SetupConfig())
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// zipArtistEntries zips the given tracks as artist "1" and returns the entry names in zip order.
|
|
|
|
|
zipArtistEntries := func(mfs model.MediaFiles) []string {
|
|
|
|
|
mfRepo := &mockMediaFileRepository{}
|
|
|
|
|
mfRepo.On("GetAll", mock.Anything).Return(mfs, nil)
|
|
|
|
|
ds.On("MediaFile", mock.Anything).Return(mfRepo)
|
|
|
|
|
ms.On("NewStream", mock.Anything, mock.Anything, mock.Anything).Return(io.NopCloser(strings.NewReader("test")), nil)
|
|
|
|
|
|
|
|
|
|
out := new(bytes.Buffer)
|
|
|
|
|
Expect(arch.ZipArtist(context.Background(), "1", "mp3", 128, out)).To(Succeed())
|
|
|
|
|
zr, err := zip.NewReader(bytes.NewReader(out.Bytes()), int64(out.Len()))
|
|
|
|
|
Expect(err).To(BeNil())
|
|
|
|
|
names := make([]string, len(zr.File))
|
|
|
|
|
for i, f := range zr.File {
|
|
|
|
|
names[i] = f.Name
|
|
|
|
|
}
|
|
|
|
|
return names
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It("keeps the albums in query order", func() {
|
|
|
|
|
names := zipArtistEntries(model.MediaFiles{
|
|
|
|
|
{Path: "a/01.mp3", Suffix: "mp3", AlbumID: "3", Album: "Album C"},
|
|
|
|
|
{Path: "a/01.mp3", Suffix: "mp3", AlbumID: "1", Album: "Album A"},
|
|
|
|
|
{Path: "a/02.mp3", Suffix: "mp3", AlbumID: "1", Album: "Album A"},
|
|
|
|
|
{Path: "a/01.mp3", Suffix: "mp3", AlbumID: "2", Album: "Album B"},
|
|
|
|
|
})
|
|
|
|
|
Expect(names).To(Equal([]string{"Album C/01.mp3", "Album A/01.mp3", "Album A/02.mp3", "Album B/01.mp3"}))
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
It("suffixes the year when it tells the albums apart", func() {
|
|
|
|
|
names := zipArtistEntries(model.MediaFiles{
|
|
|
|
|
{Path: "a/01 - Intro.mp3", Suffix: "mp3", AlbumID: "1", Album: "Greatest Hits", Year: 2001},
|
|
|
|
|
{Path: "b/01 - Intro.mp3", Suffix: "mp3", AlbumID: "2", Album: "Greatest Hits", Year: 2005},
|
|
|
|
|
})
|
|
|
|
|
Expect(names).To(Equal([]string{"Greatest Hits [2001]/01 - Intro.mp3", "Greatest Hits [2005]/01 - Intro.mp3"}))
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
It("prefers the release year, so reissues of the same original are told apart", func() {
|
|
|
|
|
names := zipArtistEntries(model.MediaFiles{
|
|
|
|
|
{Path: "a/01.mp3", Suffix: "mp3", AlbumID: "1", Album: "Greatest Hits", Year: 1996, ReleaseYear: 2001},
|
|
|
|
|
{Path: "b/01.mp3", Suffix: "mp3", AlbumID: "2", Album: "Greatest Hits", Year: 1996, ReleaseYear: 2011},
|
|
|
|
|
{Path: "c/01.mp3", Suffix: "mp3", AlbumID: "3", Album: "Greatest Hits", Year: 1996},
|
|
|
|
|
})
|
|
|
|
|
Expect(names).To(Equal([]string{"Greatest Hits [2001]/01.mp3", "Greatest Hits [2011]/01.mp3", "Greatest Hits [1996]/01.mp3"}))
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
It("names the folder after the full album name", func() {
|
|
|
|
|
names := zipArtistEntries(model.MediaFiles{
|
|
|
|
|
{Path: "a/01.mp3", Suffix: "mp3", AlbumID: "1", Album: "Greatest Hits", Year: 2001,
|
|
|
|
|
Tags: model.Tags{model.TagAlbumVersion: {"Original"}}},
|
|
|
|
|
{Path: "b/01.mp3", Suffix: "mp3", AlbumID: "2", Album: "Greatest Hits", Year: 2005,
|
|
|
|
|
Tags: model.Tags{model.TagAlbumVersion: {"CD/Digital"}}},
|
|
|
|
|
})
|
|
|
|
|
Expect(names).To(Equal([]string{"Greatest Hits (Original)/01.mp3", "Greatest Hits (CD_Digital)/01.mp3"}))
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
It("prefers the album version over the year when it is not part of the name", func() {
|
|
|
|
|
conf.Server.Subsonic.AppendAlbumVersion = false
|
|
|
|
|
names := zipArtistEntries(model.MediaFiles{
|
|
|
|
|
{Path: "a/01.mp3", Suffix: "mp3", AlbumID: "1", Album: "Greatest Hits", Year: 2001,
|
|
|
|
|
Tags: model.Tags{model.TagAlbumVersion: {"Original"}}},
|
|
|
|
|
{Path: "b/01.mp3", Suffix: "mp3", AlbumID: "2", Album: "Greatest Hits", Year: 2005,
|
|
|
|
|
Tags: model.Tags{model.TagAlbumVersion: {"Deluxe Edition"}}},
|
|
|
|
|
})
|
|
|
|
|
Expect(names).To(Equal([]string{"Greatest Hits [Original]/01.mp3", "Greatest Hits [Deluxe Edition]/01.mp3"}))
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
It("leaves the one album without the field unsuffixed", func() {
|
|
|
|
|
conf.Server.Subsonic.AppendAlbumVersion = false
|
|
|
|
|
names := zipArtistEntries(model.MediaFiles{
|
|
|
|
|
{Path: "a/01.mp3", Suffix: "mp3", AlbumID: "1", Album: "Greatest Hits", Year: 2001},
|
|
|
|
|
{Path: "b/01.mp3", Suffix: "mp3", AlbumID: "2", Album: "Greatest Hits", Year: 2005,
|
|
|
|
|
Tags: model.Tags{model.TagAlbumVersion: {"Deluxe Edition"}}},
|
|
|
|
|
})
|
|
|
|
|
Expect(names).To(Equal([]string{"Greatest Hits/01.mp3", "Greatest Hits [Deluxe Edition]/01.mp3"}))
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
It("skips a field that is empty on more than one album", func() {
|
|
|
|
|
conf.Server.Subsonic.AppendAlbumVersion = false
|
|
|
|
|
names := zipArtistEntries(model.MediaFiles{
|
|
|
|
|
{Path: "a/01.mp3", Suffix: "mp3", AlbumID: "1", Album: "Greatest Hits", Year: 2001},
|
|
|
|
|
{Path: "b/01.mp3", Suffix: "mp3", AlbumID: "2", Album: "Greatest Hits", Year: 2005},
|
|
|
|
|
{Path: "c/01.mp3", Suffix: "mp3", AlbumID: "3", Album: "Greatest Hits", Year: 2010,
|
|
|
|
|
Tags: model.Tags{model.TagAlbumVersion: {"Deluxe Edition"}}},
|
|
|
|
|
})
|
|
|
|
|
Expect(names).To(Equal([]string{"Greatest Hits [2001]/01.mp3", "Greatest Hits [2005]/01.mp3", "Greatest Hits [2010]/01.mp3"}))
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
It("skips a field that is the same on every album", func() {
|
|
|
|
|
names := zipArtistEntries(model.MediaFiles{
|
|
|
|
|
{Path: "a/01.mp3", Suffix: "mp3", AlbumID: "1", Album: "Live", Year: 2001, MbzAlbumType: "album", CatalogNum: "CAT-1"},
|
|
|
|
|
{Path: "b/01.mp3", Suffix: "mp3", AlbumID: "2", Album: "Live", Year: 2001, MbzAlbumType: "album", CatalogNum: "CAT-2"},
|
|
|
|
|
})
|
|
|
|
|
Expect(names).To(Equal([]string{"Live [CAT-1]/01.mp3", "Live [CAT-2]/01.mp3"}))
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
It("falls back to the album id when nothing differs", func() {
|
|
|
|
|
names := zipArtistEntries(model.MediaFiles{
|
|
|
|
|
{Path: "a/01.mp3", Suffix: "mp3", AlbumID: "0123456789abcdef", Album: "Greatest Hits", Year: 2001},
|
|
|
|
|
{Path: "b/01.mp3", Suffix: "mp3", AlbumID: "fedcba9876543210", Album: "Greatest Hits", Year: 2001},
|
|
|
|
|
})
|
|
|
|
|
Expect(names).To(Equal([]string{"Greatest Hits [012345]/01.mp3", "Greatest Hits [fedcba]/01.mp3"}))
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
It("treats names that sanitize to the same folder as a clash", func() {
|
|
|
|
|
names := zipArtistEntries(model.MediaFiles{
|
|
|
|
|
{Path: "a/01.mp3", Suffix: "mp3", AlbumID: "1", Album: "A/B", Year: 2001},
|
|
|
|
|
{Path: "b/01.mp3", Suffix: "mp3", AlbumID: "2", Album: `A\B`, Year: 2005},
|
|
|
|
|
})
|
|
|
|
|
Expect(names).To(Equal([]string{"A_B [2001]/01.mp3", "A_B [2005]/01.mp3"}))
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
It("sanitizes the suffix", func() {
|
|
|
|
|
conf.Server.Subsonic.AppendAlbumVersion = false
|
|
|
|
|
names := zipArtistEntries(model.MediaFiles{
|
|
|
|
|
{Path: "a/01.mp3", Suffix: "mp3", AlbumID: "1", Album: "Hits", Tags: model.Tags{model.TagAlbumVersion: {"Vinyl"}}},
|
|
|
|
|
{Path: "b/01.mp3", Suffix: "mp3", AlbumID: "2", Album: "Hits", Tags: model.Tags{model.TagAlbumVersion: {"CD/Digital"}}},
|
|
|
|
|
})
|
|
|
|
|
Expect(names).To(Equal([]string{"Hits [Vinyl]/01.mp3", "Hits [CD_Digital]/01.mp3"}))
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
It("leaves the folder name alone when only one album has it", func() {
|
|
|
|
|
names := zipArtistEntries(model.MediaFiles{
|
|
|
|
|
{Path: "a/01.mp3", Suffix: "mp3", AlbumID: "1", Album: "Greatest Hits", Year: 2001},
|
|
|
|
|
{Path: "b/01.mp3", Suffix: "mp3", AlbumID: "2", Album: "Other", Year: 2005},
|
|
|
|
|
})
|
|
|
|
|
Expect(names).To(Equal([]string{"Greatest Hits/01.mp3", "Other/01.mp3"}))
|
|
|
|
|
})
|
|
|
|
|
})
|
2023-03-26 19:34:12 -04:00
|
|
|
})
|
|
|
|
|
|
fix(transcoding): cap concurrent transcodes to prevent ffmpeg DoS (#5522)
* feat(transcoding): add MaxConcurrent and MaxConcurrentPerUser config
Introduce Transcoding.MaxConcurrent (default NumCPU()*2) and
Transcoding.MaxConcurrentPerUser (default 3) to support upcoming
concurrency limits on the streaming pipeline. No behavior change yet.
Refs #5246
* feat(transcoding): add TranscodeLimiter with global and per-user caps
Introduce a non-blocking limiter that gates concurrent transcodes. Returns
ErrTooManyTranscodes immediately when the cap is reached so callers can
translate it into a 429 response, rather than queuing requests.
The per-user reservation is taken first to avoid burning a global slot
that would only be rolled back when the per-user cap rejects the caller.
Release is idempotent so wrapping the transcoder reader's Close is safe.
Refs #5246
* feat(transcoding): cap concurrent transcodes in media streamer
Acquire a TranscodeLimiter slot before spawning ffmpeg in the transcoding
cache's read function, and release it when the resulting reader is closed.
Raw streams and cache hits bypass the limiter so a single saturating client
cannot block ordinary playback.
When the cap is reached, ErrTooManyTranscodes bubbles up through cache.Get,
ready for the HTTP layer to translate into a 429 response.
Refs #5246
* feat(transcoding): return HTTP 429 with Retry-After when transcode cap is hit
Map stream.ErrTooManyTranscodes to HTTP 429 in both the Subsonic API
(/stream, /download) and the public share endpoint, including a 5s
Retry-After hint. The Subsonic response still carries a failed-status
envelope so clients that ignore HTTP codes also see the failure.
Refs #5246
* feat(transcoding): default MaxConcurrent to 0 (disabled)
Ship the limiter opt-in so existing installations are not affected by a
behavior change on upgrade. Users hitting the DoS reported in #5246 can
enable it by setting Transcoding.MaxConcurrent to a positive value
(NumCPU()*2 is a reasonable starting point).
Refs #5246
* fix(transcoding): make global and per-user caps independent
Previously the limiter short-circuited to a no-op whenever MaxConcurrent
was zero, silently ignoring a configured MaxConcurrentPerUser. Treat each
cap independently so an operator can throttle per-user without enforcing
a global ceiling (or vice versa), and only fall back to the no-op limiter
when both caps are disabled.
* fix(archiver): abort archive download when the transcode limiter rejects
The album/artist/playlist zip writers were silently producing zip entries
with headers but no data when ms.NewStream returned ErrTooManyTranscodes,
because the per-file error was discarded by `_ = a.addFileToZip(...)`.
The client received HTTP 200 with a corrupt zip and no indication that
the server was rate-limited.
Now the zip loop bails out as soon as it sees ErrTooManyTranscodes, and
the Download handler swallows the error (the response status and
Content-Disposition are already flushed by the time the limit is hit, so
no 429 can be sent). The truncated zip surfaces the problem to the
client; operators see a clear "transcode cap reached" warning in the
server logs.
Refs #5246
* fix(transcoding): release limiter slot on client close, not ffmpeg EOF
Previously the slot was wrapped around the ffmpeg source reader, so it
was only released by the cache's background copyAndClose goroutine when
ffmpeg finished producing the file — meaning a client that disconnected
after a single byte still held the slot for the full transcode duration.
Under MaxConcurrent=N this serialized fresh requests behind abandoned
encodes for minutes.
Hand the release function back from the cache producer via the streamJob
struct and wire it into the consumer-side Stream.Close. The HTTP handler
already runs `defer stream.Close()`, so disconnect now frees the slot
immediately. Cache hits never enter the producer and still pay no slot,
and singleflight waiters on the same key correctly inherit no release
(only the original producer's job holds the slot).
Refs #5246
* fix(transcoding): skip per-user cap for anonymous requests
Public share viewers have no user in context, so userName(ctx) returned
the literal string "UNKNOWN" and the limiter mapped every anonymous
viewer to the same bucket. With MaxConcurrentPerUser=N, only N
unrelated anonymous clients could stream a viral share at any time —
the opposite of the fairness the per-user cap is meant to provide.
Introduce a limiterKey(ctx) helper that returns "" for anonymous
callers (userName(ctx) is unchanged for logs), and teach Acquire to
skip the per-user reservation when the key is empty. The global cap is
still enforced for anonymous traffic and remains the protection against
runaway anonymous load.
Refs #5246
* refactor(transcoding): tidy limiter struct and centralize Retry-After
Per review feedback:
- Drop the redundant maxConcurrent field on transcodeLimiter; the channel
capacity already enforces the global cap and the field was only used
inside the constructor.
- Only allocate the perUser map when MaxConcurrentPerUser > 0.
- Move the Retry-After value into core/stream as RetryAfterSeconds so the
Subsonic API and public-share handlers cannot drift if the window is
later tuned.
* fix(transcoding): do not log limiter rejections as cache failures
NewStream was emitting an error-level "Error accessing transcoding cache"
log whenever cache.Get returned anything non-nil, including the limiter's
ErrTooManyTranscodes — even though the producer had already logged the
rejection at warn level. The result was double logging and a misleading
"cache failure" classification that buries real cache problems.
Skip the error log when the cause is ErrTooManyTranscodes; the warn line
from the producer is the canonical signal.
* fix(archiver): open stream before writing zip entry header
Per review: addFileToZip previously called z.CreateHeader before
NewStream, so when the limiter rejected a transcode the zip already
contained a 0-byte entry for that track. Open the source first and only
write the header once the read side is ready; rejections now skip the
entry entirely.
The truncation comment in handleArchiveErr was also misleading — z.Close
finalises the central directory, so the client receives a well-formed
zip containing only the tracks written before the rejection, not a
"truncated" archive. Reword to match reality.
* fix(transcoding): hold slot for ffmpeg lifetime, force cancellable ctx
The previous release-on-consumer-close design let a client open many
unique transcodes, disconnect immediately, and still spawn the
configured cap's worth of ffmpeg processes — the cache writer goroutine
continued draining ffmpeg to disk after the client disappeared, defeating
the DoS protection the limiter is meant to provide.
Move the release back onto the source reader so the slot is freed only
when ffmpeg actually exits (either EOF or context cancellation). To keep
disconnects from leaking slots for the full transcode duration, force
the request context into ffmpeg whenever the limiter is enabled — so
client disconnect cancels the process and frees the slot promptly.
When the limiter is disabled, the legacy EnableTranscodingCancellation
behavior is preserved unchanged.
Reported by codex and Copilot reviewers on #5522.
2026-05-24 00:24:30 -03:00
|
|
|
Context("when the transcode limiter rejects a file", func() {
|
|
|
|
|
It("aborts the archive instead of continuing with empty entries", func() {
|
|
|
|
|
mfs := model.MediaFiles{
|
|
|
|
|
{Path: "test_data/01 - track1.mp3", Suffix: "mp3", AlbumID: "1", Album: "Album", DiscNumber: 1},
|
|
|
|
|
{Path: "test_data/02 - track2.mp3", Suffix: "mp3", AlbumID: "1", Album: "Album", DiscNumber: 1},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
mfRepo := &mockMediaFileRepository{}
|
|
|
|
|
mfRepo.On("GetAll", []model.QueryOptions{{
|
|
|
|
|
Filters: squirrel.Eq{"album_id": "1"},
|
|
|
|
|
Sort: "album",
|
|
|
|
|
}}).Return(mfs, nil)
|
2026-09-25 18:06:10 -04:00
|
|
|
ds.On("MediaFile").Return(mfRepo)
|
fix(transcoding): cap concurrent transcodes to prevent ffmpeg DoS (#5522)
* feat(transcoding): add MaxConcurrent and MaxConcurrentPerUser config
Introduce Transcoding.MaxConcurrent (default NumCPU()*2) and
Transcoding.MaxConcurrentPerUser (default 3) to support upcoming
concurrency limits on the streaming pipeline. No behavior change yet.
Refs #5246
* feat(transcoding): add TranscodeLimiter with global and per-user caps
Introduce a non-blocking limiter that gates concurrent transcodes. Returns
ErrTooManyTranscodes immediately when the cap is reached so callers can
translate it into a 429 response, rather than queuing requests.
The per-user reservation is taken first to avoid burning a global slot
that would only be rolled back when the per-user cap rejects the caller.
Release is idempotent so wrapping the transcoder reader's Close is safe.
Refs #5246
* feat(transcoding): cap concurrent transcodes in media streamer
Acquire a TranscodeLimiter slot before spawning ffmpeg in the transcoding
cache's read function, and release it when the resulting reader is closed.
Raw streams and cache hits bypass the limiter so a single saturating client
cannot block ordinary playback.
When the cap is reached, ErrTooManyTranscodes bubbles up through cache.Get,
ready for the HTTP layer to translate into a 429 response.
Refs #5246
* feat(transcoding): return HTTP 429 with Retry-After when transcode cap is hit
Map stream.ErrTooManyTranscodes to HTTP 429 in both the Subsonic API
(/stream, /download) and the public share endpoint, including a 5s
Retry-After hint. The Subsonic response still carries a failed-status
envelope so clients that ignore HTTP codes also see the failure.
Refs #5246
* feat(transcoding): default MaxConcurrent to 0 (disabled)
Ship the limiter opt-in so existing installations are not affected by a
behavior change on upgrade. Users hitting the DoS reported in #5246 can
enable it by setting Transcoding.MaxConcurrent to a positive value
(NumCPU()*2 is a reasonable starting point).
Refs #5246
* fix(transcoding): make global and per-user caps independent
Previously the limiter short-circuited to a no-op whenever MaxConcurrent
was zero, silently ignoring a configured MaxConcurrentPerUser. Treat each
cap independently so an operator can throttle per-user without enforcing
a global ceiling (or vice versa), and only fall back to the no-op limiter
when both caps are disabled.
* fix(archiver): abort archive download when the transcode limiter rejects
The album/artist/playlist zip writers were silently producing zip entries
with headers but no data when ms.NewStream returned ErrTooManyTranscodes,
because the per-file error was discarded by `_ = a.addFileToZip(...)`.
The client received HTTP 200 with a corrupt zip and no indication that
the server was rate-limited.
Now the zip loop bails out as soon as it sees ErrTooManyTranscodes, and
the Download handler swallows the error (the response status and
Content-Disposition are already flushed by the time the limit is hit, so
no 429 can be sent). The truncated zip surfaces the problem to the
client; operators see a clear "transcode cap reached" warning in the
server logs.
Refs #5246
* fix(transcoding): release limiter slot on client close, not ffmpeg EOF
Previously the slot was wrapped around the ffmpeg source reader, so it
was only released by the cache's background copyAndClose goroutine when
ffmpeg finished producing the file — meaning a client that disconnected
after a single byte still held the slot for the full transcode duration.
Under MaxConcurrent=N this serialized fresh requests behind abandoned
encodes for minutes.
Hand the release function back from the cache producer via the streamJob
struct and wire it into the consumer-side Stream.Close. The HTTP handler
already runs `defer stream.Close()`, so disconnect now frees the slot
immediately. Cache hits never enter the producer and still pay no slot,
and singleflight waiters on the same key correctly inherit no release
(only the original producer's job holds the slot).
Refs #5246
* fix(transcoding): skip per-user cap for anonymous requests
Public share viewers have no user in context, so userName(ctx) returned
the literal string "UNKNOWN" and the limiter mapped every anonymous
viewer to the same bucket. With MaxConcurrentPerUser=N, only N
unrelated anonymous clients could stream a viral share at any time —
the opposite of the fairness the per-user cap is meant to provide.
Introduce a limiterKey(ctx) helper that returns "" for anonymous
callers (userName(ctx) is unchanged for logs), and teach Acquire to
skip the per-user reservation when the key is empty. The global cap is
still enforced for anonymous traffic and remains the protection against
runaway anonymous load.
Refs #5246
* refactor(transcoding): tidy limiter struct and centralize Retry-After
Per review feedback:
- Drop the redundant maxConcurrent field on transcodeLimiter; the channel
capacity already enforces the global cap and the field was only used
inside the constructor.
- Only allocate the perUser map when MaxConcurrentPerUser > 0.
- Move the Retry-After value into core/stream as RetryAfterSeconds so the
Subsonic API and public-share handlers cannot drift if the window is
later tuned.
* fix(transcoding): do not log limiter rejections as cache failures
NewStream was emitting an error-level "Error accessing transcoding cache"
log whenever cache.Get returned anything non-nil, including the limiter's
ErrTooManyTranscodes — even though the producer had already logged the
rejection at warn level. The result was double logging and a misleading
"cache failure" classification that buries real cache problems.
Skip the error log when the cause is ErrTooManyTranscodes; the warn line
from the producer is the canonical signal.
* fix(archiver): open stream before writing zip entry header
Per review: addFileToZip previously called z.CreateHeader before
NewStream, so when the limiter rejected a transcode the zip already
contained a 0-byte entry for that track. Open the source first and only
write the header once the read side is ready; rejections now skip the
entry entirely.
The truncation comment in handleArchiveErr was also misleading — z.Close
finalises the central directory, so the client receives a well-formed
zip containing only the tracks written before the rejection, not a
"truncated" archive. Reword to match reality.
* fix(transcoding): hold slot for ffmpeg lifetime, force cancellable ctx
The previous release-on-consumer-close design let a client open many
unique transcodes, disconnect immediately, and still spawn the
configured cap's worth of ffmpeg processes — the cache writer goroutine
continued draining ffmpeg to disk after the client disappeared, defeating
the DoS protection the limiter is meant to provide.
Move the release back onto the source reader so the slot is freed only
when ffmpeg actually exits (either EOF or context cancellation). To keep
disconnects from leaking slots for the full transcode duration, force
the request context into ffmpeg whenever the limiter is enabled — so
client disconnect cancels the process and frees the slot promptly.
When the limiter is disabled, the legacy EnableTranscodingCancellation
behavior is preserved unchanged.
Reported by codex and Copilot reviewers on #5522.
2026-05-24 00:24:30 -03:00
|
|
|
|
|
|
|
|
ms.On("NewStream", mock.Anything, mock.Anything, stream.Request{Format: "mp3", BitRate: 128}).
|
|
|
|
|
Return(nil, stream.ErrTooManyTranscodes).Once()
|
|
|
|
|
|
|
|
|
|
out := new(bytes.Buffer)
|
|
|
|
|
err := arch.ZipAlbum(context.Background(), "1", "mp3", 128, out)
|
|
|
|
|
Expect(err).To(MatchError(stream.ErrTooManyTranscodes))
|
|
|
|
|
// NewStream should only have been called once: the loop must bail
|
|
|
|
|
// out on the rejection instead of trying every remaining track.
|
|
|
|
|
ms.AssertNumberOfCalls(GinkgoT(), "NewStream", 1)
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
2023-03-26 19:34:12 -04:00
|
|
|
Context("ZipShare", func() {
|
|
|
|
|
It("zips a share correctly", func() {
|
|
|
|
|
mfs := model.MediaFiles{
|
|
|
|
|
{ID: "1", Path: "test_data/01 - track1.mp3", Suffix: "mp3", Artist: "Artist 1", Title: "track1"},
|
|
|
|
|
{ID: "2", Path: "test_data/02 - track2.mp3", Suffix: "mp3", Artist: "Artist 2", Title: "track2"},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
share := &model.Share{
|
|
|
|
|
ID: "1",
|
|
|
|
|
Downloadable: true,
|
|
|
|
|
Format: "mp3",
|
|
|
|
|
MaxBitRate: 128,
|
|
|
|
|
Tracks: mfs,
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-09 22:22:58 -04:00
|
|
|
ms.On("NewStream", mock.Anything, mock.Anything, stream.Request{Format: "mp3", BitRate: 128}).Return(io.NopCloser(strings.NewReader("test")), nil).Times(2)
|
2023-03-26 19:34:12 -04:00
|
|
|
|
|
|
|
|
out := new(bytes.Buffer)
|
fix(share): give visual feedback when downloading from a share (#5865)
* fix(share): give visual feedback when downloading a share
The share page handed the download URL to navidrome-music-player, which fell
through to downloadjs and buffered the whole ZIP into memory via XHR before
saving it. Nothing was handed to the browser until the last byte arrived, so a
large share produced a long silent window with no player feedback and no
browser download UI, inviting repeat clicks that each spawn another server-side
zip+transcode.
Use the player's customDownloader prop to trigger a synthetic anchor instead,
so the browser performs the download and reports its own progress. An anchor
rather than assigning window.location.href: the share page's service worker
registers a NavigationRoute over all navigations, which intercepts the streamed
archive and fails it into the offline fallback (observed as HTTP 503 in Chrome).
handleDownloads now loads the share before streaming so it can set
Content-Disposition and Content-Type. This also fixes error reporting: ZipShare
previously wrote to the ResponseWriter before checkShareError ran, locking the
status at 200, so expired, missing and non-downloadable shares all returned 200.
They now correctly return 410, 404 and 403.
* feat(share): acknowledge the download click in the player
The browser's download UI is the real progress indicator, but nothing in the
page itself reacted to the click, so the moment before the browser catches up
still read as unresponsive. Dim the download button and make it unclickable for
two seconds after a download starts, reusing the JSS function-value pattern the
existing single-track styling already uses.
A repeat download restarts the window instead of extending the original, and
the timer is cleared on unmount. This also blunts repeat clicking, where every
extra click costs another server-side zip and transcode.
Add SharePlayer tests covering the download mechanism and this state machine.
The dimming itself is verified in a browser rather than jsdom: JSS function
values are not evaluated there, so the rule is never emitted and a CSS
assertion would pass or fail for the wrong reason.
Signed-off-by: Deluan <deluan@navidrome.org>
* test(share): assert render counts in SharePlayer feedback tests
The two acknowledgement tests compared the props object across renders, which
React may reuse, so they passed without proving anything and then failed once
the surrounding assertions changed. Count renders instead, and let the pending
timer run out rather than advancing exactly to its deadline, which does not
cross it.
The repeat-download test now also asserts that no render happens at the
original deadline, proving the timer was replaced rather than merely that one
eventually fired.
* fix(share): count one visit per share download
The preflight share load added in this branch made every download record two
visits: handleDownloads called Share.Load, and ZipShare then loaded the share
again internally. Share.Load increments and persists VisitCount, so the counter
advanced twice per download and the repository work was duplicated.
Pass the already-loaded share into ZipShare instead of its id. handleDownloads
is its only production caller, and it now has the share in hand for the
Content-Disposition header anyway.
The archiver test asserts Load is not called, so the double-load cannot come
back unnoticed. Verified against a running server: the counter now advances by
one per download.
* test(share): derive feedback-window timings from the constant
The acknowledgement tests hardcoded clock advances tuned to a 2000ms window.
Raising DOWNLOAD_FEEDBACK_MS to 5000 left them advancing 1500ms and 1001ms,
which no longer reach the deadline they are meant to cross, so the repeat-
download test passed without proving the timer had been replaced.
Export the constant and derive the advances from it, and let the pending timer
run out in the unmount test rather than advancing a fixed amount. Changing the
duration can no longer silently strand a test short of its deadline.
---------
Signed-off-by: Deluan <deluan@navidrome.org>
2026-07-28 16:13:44 -04:00
|
|
|
err := arch.ZipShare(context.Background(), share, out)
|
2023-03-26 19:34:12 -04:00
|
|
|
Expect(err).To(BeNil())
|
|
|
|
|
|
fix(share): give visual feedback when downloading from a share (#5865)
* fix(share): give visual feedback when downloading a share
The share page handed the download URL to navidrome-music-player, which fell
through to downloadjs and buffered the whole ZIP into memory via XHR before
saving it. Nothing was handed to the browser until the last byte arrived, so a
large share produced a long silent window with no player feedback and no
browser download UI, inviting repeat clicks that each spawn another server-side
zip+transcode.
Use the player's customDownloader prop to trigger a synthetic anchor instead,
so the browser performs the download and reports its own progress. An anchor
rather than assigning window.location.href: the share page's service worker
registers a NavigationRoute over all navigations, which intercepts the streamed
archive and fails it into the offline fallback (observed as HTTP 503 in Chrome).
handleDownloads now loads the share before streaming so it can set
Content-Disposition and Content-Type. This also fixes error reporting: ZipShare
previously wrote to the ResponseWriter before checkShareError ran, locking the
status at 200, so expired, missing and non-downloadable shares all returned 200.
They now correctly return 410, 404 and 403.
* feat(share): acknowledge the download click in the player
The browser's download UI is the real progress indicator, but nothing in the
page itself reacted to the click, so the moment before the browser catches up
still read as unresponsive. Dim the download button and make it unclickable for
two seconds after a download starts, reusing the JSS function-value pattern the
existing single-track styling already uses.
A repeat download restarts the window instead of extending the original, and
the timer is cleared on unmount. This also blunts repeat clicking, where every
extra click costs another server-side zip and transcode.
Add SharePlayer tests covering the download mechanism and this state machine.
The dimming itself is verified in a browser rather than jsdom: JSS function
values are not evaluated there, so the rule is never emitted and a CSS
assertion would pass or fail for the wrong reason.
Signed-off-by: Deluan <deluan@navidrome.org>
* test(share): assert render counts in SharePlayer feedback tests
The two acknowledgement tests compared the props object across renders, which
React may reuse, so they passed without proving anything and then failed once
the surrounding assertions changed. Count renders instead, and let the pending
timer run out rather than advancing exactly to its deadline, which does not
cross it.
The repeat-download test now also asserts that no render happens at the
original deadline, proving the timer was replaced rather than merely that one
eventually fired.
* fix(share): count one visit per share download
The preflight share load added in this branch made every download record two
visits: handleDownloads called Share.Load, and ZipShare then loaded the share
again internally. Share.Load increments and persists VisitCount, so the counter
advanced twice per download and the repository work was duplicated.
Pass the already-loaded share into ZipShare instead of its id. handleDownloads
is its only production caller, and it now has the share in hand for the
Content-Disposition header anyway.
The archiver test asserts Load is not called, so the double-load cannot come
back unnoticed. Verified against a running server: the counter now advances by
one per download.
* test(share): derive feedback-window timings from the constant
The acknowledgement tests hardcoded clock advances tuned to a 2000ms window.
Raising DOWNLOAD_FEEDBACK_MS to 5000 left them advancing 1500ms and 1001ms,
which no longer reach the deadline they are meant to cross, so the repeat-
download test passed without proving the timer had been replaced.
Export the constant and derive the advances from it, and let the pending timer
run out in the unmount test rather than advancing a fixed amount. Changing the
duration can no longer silently strand a test short of its deadline.
---------
Signed-off-by: Deluan <deluan@navidrome.org>
2026-07-28 16:13:44 -04:00
|
|
|
// Share.Load records a visit; re-loading here would double-count
|
|
|
|
|
// every download.
|
|
|
|
|
sh.AssertNotCalled(GinkgoT(), "Load", mock.Anything, mock.Anything)
|
|
|
|
|
|
2023-03-26 19:34:12 -04:00
|
|
|
zr, err := zip.NewReader(bytes.NewReader(out.Bytes()), int64(out.Len()))
|
|
|
|
|
Expect(err).To(BeNil())
|
|
|
|
|
|
|
|
|
|
Expect(len(zr.File)).To(Equal(2))
|
|
|
|
|
Expect(zr.File[0].Name).To(Equal("01 - Artist 1 - track1.mp3"))
|
|
|
|
|
Expect(zr.File[1].Name).To(Equal("02 - Artist 2 - track2.mp3"))
|
|
|
|
|
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
Context("ZipPlaylist", func() {
|
fix(share): reuse cached transcodes for share streams and zip downloads (#6262)
* fix(share): reuse cached transcodes when streaming from share links
Public share streams built the stream request with only the share's format
and bit rate, leaving sample rate, bit depth and channels at zero. Regular
playback resolves those through the transcode decider (e.g. 48000 Hz for
Opus), and they are part of the transcoding cache key, so a track already
transcoded during normal playback was transcoded again into a separate,
identical cache entry when played through a share link.
The public router now resolves share stream requests with the same
TranscodeDecider.ResolveRequest used by the Subsonic stream endpoint, so
both paths produce the same request and share cache entries.
Fixes #6261
* fix(archiver): reuse cached transcodes when zipping downloads
Zip downloads (album, artist, playlist and share) built the stream request
with only the format and bit rate, leaving sample rate, bit depth and
channels at zero. Those are part of the transcoding cache key, so a track
already transcoded for playback was transcoded again into a separate cache
entry when downloaded in a zip, and vice versa.
The archiver now resolves each request with TranscodeDecider.ResolveRequest,
the same as single-song downloads and streams. This also applies the
decider's defaults, so a zip requested without a bit rate uses the target
format's default bit rate instead of leaving it to ffmpeg.
* fix(archiver): name zip entries after the resolved transcoding format
The transcode decider can pick a different format than the one requested
(for example a player's forced transcoding, or a fallback to the default
downsampling format when the requested one can't be produced). Zip entry
names and the playlist M3U were still built from the requested format, so
an entry could end in .mp3 or .flac while holding Opus data.
Each track's request is now resolved before its entry name is built, and
the name uses the resolved format.
2026-10-03 08:58:54 -07:00
|
|
|
It("names the entries and the M3U lines after the resolved format", func() {
|
|
|
|
|
pls := &model.Playlist{ID: "1", Name: "Test Playlist", Tracks: []model.PlaylistTrack{
|
|
|
|
|
{MediaFile: model.MediaFile{Path: "test_data/01 - track1.flac", Suffix: "flac", Artist: "Artist 1", Title: "track1"}},
|
|
|
|
|
}}
|
|
|
|
|
plRepo := &mockPlaylistRepository{}
|
|
|
|
|
plRepo.On("GetWithTracks", "1", true, false).Return(pls, nil)
|
|
|
|
|
ds.On("Playlist").Return(plRepo)
|
|
|
|
|
dc.resolved = &stream.Request{Format: "opus", BitRate: 128}
|
|
|
|
|
ms.On("NewStream", mock.Anything, mock.Anything, *dc.resolved).Return(io.NopCloser(strings.NewReader("test")), nil)
|
|
|
|
|
|
|
|
|
|
out := new(bytes.Buffer)
|
|
|
|
|
Expect(arch.ZipPlaylist(GinkgoT().Context(), "1", "mp3", 128, out)).To(Succeed())
|
|
|
|
|
|
|
|
|
|
zr, err := zip.NewReader(bytes.NewReader(out.Bytes()), int64(out.Len()))
|
|
|
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
|
Expect(zr.File[0].Name).To(Equal("01 - Artist 1 - track1.opus"))
|
|
|
|
|
m3u, err := zr.File[1].Open()
|
|
|
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
|
defer m3u.Close()
|
|
|
|
|
content, err := io.ReadAll(m3u)
|
|
|
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
|
Expect(string(content)).To(ContainSubstring("01 - Artist 1 - track1.opus"))
|
|
|
|
|
})
|
|
|
|
|
|
2023-03-26 19:34:12 -04:00
|
|
|
It("zips a playlist correctly", func() {
|
|
|
|
|
tracks := []model.PlaylistTrack{
|
2023-05-16 18:34:15 -04:00
|
|
|
{MediaFile: model.MediaFile{Path: "test_data/01 - track1.mp3", Suffix: "mp3", AlbumID: "1", Album: "Album 1", DiscNumber: 1, Artist: "AC/DC", Title: "track1"}},
|
2023-03-26 19:34:12 -04:00
|
|
|
{MediaFile: model.MediaFile{Path: "test_data/02 - track2.mp3", Suffix: "mp3", AlbumID: "1", Album: "Album 1", DiscNumber: 1, Artist: "Artist 2", Title: "track2"}},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pls := &model.Playlist{
|
|
|
|
|
ID: "1",
|
|
|
|
|
Name: "Test Playlist",
|
|
|
|
|
Tracks: tracks,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
plRepo := &mockPlaylistRepository{}
|
feat(bfr): Big Refactor: new scanner, lots of new fields and tags, improvements and DB schema changes (#2709)
* fix(server): more race conditions when updating artist/album from external sources
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(scanner): add .gitignore syntax to .ndignore. Resolves #1394
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(ui): null
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(scanner): pass configfile option to child process
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(scanner): resume interrupted fullScans
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(scanner): remove old scanner code
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(scanner): rename old metadata package
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(scanner): move old metadata package
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: tests
Signed-off-by: Deluan <deluan@navidrome.org>
* chore(deps): update Go to 1.23.4
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: logs
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(test):
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: log level
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: remove log message
Signed-off-by: Deluan <deluan@navidrome.org>
* feat: add config for scanner watcher
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: children playlists
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: replace `interface{}` with `any`
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: smart playlists with genres
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: allow any tags in smart playlists
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: artist names in playlists
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: smart playlist's sort by tags
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(subsonic): add moods to child
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(subsonic): add moods to AlbumID3
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor(subsonic): use generic JSONArray for OS arrays
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor(subsonic): use https in test
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(subsonic): add releaseTypes to AlbumID3
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(subsonic): add recordLabels to AlbumID3
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor(subsonic): rename JSONArray to Array
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(subsonic): add artists to AlbumID3
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(subsonic): add artists to Child
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(scanner): do not pre-populate smart playlists
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(subsonic): implement a simplified version of ArtistID3.
See https://github.com/opensubsonic/open-subsonic-api/discussions/120
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(subsonic): add artists to album child
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(subsonic): add contributors to mediafile Child
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(subsonic): add albumArtists to mediafile Child
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(subsonic): add displayArtist and displayAlbumArtist
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(subsonic): add displayComposer to Child
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(subsonic): add roles to ArtistID3
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(subsonic): use " • " separator for displayComposer
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor:
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(subsonic):
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(subsonic): respect `PreferSortTags` config option
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor(subsonic):
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: optimize purging non-unused tags
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: don't run 'refresh artist stats' concurrently with other transactions
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor:
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: log message
Signed-off-by: Deluan <deluan@navidrome.org>
* feat: add Scanner.ScanOnStartup config option, default true
Signed-off-by: Deluan <deluan@navidrome.org>
* feat: better json parsing error msg when importing NSPs
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: don't update album's imported_time when updating external_metadata
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: handle interrupted scans and full scans after migrations
Signed-off-by: Deluan <deluan@navidrome.org>
* feat: run `analyze` when migration requires a full rescan
Signed-off-by: Deluan <deluan@navidrome.org>
* feat: run `PRAGMA optimize` at the end of the scan
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: don't update artist's updated_at when updating external_metadata
Signed-off-by: Deluan <deluan@navidrome.org>
* feat: handle multiple artists and roles in smart playlists
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(ui): dim missing tracks
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: album missing logic
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: error encoding in gob
Signed-off-by: Deluan <deluan@navidrome.org>
* feat: separate warnings from errors
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: mark albums as missing if they were contained in a deleted folder
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: add participant names to media_file and album tables
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: use participations in criteria, instead of m2m relationship
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: rename participations to participants
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(subsonic): add moods to album child
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: albumartist role case
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(scanner): run scanner as an external process by default
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(ui): show albumArtist names
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(ui): dim out missing albums
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: flaky test
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(server): scrobble buffer mapping. fix #3583
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: more participations renaming
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: listenbrainz scrobbling
Signed-off-by: Deluan <deluan@navidrome.org>
* feat: send release_group_mbid to listenbrainz
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(subsonic): implement OpenSubsonic explicitStatus field (#3597)
* feat: implement OpenSubsonic explicitStatus field
* fix(subsonic): fix failing snapshot tests
* refactor: create helper for setting explicitStatus
* fix: store smaller values for explicit-status on database
* test: ToAlbum explicitStatus
* refactor: rename explicitStatus helper function
---------
Co-authored-by: Deluan Quintão <deluan@navidrome.org>
* fix: handle album and track tags in the DB based on the mappings.yaml file
Signed-off-by: Deluan <deluan@navidrome.org>
* save similar artists as JSONB
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: getAlbumList byGenre
Signed-off-by: Deluan <deluan@navidrome.org>
* detect changes in PID configuration
Signed-off-by: Deluan <deluan@navidrome.org>
* set default album PID to legacy_pid
Signed-off-by: Deluan <deluan@navidrome.org>
* fix tests
Signed-off-by: Deluan <deluan@navidrome.org>
* fix SIGSEGV
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: don't lose album stars/ratings when migrating
Signed-off-by: Deluan <deluan@navidrome.org>
* store full PID conf in properties
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: keep album annotations when changing PID.Album config
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: reassign album annotations
Signed-off-by: Deluan <deluan@navidrome.org>
* feat: use (display) albumArtist and add links to each artist
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: not showing albums by albumartist
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: error msgs
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: hide PID from Native API
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: album cover art resolution
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: trim participant names
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: reduce watcher log spam
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: panic when initializing the watcher
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: various artists
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: don't store empty lyrics in the DB
Signed-off-by: Deluan <deluan@navidrome.org>
* remove unused methods
Signed-off-by: Deluan <deluan@navidrome.org>
* drop full_text indexes, as they are not being used by SQLite
Signed-off-by: Deluan <deluan@navidrome.org>
* keep album created_at when upgrading
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(ui): null pointer
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: album artwork cache
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: don't expose missing files in Subsonic API
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: searchable interface
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: filter out missing items from subsonic search
* fix: filter out missing items from playlists
* fix: filter out missing items from shares
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(ui): add filter by artist role
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(subsonic): only return albumartists in getIndexes and getArtists endpoints
Signed-off-by: Deluan <deluan@navidrome.org>
* sort roles alphabetically
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: artist playcounts
Signed-off-by: Deluan <deluan@navidrome.org>
* change default Album PID conf
Signed-off-by: Deluan <deluan@navidrome.org>
* fix albumartist link when it does not match any albumartists values
Signed-off-by: Deluan <deluan@navidrome.org>
* fix `Ignoring filter not whitelisted` (role) message
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: trim any names/titles being imported
Signed-off-by: Deluan <deluan@navidrome.org>
* remove unused genre code
Signed-off-by: Deluan <deluan@navidrome.org>
* serialize calls to Last.fm's getArtist
Signed-off-by: Deluan <deluan@navidrome.org>
xxx
Signed-off-by: Deluan <deluan@navidrome.org>
* add counters to genres
Signed-off-by: Deluan <deluan@navidrome.org>
* nit: fix migration `notice` message
Signed-off-by: Deluan <deluan@navidrome.org>
* optimize similar artists query
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: last.fm.getInfo when mbid does not exist
Signed-off-by: Deluan <deluan@navidrome.org>
* ui only show missing items for admins
Signed-off-by: Deluan <deluan@navidrome.org>
* don't allow interaction with missing items
Signed-off-by: Deluan <deluan@navidrome.org>
* Add Missing Files view (WIP)
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: merged tag_counts into tag table
Signed-off-by: Deluan <deluan@navidrome.org>
* add option to completely disable automatic scanner
Signed-off-by: Deluan <deluan@navidrome.org>
* add delete missing files functionality
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: playlists not showing for regular users
Signed-off-by: Deluan <deluan@navidrome.org>
* reduce updateLastAccess frequency to once every minute
Signed-off-by: Deluan <deluan@navidrome.org>
* reduce update player frequency to once every minute
Signed-off-by: Deluan <deluan@navidrome.org>
* add timeout when updating player
Signed-off-by: Deluan <deluan@navidrome.org>
* remove dead code
Signed-off-by: Deluan <deluan@navidrome.org>
* fix duplicated roles in stats
Signed-off-by: Deluan <deluan@navidrome.org>
* add `; ` to artist splitters
Signed-off-by: Deluan <deluan@navidrome.org>
* fix stats query
Signed-off-by: Deluan <deluan@navidrome.org>
* more logs
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: support legacy clients (DSub) by removing OpenSubsonic extra fields - WIP
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: support legacy clients (DSub) by removing OpenSubsonic extra fields - WIP
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: support legacy clients (DSub) by removing OpenSubsonic extra fields - WIP
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: support legacy clients (DSub) by removing OpenSubsonic extra fields - WIP
Signed-off-by: Deluan <deluan@navidrome.org>
* add record label filter
Signed-off-by: Deluan <deluan@navidrome.org>
* add release type filter
Signed-off-by: Deluan <deluan@navidrome.org>
* fix purgeUnused tags
Signed-off-by: Deluan <deluan@navidrome.org>
* add grouping filter to albums
Signed-off-by: Deluan <deluan@navidrome.org>
* allow any album tags to be used in as filters in the API
Signed-off-by: Deluan <deluan@navidrome.org>
* remove empty tags from album info
Signed-off-by: Deluan <deluan@navidrome.org>
* comments in the migration
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: Cannot read properties of undefined
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: listenbrainz scrobbling (#3640)
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: remove duplicated tag values
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: don't ignore the taglib folder!
Signed-off-by: Deluan <deluan@navidrome.org>
* feat: show track subtitle tag
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: show artists stats based on selected role
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: inspect
Signed-off-by: Deluan <deluan@navidrome.org>
* add media type to album info/filters
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: change format of subtitle in the UI
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: subtitle in Subsonic API and search
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: subtitle in UI's player
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: split strings should be case-insensitive
Signed-off-by: Deluan <deluan@navidrome.org>
* disable ScanSchedule
Signed-off-by: Deluan <deluan@navidrome.org>
* increase default sessiontimeout
Signed-off-by: Deluan <deluan@navidrome.org>
* add sqlite command line tool to docker image
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: resources override
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: album PID conf
Signed-off-by: Deluan <deluan@navidrome.org>
* change migration to mark current artists as albumArtists
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(ui): Allow filtering on multiple genres (#3679)
* feat(ui): Allow filtering on multiple genres
Signed-off-by: Henrik Nordvik <henrikno@gmail.com>
Signed-off-by: Deluan <deluan@navidrome.org>
* add multi-genre filter in Album list
Signed-off-by: Deluan <deluan@navidrome.org>
---------
Signed-off-by: Henrik Nordvik <henrikno@gmail.com>
Signed-off-by: Deluan <deluan@navidrome.org>
Co-authored-by: Henrik Nordvik <henrikno@gmail.com>
* add more multi-valued tag filters to Album and Song views
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(ui): unselect missing files after removing
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(ui): song filter
Signed-off-by: Deluan <deluan@navidrome.org>
* fix sharing tracks. fix #3687
Signed-off-by: Deluan <deluan@navidrome.org>
* use rowids when using search for sync (ex: Symfonium)
Signed-off-by: Deluan <deluan@navidrome.org>
* fix "Report Real Paths" option for subsonic clients
Signed-off-by: Deluan <deluan@navidrome.org>
* fix "Report Real Paths" option for subsonic clients for search
Signed-off-by: Deluan <deluan@navidrome.org>
* add libraryPath to Native API /songs endpoint
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(subsonic): add album version
Signed-off-by: Deluan <deluan@navidrome.org>
* made all tags lowercase as they are case-insensitive anyways.
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(ui): Show full paths, extended properties for album/song (#3691)
* feat(ui): Show full paths, extended properties for album/song
- uses library path + os separator + path
- show participants (album/song) and tags (song)
- make album/participant clickable in show info
* add source to path
* fix pathSeparator in UI
Signed-off-by: Deluan <deluan@navidrome.org>
* fix local artist artwork (#3695)
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: parse vorbis performers
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: clean function into smaller functions
Signed-off-by: Deluan <deluan@navidrome.org>
* fix translations for en and pt
Signed-off-by: Deluan <deluan@navidrome.org>
* add trace log to show annotations reassignment
Signed-off-by: Deluan <deluan@navidrome.org>
* add trace log to show annotations reassignment
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: allow performers without instrument/subrole
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: metadata clean function again
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: optimize split function
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: split function is now a method of TagConf
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: humanize Artist total size
Signed-off-by: Deluan <deluan@navidrome.org>
* add album version to album details
Signed-off-by: Deluan <deluan@navidrome.org>
* don't display album-level tags in SongInfo
Signed-off-by: Deluan <deluan@navidrome.org>
* fix genre clicking in Album Page
Signed-off-by: Deluan <deluan@navidrome.org>
* don't use mbids in Last.fm api calls.
From https://discord.com/channels/671335427726114836/704303730660737113/1337574018143879248:
With MBID:
```
GET https://ws.audioscrobbler.com/2.0/?api_key=XXXX&artist=Van+Morrison&format=json&lang=en&mbid=a41ac10f-0a56-4672-9161-b83f9b223559&method=artist.getInfo
{
artist: {
name: "Bee Gees",
mbid: "bf0f7e29-dfe1-416c-b5c6-f9ebc19ea810",
url: "https://www.last.fm/music/Bee+Gees",
}
```
Without MBID:
```
GET https://ws.audioscrobbler.com/2.0/?api_key=XXXX&artist=Van+Morrison&format=json&lang=en&method=artist.getInfo
{
artist: {
name: "Van Morrison",
mbid: "a41ac10f-0a56-4672-9161-b83f9b223559",
url: "https://www.last.fm/music/Van+Morrison",
}
```
Signed-off-by: Deluan <deluan@navidrome.org>
* better logging for when the artist folder is not found
Signed-off-by: Deluan <deluan@navidrome.org>
* fix various issues with artist image resolution
Signed-off-by: Deluan <deluan@navidrome.org>
* hide "Additional Tags" header if there are none.
Signed-off-by: Deluan <deluan@navidrome.org>
* simplify tag rendering
Signed-off-by: Deluan <deluan@navidrome.org>
* enhance logging for artist folder detection
Signed-off-by: Deluan <deluan@navidrome.org>
* make folderID consistent for relative and absolute folderPaths
Signed-off-by: Deluan <deluan@navidrome.org>
* handle more folder paths scenarios
Signed-off-by: Deluan <deluan@navidrome.org>
* filter out other roles when SubsonicArtistParticipations = true
Signed-off-by: Deluan <deluan@navidrome.org>
* fix "Cannot read properties of undefined"
Signed-off-by: Deluan <deluan@navidrome.org>
* fix lyrics and comments being truncated (#3701)
* fix lyrics and comments being truncated
* specifically test for lyrics and comment length
* reorder assertions
Signed-off-by: Deluan <deluan@navidrome.org>
---------
Signed-off-by: Deluan <deluan@navidrome.org>
Co-authored-by: Deluan <deluan@navidrome.org>
* fix(server): Expose library_path for playlist (#3705)
Allows showing absolute path for UI, and makes "report real path" work for playlists (Subsonic)
* fix BFR on Windows (#3704)
* fix potential reflected cross-site scripting vulnerability
Signed-off-by: Deluan <deluan@navidrome.org>
* hack to make it work on Windows
* ignore windows executables
* try fixing the pipeline
Signed-off-by: Deluan <deluan@navidrome.org>
* allow MusicFolder in other drives
* move windows local drive logic to local storage implementation
---------
Signed-off-by: Deluan <deluan@navidrome.org>
* increase pagination sizes for missing files
Signed-off-by: Deluan <deluan@navidrome.org>
* reduce level of "already scanning" watcher log message
Signed-off-by: Deluan <deluan@navidrome.org>
* only count folders with audio files in it
See https://github.com/navidrome/navidrome/discussions/3676#discussioncomment-11990930
Signed-off-by: Deluan <deluan@navidrome.org>
* add album version and catalog number to search
Signed-off-by: Deluan <deluan@navidrome.org>
* add `organization` alias for `recordlabel`
Signed-off-by: Deluan <deluan@navidrome.org>
* remove mbid from Last.fm agent
Signed-off-by: Deluan <deluan@navidrome.org>
* feat: support inspect in ui (#3726)
* inspect in ui
* address round 1
* add catalogNum to AlbumInfo
Signed-off-by: Deluan <deluan@navidrome.org>
* remove dependency on metadata_old (deprecated) package
Signed-off-by: Deluan <deluan@navidrome.org>
* add `RawTags` to model
Signed-off-by: Deluan <deluan@navidrome.org>
* support parsing MBIDs for roles (from the https://github.com/kgarner7/picard-all-mbids plugin) (#3698)
* parse standard roles, vorbis/m4a work for now
* fix djmixer
* working roles, use DJ-mix
* add performers to file
* map mbids
* add a few more tests
* add test
Signed-off-by: Deluan <deluan@navidrome.org>
* try to simplify the performers logic
Signed-off-by: Deluan <deluan@navidrome.org>
* stylistic changes
---------
Signed-off-by: Deluan <deluan@navidrome.org>
Co-authored-by: Deluan <deluan@navidrome.org>
* remove param mutation
Signed-off-by: Deluan <deluan@navidrome.org>
* run automated SQLite optimizations
Signed-off-by: Deluan <deluan@navidrome.org>
* fix playlists import/export on Windows
* fix import playlists
* fix export playlists
* better handling of Windows volumes
Signed-off-by: Deluan <deluan@navidrome.org>
* handle more album ID reassignments
Signed-off-by: Deluan <deluan@navidrome.org>
* allow adding/overriding tags in the config file
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(ui): Fix playlist track id, handle missing tracks better (#3734)
- Use `mediaFileId` instead of `id` for playlist tracks
- Only fetch if the file is not missing
- If extractor fails to get the file, also error (rather than panic)
* optimize DB after each scan.
Signed-off-by: Deluan <deluan@navidrome.org>
* remove sortable from AlbumSongs columns
Signed-off-by: Deluan <deluan@navidrome.org>
* simplify query to get missing tracks
Signed-off-by: Deluan <deluan@navidrome.org>
* mark Scanner.Extractor as deprecated
Signed-off-by: Deluan <deluan@navidrome.org>
---------
Signed-off-by: Deluan <deluan@navidrome.org>
Signed-off-by: Henrik Nordvik <henrikno@gmail.com>
Co-authored-by: Caio Cotts <caio@cotts.com.br>
Co-authored-by: Henrik Nordvik <henrikno@gmail.com>
Co-authored-by: Kendall Garner <17521368+kgarner7@users.noreply.github.com>
2025-02-19 17:35:17 -08:00
|
|
|
plRepo.On("GetWithTracks", "1", true, false).Return(pls, nil)
|
2026-09-25 18:06:10 -04:00
|
|
|
ds.On("Playlist").Return(plRepo)
|
2026-03-09 22:22:58 -04:00
|
|
|
ms.On("NewStream", mock.Anything, mock.Anything, stream.Request{Format: "mp3", BitRate: 128}).Return(io.NopCloser(strings.NewReader("test")), nil).Times(2)
|
2023-03-26 19:34:12 -04:00
|
|
|
|
|
|
|
|
out := new(bytes.Buffer)
|
|
|
|
|
err := arch.ZipPlaylist(context.Background(), "1", "mp3", 128, out)
|
|
|
|
|
Expect(err).To(BeNil())
|
|
|
|
|
|
|
|
|
|
zr, err := zip.NewReader(bytes.NewReader(out.Bytes()), int64(out.Len()))
|
|
|
|
|
Expect(err).To(BeNil())
|
|
|
|
|
|
2025-05-27 20:13:37 -04:00
|
|
|
Expect(len(zr.File)).To(Equal(3))
|
2023-05-16 18:34:15 -04:00
|
|
|
Expect(zr.File[0].Name).To(Equal("01 - AC_DC - track1.mp3"))
|
2023-03-26 19:34:12 -04:00
|
|
|
Expect(zr.File[1].Name).To(Equal("02 - Artist 2 - track2.mp3"))
|
2025-05-27 20:13:37 -04:00
|
|
|
Expect(zr.File[2].Name).To(Equal("Test Playlist.m3u"))
|
|
|
|
|
|
|
|
|
|
// Verify M3U content
|
|
|
|
|
m3uFile, err := zr.File[2].Open()
|
|
|
|
|
Expect(err).To(BeNil())
|
|
|
|
|
defer m3uFile.Close()
|
|
|
|
|
|
|
|
|
|
m3uContent, err := io.ReadAll(m3uFile)
|
|
|
|
|
Expect(err).To(BeNil())
|
|
|
|
|
|
|
|
|
|
expectedM3U := "#EXTM3U\n#PLAYLIST:Test Playlist\n#EXTINF:0,AC/DC - track1\n01 - AC_DC - track1.mp3\n#EXTINF:0,Artist 2 - track2\n02 - Artist 2 - track2.mp3\n"
|
|
|
|
|
Expect(string(m3uContent)).To(Equal(expectedM3U))
|
2023-03-26 19:34:12 -04:00
|
|
|
})
|
|
|
|
|
})
|
feat(archiver): add folder cover image to downloaded zips (#6224)
* feat(archiver): add folder cover image to downloaded zips
Album, artist, playlist and share downloads now include the item's cover as
folder.<ext>, the image most players and car stereos show for the files next to
it. This helps users who copy transcoded downloads to offline devices, since
transcoding drops the embedded artwork (#5841).
Album folders get the album cover, the artist zip root gets the artist image,
and playlist and share zips get the playlist or shared item's cover at the root.
The image is the same one getCoverArt serves, resized to 500px (not square),
and named by its detected type. Items without artwork get no image, and a cover
that fails to load is logged and skipped so it never breaks the archive.
The archiver reads covers through a new core.CoverArtReader interface,
implemented by artwork.CoverArtReader, to avoid an import cycle between core
and core/artwork.
* refactor(archiver): read covers through artwork.Artwork directly
The archiver no longer needs a local CoverArtReader interface and adapter.
The only reason core/artwork imported core was a core.AbsolutePath call in
loadArtistFolder, which now reads the library path from the repository and
cleans it the same way. With the import cycle gone, the archiver takes
artwork.Artwork and treats ErrUnavailable and ErrNotFound as no cover.
Covers are now written after each album's tracks (and after all tracks for
the archive root), so a slow artwork lookup does not delay the first bytes
of the download.
* fix(archiver): read share covers as admin so private playlists keep theirs
Public share downloads run with an anonymous context, and the playlist
repository hides private playlists from anonymous users, so a zip of a shared
private playlist silently had no folder image. Like the public image handler,
the share itself is the authorization: the cover lookup now runs with an admin
user. Only the cover read is elevated; streaming keeps the anonymous context,
so the transcode limiter still keys public downloads the same way.
* style(archiver): trim comments
Shorten the comments added by the folder cover image change and drop the ones the names already explain.
* fix(archiver): add one cover per album folder in artist zips
Albums with the same name share a zip folder (pre-existing naming), so an
artist zip with two such albums wrote two folder.<ext> entries at the same
path. Keep the first cover and skip the rest for that folder.
2026-09-25 16:42:49 -04:00
|
|
|
Context("cover art", func() {
|
|
|
|
|
var (
|
|
|
|
|
jpegData = []byte("\xff\xd8\xff\xe0 fake jpeg")
|
|
|
|
|
pngData = []byte("\x89PNG\x0d\x0a\x1a\x0a fake png")
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
mockAlbumTracks := func(filter squirrel.Sqlizer, mfs model.MediaFiles) {
|
|
|
|
|
mfRepo := &mockMediaFileRepository{}
|
|
|
|
|
mfRepo.On("GetAll", []model.QueryOptions{{Filters: filter, Sort: "album"}}).Return(mfs, nil)
|
|
|
|
|
ds.On("MediaFile", mock.Anything).Return(mfRepo)
|
|
|
|
|
ms.On("NewStream", mock.Anything, mock.Anything, mock.Anything).Return(io.NopCloser(strings.NewReader("test")), nil)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It("adds the album cover to the album folder", func() {
|
|
|
|
|
ca.images["al-1"] = jpegData
|
|
|
|
|
mockAlbumTracks(squirrel.Eq{"album_id": "1"}, model.MediaFiles{
|
|
|
|
|
{Path: "test_data/01 - track1.mp3", Suffix: "mp3", AlbumID: "1", Album: "Album/Promo", DiscNumber: 1},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
out := new(bytes.Buffer)
|
|
|
|
|
Expect(arch.ZipAlbum(context.Background(), "1", "mp3", 128, out)).To(Succeed())
|
|
|
|
|
|
|
|
|
|
files := readZip(out)
|
|
|
|
|
Expect(files).To(HaveLen(2))
|
|
|
|
|
Expect(files).To(HaveKeyWithValue("Album_Promo/folder.jpg", jpegData))
|
|
|
|
|
Expect(ca.requests).To(ConsistOf(coverRequest{id: "al-1", size: 500, square: false}))
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
It("adds the artist image to the root and each album cover to its folder", func() {
|
|
|
|
|
ca.images["ar-1"] = pngData
|
|
|
|
|
ca.images["al-1"] = jpegData
|
|
|
|
|
ca.images["al-2"] = jpegData
|
|
|
|
|
mockAlbumTracks(squirrel.And{
|
|
|
|
|
persistence.ParticipantIDFilter("media_file", "1", model.RoleAlbumArtist),
|
|
|
|
|
squirrel.Eq{"missing": false},
|
|
|
|
|
}, model.MediaFiles{
|
|
|
|
|
{Path: "test_data/01 - track1.mp3", Suffix: "mp3", AlbumID: "1", Album: "Album 1", DiscNumber: 1},
|
|
|
|
|
{Path: "test_data/02 - track2.mp3", Suffix: "mp3", AlbumID: "2", Album: "Album 2", DiscNumber: 1},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
out := new(bytes.Buffer)
|
|
|
|
|
Expect(arch.ZipArtist(context.Background(), "1", "mp3", 128, out)).To(Succeed())
|
|
|
|
|
|
|
|
|
|
files := readZip(out)
|
|
|
|
|
Expect(files).To(HaveLen(5))
|
|
|
|
|
Expect(files).To(HaveKeyWithValue("folder.png", pngData))
|
|
|
|
|
Expect(files).To(HaveKeyWithValue("Album 1/folder.jpg", jpegData))
|
|
|
|
|
Expect(files).To(HaveKeyWithValue("Album 2/folder.jpg", jpegData))
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
It("puts each same-named album's cover in that album's own folder", func() {
|
|
|
|
|
ca.images["al-1"] = jpegData
|
|
|
|
|
ca.images["al-2"] = pngData
|
|
|
|
|
mockAlbumTracks(squirrel.And{
|
|
|
|
|
persistence.ParticipantIDFilter("media_file", "1", model.RoleAlbumArtist),
|
|
|
|
|
squirrel.Eq{"missing": false},
|
|
|
|
|
}, model.MediaFiles{
|
|
|
|
|
{Path: "test_data/01 - track1.mp3", Suffix: "mp3", AlbumID: "1", Album: "Greatest Hits", Year: 2001, DiscNumber: 1},
|
|
|
|
|
{Path: "test_data/02 - track2.mp3", Suffix: "mp3", AlbumID: "2", Album: "Greatest Hits", Year: 2005, DiscNumber: 1},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
out := new(bytes.Buffer)
|
|
|
|
|
Expect(arch.ZipArtist(context.Background(), "1", "mp3", 128, out)).To(Succeed())
|
|
|
|
|
|
|
|
|
|
files := readZip(out)
|
|
|
|
|
Expect(files).To(HaveKeyWithValue("Greatest Hits [2001]/folder.jpg", jpegData))
|
|
|
|
|
Expect(files).To(HaveKeyWithValue("Greatest Hits [2005]/folder.png", pngData))
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
It("adds the playlist cover to the root", func() {
|
|
|
|
|
ca.images["pl-1"] = jpegData
|
|
|
|
|
plRepo := &mockPlaylistRepository{}
|
|
|
|
|
plRepo.On("GetWithTracks", "1", true, false).Return(&model.Playlist{
|
|
|
|
|
ID: "1",
|
|
|
|
|
Name: "Test Playlist",
|
|
|
|
|
Tracks: []model.PlaylistTrack{
|
|
|
|
|
{MediaFile: model.MediaFile{Path: "test_data/01 - track1.mp3", Suffix: "mp3", AlbumID: "1", Artist: "Artist 1", Title: "track1"}},
|
|
|
|
|
},
|
|
|
|
|
}, nil)
|
|
|
|
|
ds.On("Playlist", mock.Anything).Return(plRepo)
|
|
|
|
|
ms.On("NewStream", mock.Anything, mock.Anything, mock.Anything).Return(io.NopCloser(strings.NewReader("test")), nil)
|
|
|
|
|
|
|
|
|
|
out := new(bytes.Buffer)
|
|
|
|
|
Expect(arch.ZipPlaylist(context.Background(), "1", "mp3", 128, out)).To(Succeed())
|
|
|
|
|
|
|
|
|
|
files := readZip(out)
|
|
|
|
|
Expect(files).To(HaveLen(3))
|
|
|
|
|
Expect(files).To(HaveKeyWithValue("folder.jpg", jpegData))
|
|
|
|
|
Expect(files).To(HaveKey("Test Playlist.m3u"))
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
It("adds the shared item's cover to the root, even for a private playlist", func() {
|
|
|
|
|
ca.images["pl-10"] = jpegData
|
|
|
|
|
ms.On("NewStream", mock.Anything, mock.Anything, mock.Anything).Return(io.NopCloser(strings.NewReader("test")), nil)
|
|
|
|
|
share := &model.Share{
|
|
|
|
|
ID: "1",
|
|
|
|
|
Downloadable: true,
|
|
|
|
|
Format: "mp3",
|
|
|
|
|
MaxBitRate: 128,
|
|
|
|
|
ResourceType: "playlist",
|
|
|
|
|
ResourceIDs: "10",
|
|
|
|
|
Tracks: model.MediaFiles{
|
|
|
|
|
{ID: "1", Path: "test_data/01 - track1.mp3", Suffix: "mp3", Artist: "Artist 1", Title: "track1"},
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
out := new(bytes.Buffer)
|
|
|
|
|
Expect(arch.ZipShare(context.Background(), share, out)).To(Succeed())
|
|
|
|
|
|
|
|
|
|
files := readZip(out)
|
|
|
|
|
Expect(files).To(HaveLen(2))
|
|
|
|
|
Expect(files).To(HaveKeyWithValue("folder.jpg", jpegData))
|
|
|
|
|
Expect(ca.requests).To(ConsistOf(coverRequest{id: "pl-10", size: 500, square: false, admin: true}))
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
It("still builds the archive when the cover cannot be read", func() {
|
|
|
|
|
ca.err = errors.New("boom")
|
|
|
|
|
mockAlbumTracks(squirrel.Eq{"album_id": "1"}, model.MediaFiles{
|
|
|
|
|
{Path: "test_data/01 - track1.mp3", Suffix: "mp3", AlbumID: "1", Album: "Album", DiscNumber: 1},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
out := new(bytes.Buffer)
|
|
|
|
|
Expect(arch.ZipAlbum(context.Background(), "1", "mp3", 128, out)).To(Succeed())
|
|
|
|
|
|
|
|
|
|
files := readZip(out)
|
|
|
|
|
Expect(files).To(HaveLen(1))
|
|
|
|
|
Expect(files).To(HaveKey("Album/01 - track1.mp3"))
|
|
|
|
|
})
|
|
|
|
|
})
|
2023-03-26 19:34:12 -04:00
|
|
|
})
|
|
|
|
|
|
feat(archiver): add folder cover image to downloaded zips (#6224)
* feat(archiver): add folder cover image to downloaded zips
Album, artist, playlist and share downloads now include the item's cover as
folder.<ext>, the image most players and car stereos show for the files next to
it. This helps users who copy transcoded downloads to offline devices, since
transcoding drops the embedded artwork (#5841).
Album folders get the album cover, the artist zip root gets the artist image,
and playlist and share zips get the playlist or shared item's cover at the root.
The image is the same one getCoverArt serves, resized to 500px (not square),
and named by its detected type. Items without artwork get no image, and a cover
that fails to load is logged and skipped so it never breaks the archive.
The archiver reads covers through a new core.CoverArtReader interface,
implemented by artwork.CoverArtReader, to avoid an import cycle between core
and core/artwork.
* refactor(archiver): read covers through artwork.Artwork directly
The archiver no longer needs a local CoverArtReader interface and adapter.
The only reason core/artwork imported core was a core.AbsolutePath call in
loadArtistFolder, which now reads the library path from the repository and
cleans it the same way. With the import cycle gone, the archiver takes
artwork.Artwork and treats ErrUnavailable and ErrNotFound as no cover.
Covers are now written after each album's tracks (and after all tracks for
the archive root), so a slow artwork lookup does not delay the first bytes
of the download.
* fix(archiver): read share covers as admin so private playlists keep theirs
Public share downloads run with an anonymous context, and the playlist
repository hides private playlists from anonymous users, so a zip of a shared
private playlist silently had no folder image. Like the public image handler,
the share itself is the authorization: the cover lookup now runs with an admin
user. Only the cover read is elevated; streaming keeps the anonymous context,
so the transcode limiter still keys public downloads the same way.
* style(archiver): trim comments
Shorten the comments added by the folder cover image change and drop the ones the names already explain.
* fix(archiver): add one cover per album folder in artist zips
Albums with the same name share a zip folder (pre-existing naming), so an
artist zip with two such albums wrote two folder.<ext> entries at the same
path. Keep the first cover and skip the rest for that folder.
2026-09-25 16:42:49 -04:00
|
|
|
func readZip(out *bytes.Buffer) map[string][]byte {
|
|
|
|
|
zr, err := zip.NewReader(bytes.NewReader(out.Bytes()), int64(out.Len()))
|
|
|
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
|
files := make(map[string][]byte, len(zr.File))
|
|
|
|
|
for _, f := range zr.File {
|
|
|
|
|
r, err := f.Open()
|
|
|
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
|
data, err := io.ReadAll(r)
|
|
|
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
|
_ = r.Close()
|
|
|
|
|
files[f.Name] = data
|
|
|
|
|
}
|
|
|
|
|
return files
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type coverRequest struct {
|
|
|
|
|
id string
|
|
|
|
|
size int
|
|
|
|
|
square bool
|
|
|
|
|
admin bool
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type mockCoverArt struct {
|
|
|
|
|
artwork.Artwork
|
|
|
|
|
images map[string][]byte
|
|
|
|
|
err error
|
|
|
|
|
requests []coverRequest
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (m *mockCoverArt) Get(ctx context.Context, artID model.ArtworkID, size int, square bool) (*artwork.Image, error) {
|
|
|
|
|
user, _ := request.UserFrom(ctx)
|
|
|
|
|
m.requests = append(m.requests, coverRequest{id: artID.String(), size: size, square: square, admin: user.IsAdmin})
|
|
|
|
|
if m.err != nil {
|
|
|
|
|
return nil, m.err
|
|
|
|
|
}
|
|
|
|
|
data, ok := m.images[artID.String()]
|
|
|
|
|
if !ok {
|
|
|
|
|
return nil, artwork.ErrUnavailable
|
|
|
|
|
}
|
|
|
|
|
return &artwork.Image{ReadCloser: io.NopCloser(bytes.NewReader(data))}, nil
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-26 19:34:12 -04:00
|
|
|
type mockDataStore struct {
|
|
|
|
|
mock.Mock
|
|
|
|
|
model.DataStore
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-25 18:06:10 -04:00
|
|
|
func (m *mockDataStore) MediaFile() model.MediaFileRepository {
|
|
|
|
|
args := m.Called()
|
2023-03-26 19:34:12 -04:00
|
|
|
return args.Get(0).(model.MediaFileRepository)
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-25 18:06:10 -04:00
|
|
|
func (m *mockDataStore) Playlist() model.PlaylistRepository {
|
|
|
|
|
args := m.Called()
|
2023-03-26 19:34:12 -04:00
|
|
|
return args.Get(0).(model.PlaylistRepository)
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-25 18:06:10 -04:00
|
|
|
func (m *mockDataStore) Library() model.LibraryRepository {
|
feat(bfr): Big Refactor: new scanner, lots of new fields and tags, improvements and DB schema changes (#2709)
* fix(server): more race conditions when updating artist/album from external sources
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(scanner): add .gitignore syntax to .ndignore. Resolves #1394
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(ui): null
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(scanner): pass configfile option to child process
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(scanner): resume interrupted fullScans
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(scanner): remove old scanner code
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(scanner): rename old metadata package
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(scanner): move old metadata package
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: tests
Signed-off-by: Deluan <deluan@navidrome.org>
* chore(deps): update Go to 1.23.4
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: logs
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(test):
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: log level
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: remove log message
Signed-off-by: Deluan <deluan@navidrome.org>
* feat: add config for scanner watcher
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: children playlists
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: replace `interface{}` with `any`
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: smart playlists with genres
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: allow any tags in smart playlists
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: artist names in playlists
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: smart playlist's sort by tags
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(subsonic): add moods to child
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(subsonic): add moods to AlbumID3
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor(subsonic): use generic JSONArray for OS arrays
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor(subsonic): use https in test
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(subsonic): add releaseTypes to AlbumID3
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(subsonic): add recordLabels to AlbumID3
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor(subsonic): rename JSONArray to Array
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(subsonic): add artists to AlbumID3
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(subsonic): add artists to Child
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(scanner): do not pre-populate smart playlists
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(subsonic): implement a simplified version of ArtistID3.
See https://github.com/opensubsonic/open-subsonic-api/discussions/120
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(subsonic): add artists to album child
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(subsonic): add contributors to mediafile Child
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(subsonic): add albumArtists to mediafile Child
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(subsonic): add displayArtist and displayAlbumArtist
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(subsonic): add displayComposer to Child
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(subsonic): add roles to ArtistID3
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(subsonic): use " • " separator for displayComposer
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor:
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(subsonic):
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(subsonic): respect `PreferSortTags` config option
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor(subsonic):
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: optimize purging non-unused tags
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: don't run 'refresh artist stats' concurrently with other transactions
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor:
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: log message
Signed-off-by: Deluan <deluan@navidrome.org>
* feat: add Scanner.ScanOnStartup config option, default true
Signed-off-by: Deluan <deluan@navidrome.org>
* feat: better json parsing error msg when importing NSPs
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: don't update album's imported_time when updating external_metadata
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: handle interrupted scans and full scans after migrations
Signed-off-by: Deluan <deluan@navidrome.org>
* feat: run `analyze` when migration requires a full rescan
Signed-off-by: Deluan <deluan@navidrome.org>
* feat: run `PRAGMA optimize` at the end of the scan
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: don't update artist's updated_at when updating external_metadata
Signed-off-by: Deluan <deluan@navidrome.org>
* feat: handle multiple artists and roles in smart playlists
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(ui): dim missing tracks
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: album missing logic
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: error encoding in gob
Signed-off-by: Deluan <deluan@navidrome.org>
* feat: separate warnings from errors
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: mark albums as missing if they were contained in a deleted folder
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: add participant names to media_file and album tables
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: use participations in criteria, instead of m2m relationship
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: rename participations to participants
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(subsonic): add moods to album child
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: albumartist role case
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(scanner): run scanner as an external process by default
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(ui): show albumArtist names
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(ui): dim out missing albums
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: flaky test
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(server): scrobble buffer mapping. fix #3583
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: more participations renaming
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: listenbrainz scrobbling
Signed-off-by: Deluan <deluan@navidrome.org>
* feat: send release_group_mbid to listenbrainz
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(subsonic): implement OpenSubsonic explicitStatus field (#3597)
* feat: implement OpenSubsonic explicitStatus field
* fix(subsonic): fix failing snapshot tests
* refactor: create helper for setting explicitStatus
* fix: store smaller values for explicit-status on database
* test: ToAlbum explicitStatus
* refactor: rename explicitStatus helper function
---------
Co-authored-by: Deluan Quintão <deluan@navidrome.org>
* fix: handle album and track tags in the DB based on the mappings.yaml file
Signed-off-by: Deluan <deluan@navidrome.org>
* save similar artists as JSONB
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: getAlbumList byGenre
Signed-off-by: Deluan <deluan@navidrome.org>
* detect changes in PID configuration
Signed-off-by: Deluan <deluan@navidrome.org>
* set default album PID to legacy_pid
Signed-off-by: Deluan <deluan@navidrome.org>
* fix tests
Signed-off-by: Deluan <deluan@navidrome.org>
* fix SIGSEGV
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: don't lose album stars/ratings when migrating
Signed-off-by: Deluan <deluan@navidrome.org>
* store full PID conf in properties
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: keep album annotations when changing PID.Album config
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: reassign album annotations
Signed-off-by: Deluan <deluan@navidrome.org>
* feat: use (display) albumArtist and add links to each artist
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: not showing albums by albumartist
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: error msgs
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: hide PID from Native API
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: album cover art resolution
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: trim participant names
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: reduce watcher log spam
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: panic when initializing the watcher
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: various artists
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: don't store empty lyrics in the DB
Signed-off-by: Deluan <deluan@navidrome.org>
* remove unused methods
Signed-off-by: Deluan <deluan@navidrome.org>
* drop full_text indexes, as they are not being used by SQLite
Signed-off-by: Deluan <deluan@navidrome.org>
* keep album created_at when upgrading
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(ui): null pointer
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: album artwork cache
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: don't expose missing files in Subsonic API
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: searchable interface
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: filter out missing items from subsonic search
* fix: filter out missing items from playlists
* fix: filter out missing items from shares
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(ui): add filter by artist role
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(subsonic): only return albumartists in getIndexes and getArtists endpoints
Signed-off-by: Deluan <deluan@navidrome.org>
* sort roles alphabetically
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: artist playcounts
Signed-off-by: Deluan <deluan@navidrome.org>
* change default Album PID conf
Signed-off-by: Deluan <deluan@navidrome.org>
* fix albumartist link when it does not match any albumartists values
Signed-off-by: Deluan <deluan@navidrome.org>
* fix `Ignoring filter not whitelisted` (role) message
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: trim any names/titles being imported
Signed-off-by: Deluan <deluan@navidrome.org>
* remove unused genre code
Signed-off-by: Deluan <deluan@navidrome.org>
* serialize calls to Last.fm's getArtist
Signed-off-by: Deluan <deluan@navidrome.org>
xxx
Signed-off-by: Deluan <deluan@navidrome.org>
* add counters to genres
Signed-off-by: Deluan <deluan@navidrome.org>
* nit: fix migration `notice` message
Signed-off-by: Deluan <deluan@navidrome.org>
* optimize similar artists query
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: last.fm.getInfo when mbid does not exist
Signed-off-by: Deluan <deluan@navidrome.org>
* ui only show missing items for admins
Signed-off-by: Deluan <deluan@navidrome.org>
* don't allow interaction with missing items
Signed-off-by: Deluan <deluan@navidrome.org>
* Add Missing Files view (WIP)
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: merged tag_counts into tag table
Signed-off-by: Deluan <deluan@navidrome.org>
* add option to completely disable automatic scanner
Signed-off-by: Deluan <deluan@navidrome.org>
* add delete missing files functionality
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: playlists not showing for regular users
Signed-off-by: Deluan <deluan@navidrome.org>
* reduce updateLastAccess frequency to once every minute
Signed-off-by: Deluan <deluan@navidrome.org>
* reduce update player frequency to once every minute
Signed-off-by: Deluan <deluan@navidrome.org>
* add timeout when updating player
Signed-off-by: Deluan <deluan@navidrome.org>
* remove dead code
Signed-off-by: Deluan <deluan@navidrome.org>
* fix duplicated roles in stats
Signed-off-by: Deluan <deluan@navidrome.org>
* add `; ` to artist splitters
Signed-off-by: Deluan <deluan@navidrome.org>
* fix stats query
Signed-off-by: Deluan <deluan@navidrome.org>
* more logs
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: support legacy clients (DSub) by removing OpenSubsonic extra fields - WIP
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: support legacy clients (DSub) by removing OpenSubsonic extra fields - WIP
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: support legacy clients (DSub) by removing OpenSubsonic extra fields - WIP
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: support legacy clients (DSub) by removing OpenSubsonic extra fields - WIP
Signed-off-by: Deluan <deluan@navidrome.org>
* add record label filter
Signed-off-by: Deluan <deluan@navidrome.org>
* add release type filter
Signed-off-by: Deluan <deluan@navidrome.org>
* fix purgeUnused tags
Signed-off-by: Deluan <deluan@navidrome.org>
* add grouping filter to albums
Signed-off-by: Deluan <deluan@navidrome.org>
* allow any album tags to be used in as filters in the API
Signed-off-by: Deluan <deluan@navidrome.org>
* remove empty tags from album info
Signed-off-by: Deluan <deluan@navidrome.org>
* comments in the migration
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: Cannot read properties of undefined
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: listenbrainz scrobbling (#3640)
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: remove duplicated tag values
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: don't ignore the taglib folder!
Signed-off-by: Deluan <deluan@navidrome.org>
* feat: show track subtitle tag
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: show artists stats based on selected role
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: inspect
Signed-off-by: Deluan <deluan@navidrome.org>
* add media type to album info/filters
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: change format of subtitle in the UI
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: subtitle in Subsonic API and search
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: subtitle in UI's player
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: split strings should be case-insensitive
Signed-off-by: Deluan <deluan@navidrome.org>
* disable ScanSchedule
Signed-off-by: Deluan <deluan@navidrome.org>
* increase default sessiontimeout
Signed-off-by: Deluan <deluan@navidrome.org>
* add sqlite command line tool to docker image
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: resources override
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: album PID conf
Signed-off-by: Deluan <deluan@navidrome.org>
* change migration to mark current artists as albumArtists
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(ui): Allow filtering on multiple genres (#3679)
* feat(ui): Allow filtering on multiple genres
Signed-off-by: Henrik Nordvik <henrikno@gmail.com>
Signed-off-by: Deluan <deluan@navidrome.org>
* add multi-genre filter in Album list
Signed-off-by: Deluan <deluan@navidrome.org>
---------
Signed-off-by: Henrik Nordvik <henrikno@gmail.com>
Signed-off-by: Deluan <deluan@navidrome.org>
Co-authored-by: Henrik Nordvik <henrikno@gmail.com>
* add more multi-valued tag filters to Album and Song views
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(ui): unselect missing files after removing
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(ui): song filter
Signed-off-by: Deluan <deluan@navidrome.org>
* fix sharing tracks. fix #3687
Signed-off-by: Deluan <deluan@navidrome.org>
* use rowids when using search for sync (ex: Symfonium)
Signed-off-by: Deluan <deluan@navidrome.org>
* fix "Report Real Paths" option for subsonic clients
Signed-off-by: Deluan <deluan@navidrome.org>
* fix "Report Real Paths" option for subsonic clients for search
Signed-off-by: Deluan <deluan@navidrome.org>
* add libraryPath to Native API /songs endpoint
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(subsonic): add album version
Signed-off-by: Deluan <deluan@navidrome.org>
* made all tags lowercase as they are case-insensitive anyways.
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(ui): Show full paths, extended properties for album/song (#3691)
* feat(ui): Show full paths, extended properties for album/song
- uses library path + os separator + path
- show participants (album/song) and tags (song)
- make album/participant clickable in show info
* add source to path
* fix pathSeparator in UI
Signed-off-by: Deluan <deluan@navidrome.org>
* fix local artist artwork (#3695)
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: parse vorbis performers
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: clean function into smaller functions
Signed-off-by: Deluan <deluan@navidrome.org>
* fix translations for en and pt
Signed-off-by: Deluan <deluan@navidrome.org>
* add trace log to show annotations reassignment
Signed-off-by: Deluan <deluan@navidrome.org>
* add trace log to show annotations reassignment
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: allow performers without instrument/subrole
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: metadata clean function again
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: optimize split function
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: split function is now a method of TagConf
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: humanize Artist total size
Signed-off-by: Deluan <deluan@navidrome.org>
* add album version to album details
Signed-off-by: Deluan <deluan@navidrome.org>
* don't display album-level tags in SongInfo
Signed-off-by: Deluan <deluan@navidrome.org>
* fix genre clicking in Album Page
Signed-off-by: Deluan <deluan@navidrome.org>
* don't use mbids in Last.fm api calls.
From https://discord.com/channels/671335427726114836/704303730660737113/1337574018143879248:
With MBID:
```
GET https://ws.audioscrobbler.com/2.0/?api_key=XXXX&artist=Van+Morrison&format=json&lang=en&mbid=a41ac10f-0a56-4672-9161-b83f9b223559&method=artist.getInfo
{
artist: {
name: "Bee Gees",
mbid: "bf0f7e29-dfe1-416c-b5c6-f9ebc19ea810",
url: "https://www.last.fm/music/Bee+Gees",
}
```
Without MBID:
```
GET https://ws.audioscrobbler.com/2.0/?api_key=XXXX&artist=Van+Morrison&format=json&lang=en&method=artist.getInfo
{
artist: {
name: "Van Morrison",
mbid: "a41ac10f-0a56-4672-9161-b83f9b223559",
url: "https://www.last.fm/music/Van+Morrison",
}
```
Signed-off-by: Deluan <deluan@navidrome.org>
* better logging for when the artist folder is not found
Signed-off-by: Deluan <deluan@navidrome.org>
* fix various issues with artist image resolution
Signed-off-by: Deluan <deluan@navidrome.org>
* hide "Additional Tags" header if there are none.
Signed-off-by: Deluan <deluan@navidrome.org>
* simplify tag rendering
Signed-off-by: Deluan <deluan@navidrome.org>
* enhance logging for artist folder detection
Signed-off-by: Deluan <deluan@navidrome.org>
* make folderID consistent for relative and absolute folderPaths
Signed-off-by: Deluan <deluan@navidrome.org>
* handle more folder paths scenarios
Signed-off-by: Deluan <deluan@navidrome.org>
* filter out other roles when SubsonicArtistParticipations = true
Signed-off-by: Deluan <deluan@navidrome.org>
* fix "Cannot read properties of undefined"
Signed-off-by: Deluan <deluan@navidrome.org>
* fix lyrics and comments being truncated (#3701)
* fix lyrics and comments being truncated
* specifically test for lyrics and comment length
* reorder assertions
Signed-off-by: Deluan <deluan@navidrome.org>
---------
Signed-off-by: Deluan <deluan@navidrome.org>
Co-authored-by: Deluan <deluan@navidrome.org>
* fix(server): Expose library_path for playlist (#3705)
Allows showing absolute path for UI, and makes "report real path" work for playlists (Subsonic)
* fix BFR on Windows (#3704)
* fix potential reflected cross-site scripting vulnerability
Signed-off-by: Deluan <deluan@navidrome.org>
* hack to make it work on Windows
* ignore windows executables
* try fixing the pipeline
Signed-off-by: Deluan <deluan@navidrome.org>
* allow MusicFolder in other drives
* move windows local drive logic to local storage implementation
---------
Signed-off-by: Deluan <deluan@navidrome.org>
* increase pagination sizes for missing files
Signed-off-by: Deluan <deluan@navidrome.org>
* reduce level of "already scanning" watcher log message
Signed-off-by: Deluan <deluan@navidrome.org>
* only count folders with audio files in it
See https://github.com/navidrome/navidrome/discussions/3676#discussioncomment-11990930
Signed-off-by: Deluan <deluan@navidrome.org>
* add album version and catalog number to search
Signed-off-by: Deluan <deluan@navidrome.org>
* add `organization` alias for `recordlabel`
Signed-off-by: Deluan <deluan@navidrome.org>
* remove mbid from Last.fm agent
Signed-off-by: Deluan <deluan@navidrome.org>
* feat: support inspect in ui (#3726)
* inspect in ui
* address round 1
* add catalogNum to AlbumInfo
Signed-off-by: Deluan <deluan@navidrome.org>
* remove dependency on metadata_old (deprecated) package
Signed-off-by: Deluan <deluan@navidrome.org>
* add `RawTags` to model
Signed-off-by: Deluan <deluan@navidrome.org>
* support parsing MBIDs for roles (from the https://github.com/kgarner7/picard-all-mbids plugin) (#3698)
* parse standard roles, vorbis/m4a work for now
* fix djmixer
* working roles, use DJ-mix
* add performers to file
* map mbids
* add a few more tests
* add test
Signed-off-by: Deluan <deluan@navidrome.org>
* try to simplify the performers logic
Signed-off-by: Deluan <deluan@navidrome.org>
* stylistic changes
---------
Signed-off-by: Deluan <deluan@navidrome.org>
Co-authored-by: Deluan <deluan@navidrome.org>
* remove param mutation
Signed-off-by: Deluan <deluan@navidrome.org>
* run automated SQLite optimizations
Signed-off-by: Deluan <deluan@navidrome.org>
* fix playlists import/export on Windows
* fix import playlists
* fix export playlists
* better handling of Windows volumes
Signed-off-by: Deluan <deluan@navidrome.org>
* handle more album ID reassignments
Signed-off-by: Deluan <deluan@navidrome.org>
* allow adding/overriding tags in the config file
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(ui): Fix playlist track id, handle missing tracks better (#3734)
- Use `mediaFileId` instead of `id` for playlist tracks
- Only fetch if the file is not missing
- If extractor fails to get the file, also error (rather than panic)
* optimize DB after each scan.
Signed-off-by: Deluan <deluan@navidrome.org>
* remove sortable from AlbumSongs columns
Signed-off-by: Deluan <deluan@navidrome.org>
* simplify query to get missing tracks
Signed-off-by: Deluan <deluan@navidrome.org>
* mark Scanner.Extractor as deprecated
Signed-off-by: Deluan <deluan@navidrome.org>
---------
Signed-off-by: Deluan <deluan@navidrome.org>
Signed-off-by: Henrik Nordvik <henrikno@gmail.com>
Co-authored-by: Caio Cotts <caio@cotts.com.br>
Co-authored-by: Henrik Nordvik <henrikno@gmail.com>
Co-authored-by: Kendall Garner <17521368+kgarner7@users.noreply.github.com>
2025-02-19 17:35:17 -08:00
|
|
|
return &mockLibraryRepository{}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type mockLibraryRepository struct {
|
|
|
|
|
mock.Mock
|
|
|
|
|
model.LibraryRepository
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-25 18:06:10 -04:00
|
|
|
func (m *mockLibraryRepository) GetPath(_ context.Context, id int) (string, error) {
|
feat(bfr): Big Refactor: new scanner, lots of new fields and tags, improvements and DB schema changes (#2709)
* fix(server): more race conditions when updating artist/album from external sources
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(scanner): add .gitignore syntax to .ndignore. Resolves #1394
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(ui): null
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(scanner): pass configfile option to child process
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(scanner): resume interrupted fullScans
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(scanner): remove old scanner code
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(scanner): rename old metadata package
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(scanner): move old metadata package
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: tests
Signed-off-by: Deluan <deluan@navidrome.org>
* chore(deps): update Go to 1.23.4
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: logs
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(test):
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: log level
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: remove log message
Signed-off-by: Deluan <deluan@navidrome.org>
* feat: add config for scanner watcher
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: children playlists
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: replace `interface{}` with `any`
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: smart playlists with genres
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: allow any tags in smart playlists
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: artist names in playlists
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: smart playlist's sort by tags
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(subsonic): add moods to child
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(subsonic): add moods to AlbumID3
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor(subsonic): use generic JSONArray for OS arrays
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor(subsonic): use https in test
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(subsonic): add releaseTypes to AlbumID3
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(subsonic): add recordLabels to AlbumID3
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor(subsonic): rename JSONArray to Array
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(subsonic): add artists to AlbumID3
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(subsonic): add artists to Child
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(scanner): do not pre-populate smart playlists
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(subsonic): implement a simplified version of ArtistID3.
See https://github.com/opensubsonic/open-subsonic-api/discussions/120
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(subsonic): add artists to album child
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(subsonic): add contributors to mediafile Child
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(subsonic): add albumArtists to mediafile Child
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(subsonic): add displayArtist and displayAlbumArtist
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(subsonic): add displayComposer to Child
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(subsonic): add roles to ArtistID3
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(subsonic): use " • " separator for displayComposer
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor:
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(subsonic):
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(subsonic): respect `PreferSortTags` config option
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor(subsonic):
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: optimize purging non-unused tags
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: don't run 'refresh artist stats' concurrently with other transactions
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor:
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: log message
Signed-off-by: Deluan <deluan@navidrome.org>
* feat: add Scanner.ScanOnStartup config option, default true
Signed-off-by: Deluan <deluan@navidrome.org>
* feat: better json parsing error msg when importing NSPs
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: don't update album's imported_time when updating external_metadata
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: handle interrupted scans and full scans after migrations
Signed-off-by: Deluan <deluan@navidrome.org>
* feat: run `analyze` when migration requires a full rescan
Signed-off-by: Deluan <deluan@navidrome.org>
* feat: run `PRAGMA optimize` at the end of the scan
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: don't update artist's updated_at when updating external_metadata
Signed-off-by: Deluan <deluan@navidrome.org>
* feat: handle multiple artists and roles in smart playlists
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(ui): dim missing tracks
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: album missing logic
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: error encoding in gob
Signed-off-by: Deluan <deluan@navidrome.org>
* feat: separate warnings from errors
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: mark albums as missing if they were contained in a deleted folder
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: add participant names to media_file and album tables
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: use participations in criteria, instead of m2m relationship
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: rename participations to participants
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(subsonic): add moods to album child
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: albumartist role case
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(scanner): run scanner as an external process by default
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(ui): show albumArtist names
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(ui): dim out missing albums
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: flaky test
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(server): scrobble buffer mapping. fix #3583
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: more participations renaming
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: listenbrainz scrobbling
Signed-off-by: Deluan <deluan@navidrome.org>
* feat: send release_group_mbid to listenbrainz
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(subsonic): implement OpenSubsonic explicitStatus field (#3597)
* feat: implement OpenSubsonic explicitStatus field
* fix(subsonic): fix failing snapshot tests
* refactor: create helper for setting explicitStatus
* fix: store smaller values for explicit-status on database
* test: ToAlbum explicitStatus
* refactor: rename explicitStatus helper function
---------
Co-authored-by: Deluan Quintão <deluan@navidrome.org>
* fix: handle album and track tags in the DB based on the mappings.yaml file
Signed-off-by: Deluan <deluan@navidrome.org>
* save similar artists as JSONB
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: getAlbumList byGenre
Signed-off-by: Deluan <deluan@navidrome.org>
* detect changes in PID configuration
Signed-off-by: Deluan <deluan@navidrome.org>
* set default album PID to legacy_pid
Signed-off-by: Deluan <deluan@navidrome.org>
* fix tests
Signed-off-by: Deluan <deluan@navidrome.org>
* fix SIGSEGV
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: don't lose album stars/ratings when migrating
Signed-off-by: Deluan <deluan@navidrome.org>
* store full PID conf in properties
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: keep album annotations when changing PID.Album config
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: reassign album annotations
Signed-off-by: Deluan <deluan@navidrome.org>
* feat: use (display) albumArtist and add links to each artist
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: not showing albums by albumartist
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: error msgs
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: hide PID from Native API
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: album cover art resolution
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: trim participant names
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: reduce watcher log spam
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: panic when initializing the watcher
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: various artists
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: don't store empty lyrics in the DB
Signed-off-by: Deluan <deluan@navidrome.org>
* remove unused methods
Signed-off-by: Deluan <deluan@navidrome.org>
* drop full_text indexes, as they are not being used by SQLite
Signed-off-by: Deluan <deluan@navidrome.org>
* keep album created_at when upgrading
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(ui): null pointer
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: album artwork cache
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: don't expose missing files in Subsonic API
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: searchable interface
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: filter out missing items from subsonic search
* fix: filter out missing items from playlists
* fix: filter out missing items from shares
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(ui): add filter by artist role
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(subsonic): only return albumartists in getIndexes and getArtists endpoints
Signed-off-by: Deluan <deluan@navidrome.org>
* sort roles alphabetically
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: artist playcounts
Signed-off-by: Deluan <deluan@navidrome.org>
* change default Album PID conf
Signed-off-by: Deluan <deluan@navidrome.org>
* fix albumartist link when it does not match any albumartists values
Signed-off-by: Deluan <deluan@navidrome.org>
* fix `Ignoring filter not whitelisted` (role) message
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: trim any names/titles being imported
Signed-off-by: Deluan <deluan@navidrome.org>
* remove unused genre code
Signed-off-by: Deluan <deluan@navidrome.org>
* serialize calls to Last.fm's getArtist
Signed-off-by: Deluan <deluan@navidrome.org>
xxx
Signed-off-by: Deluan <deluan@navidrome.org>
* add counters to genres
Signed-off-by: Deluan <deluan@navidrome.org>
* nit: fix migration `notice` message
Signed-off-by: Deluan <deluan@navidrome.org>
* optimize similar artists query
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: last.fm.getInfo when mbid does not exist
Signed-off-by: Deluan <deluan@navidrome.org>
* ui only show missing items for admins
Signed-off-by: Deluan <deluan@navidrome.org>
* don't allow interaction with missing items
Signed-off-by: Deluan <deluan@navidrome.org>
* Add Missing Files view (WIP)
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: merged tag_counts into tag table
Signed-off-by: Deluan <deluan@navidrome.org>
* add option to completely disable automatic scanner
Signed-off-by: Deluan <deluan@navidrome.org>
* add delete missing files functionality
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: playlists not showing for regular users
Signed-off-by: Deluan <deluan@navidrome.org>
* reduce updateLastAccess frequency to once every minute
Signed-off-by: Deluan <deluan@navidrome.org>
* reduce update player frequency to once every minute
Signed-off-by: Deluan <deluan@navidrome.org>
* add timeout when updating player
Signed-off-by: Deluan <deluan@navidrome.org>
* remove dead code
Signed-off-by: Deluan <deluan@navidrome.org>
* fix duplicated roles in stats
Signed-off-by: Deluan <deluan@navidrome.org>
* add `; ` to artist splitters
Signed-off-by: Deluan <deluan@navidrome.org>
* fix stats query
Signed-off-by: Deluan <deluan@navidrome.org>
* more logs
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: support legacy clients (DSub) by removing OpenSubsonic extra fields - WIP
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: support legacy clients (DSub) by removing OpenSubsonic extra fields - WIP
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: support legacy clients (DSub) by removing OpenSubsonic extra fields - WIP
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: support legacy clients (DSub) by removing OpenSubsonic extra fields - WIP
Signed-off-by: Deluan <deluan@navidrome.org>
* add record label filter
Signed-off-by: Deluan <deluan@navidrome.org>
* add release type filter
Signed-off-by: Deluan <deluan@navidrome.org>
* fix purgeUnused tags
Signed-off-by: Deluan <deluan@navidrome.org>
* add grouping filter to albums
Signed-off-by: Deluan <deluan@navidrome.org>
* allow any album tags to be used in as filters in the API
Signed-off-by: Deluan <deluan@navidrome.org>
* remove empty tags from album info
Signed-off-by: Deluan <deluan@navidrome.org>
* comments in the migration
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: Cannot read properties of undefined
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: listenbrainz scrobbling (#3640)
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: remove duplicated tag values
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: don't ignore the taglib folder!
Signed-off-by: Deluan <deluan@navidrome.org>
* feat: show track subtitle tag
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: show artists stats based on selected role
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: inspect
Signed-off-by: Deluan <deluan@navidrome.org>
* add media type to album info/filters
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: change format of subtitle in the UI
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: subtitle in Subsonic API and search
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: subtitle in UI's player
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: split strings should be case-insensitive
Signed-off-by: Deluan <deluan@navidrome.org>
* disable ScanSchedule
Signed-off-by: Deluan <deluan@navidrome.org>
* increase default sessiontimeout
Signed-off-by: Deluan <deluan@navidrome.org>
* add sqlite command line tool to docker image
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: resources override
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: album PID conf
Signed-off-by: Deluan <deluan@navidrome.org>
* change migration to mark current artists as albumArtists
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(ui): Allow filtering on multiple genres (#3679)
* feat(ui): Allow filtering on multiple genres
Signed-off-by: Henrik Nordvik <henrikno@gmail.com>
Signed-off-by: Deluan <deluan@navidrome.org>
* add multi-genre filter in Album list
Signed-off-by: Deluan <deluan@navidrome.org>
---------
Signed-off-by: Henrik Nordvik <henrikno@gmail.com>
Signed-off-by: Deluan <deluan@navidrome.org>
Co-authored-by: Henrik Nordvik <henrikno@gmail.com>
* add more multi-valued tag filters to Album and Song views
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(ui): unselect missing files after removing
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(ui): song filter
Signed-off-by: Deluan <deluan@navidrome.org>
* fix sharing tracks. fix #3687
Signed-off-by: Deluan <deluan@navidrome.org>
* use rowids when using search for sync (ex: Symfonium)
Signed-off-by: Deluan <deluan@navidrome.org>
* fix "Report Real Paths" option for subsonic clients
Signed-off-by: Deluan <deluan@navidrome.org>
* fix "Report Real Paths" option for subsonic clients for search
Signed-off-by: Deluan <deluan@navidrome.org>
* add libraryPath to Native API /songs endpoint
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(subsonic): add album version
Signed-off-by: Deluan <deluan@navidrome.org>
* made all tags lowercase as they are case-insensitive anyways.
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(ui): Show full paths, extended properties for album/song (#3691)
* feat(ui): Show full paths, extended properties for album/song
- uses library path + os separator + path
- show participants (album/song) and tags (song)
- make album/participant clickable in show info
* add source to path
* fix pathSeparator in UI
Signed-off-by: Deluan <deluan@navidrome.org>
* fix local artist artwork (#3695)
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: parse vorbis performers
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: clean function into smaller functions
Signed-off-by: Deluan <deluan@navidrome.org>
* fix translations for en and pt
Signed-off-by: Deluan <deluan@navidrome.org>
* add trace log to show annotations reassignment
Signed-off-by: Deluan <deluan@navidrome.org>
* add trace log to show annotations reassignment
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: allow performers without instrument/subrole
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: metadata clean function again
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: optimize split function
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: split function is now a method of TagConf
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: humanize Artist total size
Signed-off-by: Deluan <deluan@navidrome.org>
* add album version to album details
Signed-off-by: Deluan <deluan@navidrome.org>
* don't display album-level tags in SongInfo
Signed-off-by: Deluan <deluan@navidrome.org>
* fix genre clicking in Album Page
Signed-off-by: Deluan <deluan@navidrome.org>
* don't use mbids in Last.fm api calls.
From https://discord.com/channels/671335427726114836/704303730660737113/1337574018143879248:
With MBID:
```
GET https://ws.audioscrobbler.com/2.0/?api_key=XXXX&artist=Van+Morrison&format=json&lang=en&mbid=a41ac10f-0a56-4672-9161-b83f9b223559&method=artist.getInfo
{
artist: {
name: "Bee Gees",
mbid: "bf0f7e29-dfe1-416c-b5c6-f9ebc19ea810",
url: "https://www.last.fm/music/Bee+Gees",
}
```
Without MBID:
```
GET https://ws.audioscrobbler.com/2.0/?api_key=XXXX&artist=Van+Morrison&format=json&lang=en&method=artist.getInfo
{
artist: {
name: "Van Morrison",
mbid: "a41ac10f-0a56-4672-9161-b83f9b223559",
url: "https://www.last.fm/music/Van+Morrison",
}
```
Signed-off-by: Deluan <deluan@navidrome.org>
* better logging for when the artist folder is not found
Signed-off-by: Deluan <deluan@navidrome.org>
* fix various issues with artist image resolution
Signed-off-by: Deluan <deluan@navidrome.org>
* hide "Additional Tags" header if there are none.
Signed-off-by: Deluan <deluan@navidrome.org>
* simplify tag rendering
Signed-off-by: Deluan <deluan@navidrome.org>
* enhance logging for artist folder detection
Signed-off-by: Deluan <deluan@navidrome.org>
* make folderID consistent for relative and absolute folderPaths
Signed-off-by: Deluan <deluan@navidrome.org>
* handle more folder paths scenarios
Signed-off-by: Deluan <deluan@navidrome.org>
* filter out other roles when SubsonicArtistParticipations = true
Signed-off-by: Deluan <deluan@navidrome.org>
* fix "Cannot read properties of undefined"
Signed-off-by: Deluan <deluan@navidrome.org>
* fix lyrics and comments being truncated (#3701)
* fix lyrics and comments being truncated
* specifically test for lyrics and comment length
* reorder assertions
Signed-off-by: Deluan <deluan@navidrome.org>
---------
Signed-off-by: Deluan <deluan@navidrome.org>
Co-authored-by: Deluan <deluan@navidrome.org>
* fix(server): Expose library_path for playlist (#3705)
Allows showing absolute path for UI, and makes "report real path" work for playlists (Subsonic)
* fix BFR on Windows (#3704)
* fix potential reflected cross-site scripting vulnerability
Signed-off-by: Deluan <deluan@navidrome.org>
* hack to make it work on Windows
* ignore windows executables
* try fixing the pipeline
Signed-off-by: Deluan <deluan@navidrome.org>
* allow MusicFolder in other drives
* move windows local drive logic to local storage implementation
---------
Signed-off-by: Deluan <deluan@navidrome.org>
* increase pagination sizes for missing files
Signed-off-by: Deluan <deluan@navidrome.org>
* reduce level of "already scanning" watcher log message
Signed-off-by: Deluan <deluan@navidrome.org>
* only count folders with audio files in it
See https://github.com/navidrome/navidrome/discussions/3676#discussioncomment-11990930
Signed-off-by: Deluan <deluan@navidrome.org>
* add album version and catalog number to search
Signed-off-by: Deluan <deluan@navidrome.org>
* add `organization` alias for `recordlabel`
Signed-off-by: Deluan <deluan@navidrome.org>
* remove mbid from Last.fm agent
Signed-off-by: Deluan <deluan@navidrome.org>
* feat: support inspect in ui (#3726)
* inspect in ui
* address round 1
* add catalogNum to AlbumInfo
Signed-off-by: Deluan <deluan@navidrome.org>
* remove dependency on metadata_old (deprecated) package
Signed-off-by: Deluan <deluan@navidrome.org>
* add `RawTags` to model
Signed-off-by: Deluan <deluan@navidrome.org>
* support parsing MBIDs for roles (from the https://github.com/kgarner7/picard-all-mbids plugin) (#3698)
* parse standard roles, vorbis/m4a work for now
* fix djmixer
* working roles, use DJ-mix
* add performers to file
* map mbids
* add a few more tests
* add test
Signed-off-by: Deluan <deluan@navidrome.org>
* try to simplify the performers logic
Signed-off-by: Deluan <deluan@navidrome.org>
* stylistic changes
---------
Signed-off-by: Deluan <deluan@navidrome.org>
Co-authored-by: Deluan <deluan@navidrome.org>
* remove param mutation
Signed-off-by: Deluan <deluan@navidrome.org>
* run automated SQLite optimizations
Signed-off-by: Deluan <deluan@navidrome.org>
* fix playlists import/export on Windows
* fix import playlists
* fix export playlists
* better handling of Windows volumes
Signed-off-by: Deluan <deluan@navidrome.org>
* handle more album ID reassignments
Signed-off-by: Deluan <deluan@navidrome.org>
* allow adding/overriding tags in the config file
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(ui): Fix playlist track id, handle missing tracks better (#3734)
- Use `mediaFileId` instead of `id` for playlist tracks
- Only fetch if the file is not missing
- If extractor fails to get the file, also error (rather than panic)
* optimize DB after each scan.
Signed-off-by: Deluan <deluan@navidrome.org>
* remove sortable from AlbumSongs columns
Signed-off-by: Deluan <deluan@navidrome.org>
* simplify query to get missing tracks
Signed-off-by: Deluan <deluan@navidrome.org>
* mark Scanner.Extractor as deprecated
Signed-off-by: Deluan <deluan@navidrome.org>
---------
Signed-off-by: Deluan <deluan@navidrome.org>
Signed-off-by: Henrik Nordvik <henrikno@gmail.com>
Co-authored-by: Caio Cotts <caio@cotts.com.br>
Co-authored-by: Henrik Nordvik <henrikno@gmail.com>
Co-authored-by: Kendall Garner <17521368+kgarner7@users.noreply.github.com>
2025-02-19 17:35:17 -08:00
|
|
|
return "/music", nil
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-26 19:34:12 -04:00
|
|
|
type mockMediaFileRepository struct {
|
|
|
|
|
mock.Mock
|
|
|
|
|
model.MediaFileRepository
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-25 18:06:10 -04:00
|
|
|
func (m *mockMediaFileRepository) GetAll(ctx context.Context, options ...model.QueryOptions) (model.MediaFiles, error) {
|
2023-03-26 19:34:12 -04:00
|
|
|
args := m.Called(options)
|
|
|
|
|
return args.Get(0).(model.MediaFiles), args.Error(1)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type mockPlaylistRepository struct {
|
|
|
|
|
mock.Mock
|
|
|
|
|
model.PlaylistRepository
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-25 18:06:10 -04:00
|
|
|
func (m *mockPlaylistRepository) GetWithTracks(_ context.Context, id string, refreshSmartPlaylists, includeMissing bool) (*model.Playlist, error) {
|
feat(bfr): Big Refactor: new scanner, lots of new fields and tags, improvements and DB schema changes (#2709)
* fix(server): more race conditions when updating artist/album from external sources
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(scanner): add .gitignore syntax to .ndignore. Resolves #1394
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(ui): null
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(scanner): pass configfile option to child process
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(scanner): resume interrupted fullScans
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(scanner): remove old scanner code
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(scanner): rename old metadata package
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(scanner): move old metadata package
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: tests
Signed-off-by: Deluan <deluan@navidrome.org>
* chore(deps): update Go to 1.23.4
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: logs
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(test):
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: log level
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: remove log message
Signed-off-by: Deluan <deluan@navidrome.org>
* feat: add config for scanner watcher
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: children playlists
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: replace `interface{}` with `any`
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: smart playlists with genres
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: allow any tags in smart playlists
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: artist names in playlists
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: smart playlist's sort by tags
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(subsonic): add moods to child
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(subsonic): add moods to AlbumID3
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor(subsonic): use generic JSONArray for OS arrays
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor(subsonic): use https in test
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(subsonic): add releaseTypes to AlbumID3
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(subsonic): add recordLabels to AlbumID3
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor(subsonic): rename JSONArray to Array
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(subsonic): add artists to AlbumID3
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(subsonic): add artists to Child
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(scanner): do not pre-populate smart playlists
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(subsonic): implement a simplified version of ArtistID3.
See https://github.com/opensubsonic/open-subsonic-api/discussions/120
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(subsonic): add artists to album child
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(subsonic): add contributors to mediafile Child
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(subsonic): add albumArtists to mediafile Child
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(subsonic): add displayArtist and displayAlbumArtist
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(subsonic): add displayComposer to Child
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(subsonic): add roles to ArtistID3
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(subsonic): use " • " separator for displayComposer
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor:
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(subsonic):
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(subsonic): respect `PreferSortTags` config option
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor(subsonic):
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: optimize purging non-unused tags
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: don't run 'refresh artist stats' concurrently with other transactions
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor:
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: log message
Signed-off-by: Deluan <deluan@navidrome.org>
* feat: add Scanner.ScanOnStartup config option, default true
Signed-off-by: Deluan <deluan@navidrome.org>
* feat: better json parsing error msg when importing NSPs
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: don't update album's imported_time when updating external_metadata
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: handle interrupted scans and full scans after migrations
Signed-off-by: Deluan <deluan@navidrome.org>
* feat: run `analyze` when migration requires a full rescan
Signed-off-by: Deluan <deluan@navidrome.org>
* feat: run `PRAGMA optimize` at the end of the scan
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: don't update artist's updated_at when updating external_metadata
Signed-off-by: Deluan <deluan@navidrome.org>
* feat: handle multiple artists and roles in smart playlists
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(ui): dim missing tracks
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: album missing logic
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: error encoding in gob
Signed-off-by: Deluan <deluan@navidrome.org>
* feat: separate warnings from errors
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: mark albums as missing if they were contained in a deleted folder
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: add participant names to media_file and album tables
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: use participations in criteria, instead of m2m relationship
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: rename participations to participants
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(subsonic): add moods to album child
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: albumartist role case
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(scanner): run scanner as an external process by default
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(ui): show albumArtist names
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(ui): dim out missing albums
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: flaky test
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(server): scrobble buffer mapping. fix #3583
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: more participations renaming
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: listenbrainz scrobbling
Signed-off-by: Deluan <deluan@navidrome.org>
* feat: send release_group_mbid to listenbrainz
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(subsonic): implement OpenSubsonic explicitStatus field (#3597)
* feat: implement OpenSubsonic explicitStatus field
* fix(subsonic): fix failing snapshot tests
* refactor: create helper for setting explicitStatus
* fix: store smaller values for explicit-status on database
* test: ToAlbum explicitStatus
* refactor: rename explicitStatus helper function
---------
Co-authored-by: Deluan Quintão <deluan@navidrome.org>
* fix: handle album and track tags in the DB based on the mappings.yaml file
Signed-off-by: Deluan <deluan@navidrome.org>
* save similar artists as JSONB
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: getAlbumList byGenre
Signed-off-by: Deluan <deluan@navidrome.org>
* detect changes in PID configuration
Signed-off-by: Deluan <deluan@navidrome.org>
* set default album PID to legacy_pid
Signed-off-by: Deluan <deluan@navidrome.org>
* fix tests
Signed-off-by: Deluan <deluan@navidrome.org>
* fix SIGSEGV
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: don't lose album stars/ratings when migrating
Signed-off-by: Deluan <deluan@navidrome.org>
* store full PID conf in properties
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: keep album annotations when changing PID.Album config
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: reassign album annotations
Signed-off-by: Deluan <deluan@navidrome.org>
* feat: use (display) albumArtist and add links to each artist
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: not showing albums by albumartist
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: error msgs
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: hide PID from Native API
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: album cover art resolution
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: trim participant names
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: reduce watcher log spam
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: panic when initializing the watcher
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: various artists
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: don't store empty lyrics in the DB
Signed-off-by: Deluan <deluan@navidrome.org>
* remove unused methods
Signed-off-by: Deluan <deluan@navidrome.org>
* drop full_text indexes, as they are not being used by SQLite
Signed-off-by: Deluan <deluan@navidrome.org>
* keep album created_at when upgrading
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(ui): null pointer
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: album artwork cache
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: don't expose missing files in Subsonic API
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: searchable interface
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: filter out missing items from subsonic search
* fix: filter out missing items from playlists
* fix: filter out missing items from shares
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(ui): add filter by artist role
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(subsonic): only return albumartists in getIndexes and getArtists endpoints
Signed-off-by: Deluan <deluan@navidrome.org>
* sort roles alphabetically
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: artist playcounts
Signed-off-by: Deluan <deluan@navidrome.org>
* change default Album PID conf
Signed-off-by: Deluan <deluan@navidrome.org>
* fix albumartist link when it does not match any albumartists values
Signed-off-by: Deluan <deluan@navidrome.org>
* fix `Ignoring filter not whitelisted` (role) message
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: trim any names/titles being imported
Signed-off-by: Deluan <deluan@navidrome.org>
* remove unused genre code
Signed-off-by: Deluan <deluan@navidrome.org>
* serialize calls to Last.fm's getArtist
Signed-off-by: Deluan <deluan@navidrome.org>
xxx
Signed-off-by: Deluan <deluan@navidrome.org>
* add counters to genres
Signed-off-by: Deluan <deluan@navidrome.org>
* nit: fix migration `notice` message
Signed-off-by: Deluan <deluan@navidrome.org>
* optimize similar artists query
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: last.fm.getInfo when mbid does not exist
Signed-off-by: Deluan <deluan@navidrome.org>
* ui only show missing items for admins
Signed-off-by: Deluan <deluan@navidrome.org>
* don't allow interaction with missing items
Signed-off-by: Deluan <deluan@navidrome.org>
* Add Missing Files view (WIP)
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: merged tag_counts into tag table
Signed-off-by: Deluan <deluan@navidrome.org>
* add option to completely disable automatic scanner
Signed-off-by: Deluan <deluan@navidrome.org>
* add delete missing files functionality
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: playlists not showing for regular users
Signed-off-by: Deluan <deluan@navidrome.org>
* reduce updateLastAccess frequency to once every minute
Signed-off-by: Deluan <deluan@navidrome.org>
* reduce update player frequency to once every minute
Signed-off-by: Deluan <deluan@navidrome.org>
* add timeout when updating player
Signed-off-by: Deluan <deluan@navidrome.org>
* remove dead code
Signed-off-by: Deluan <deluan@navidrome.org>
* fix duplicated roles in stats
Signed-off-by: Deluan <deluan@navidrome.org>
* add `; ` to artist splitters
Signed-off-by: Deluan <deluan@navidrome.org>
* fix stats query
Signed-off-by: Deluan <deluan@navidrome.org>
* more logs
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: support legacy clients (DSub) by removing OpenSubsonic extra fields - WIP
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: support legacy clients (DSub) by removing OpenSubsonic extra fields - WIP
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: support legacy clients (DSub) by removing OpenSubsonic extra fields - WIP
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: support legacy clients (DSub) by removing OpenSubsonic extra fields - WIP
Signed-off-by: Deluan <deluan@navidrome.org>
* add record label filter
Signed-off-by: Deluan <deluan@navidrome.org>
* add release type filter
Signed-off-by: Deluan <deluan@navidrome.org>
* fix purgeUnused tags
Signed-off-by: Deluan <deluan@navidrome.org>
* add grouping filter to albums
Signed-off-by: Deluan <deluan@navidrome.org>
* allow any album tags to be used in as filters in the API
Signed-off-by: Deluan <deluan@navidrome.org>
* remove empty tags from album info
Signed-off-by: Deluan <deluan@navidrome.org>
* comments in the migration
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: Cannot read properties of undefined
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: listenbrainz scrobbling (#3640)
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: remove duplicated tag values
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: don't ignore the taglib folder!
Signed-off-by: Deluan <deluan@navidrome.org>
* feat: show track subtitle tag
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: show artists stats based on selected role
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: inspect
Signed-off-by: Deluan <deluan@navidrome.org>
* add media type to album info/filters
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: change format of subtitle in the UI
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: subtitle in Subsonic API and search
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: subtitle in UI's player
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: split strings should be case-insensitive
Signed-off-by: Deluan <deluan@navidrome.org>
* disable ScanSchedule
Signed-off-by: Deluan <deluan@navidrome.org>
* increase default sessiontimeout
Signed-off-by: Deluan <deluan@navidrome.org>
* add sqlite command line tool to docker image
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: resources override
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: album PID conf
Signed-off-by: Deluan <deluan@navidrome.org>
* change migration to mark current artists as albumArtists
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(ui): Allow filtering on multiple genres (#3679)
* feat(ui): Allow filtering on multiple genres
Signed-off-by: Henrik Nordvik <henrikno@gmail.com>
Signed-off-by: Deluan <deluan@navidrome.org>
* add multi-genre filter in Album list
Signed-off-by: Deluan <deluan@navidrome.org>
---------
Signed-off-by: Henrik Nordvik <henrikno@gmail.com>
Signed-off-by: Deluan <deluan@navidrome.org>
Co-authored-by: Henrik Nordvik <henrikno@gmail.com>
* add more multi-valued tag filters to Album and Song views
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(ui): unselect missing files after removing
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(ui): song filter
Signed-off-by: Deluan <deluan@navidrome.org>
* fix sharing tracks. fix #3687
Signed-off-by: Deluan <deluan@navidrome.org>
* use rowids when using search for sync (ex: Symfonium)
Signed-off-by: Deluan <deluan@navidrome.org>
* fix "Report Real Paths" option for subsonic clients
Signed-off-by: Deluan <deluan@navidrome.org>
* fix "Report Real Paths" option for subsonic clients for search
Signed-off-by: Deluan <deluan@navidrome.org>
* add libraryPath to Native API /songs endpoint
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(subsonic): add album version
Signed-off-by: Deluan <deluan@navidrome.org>
* made all tags lowercase as they are case-insensitive anyways.
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(ui): Show full paths, extended properties for album/song (#3691)
* feat(ui): Show full paths, extended properties for album/song
- uses library path + os separator + path
- show participants (album/song) and tags (song)
- make album/participant clickable in show info
* add source to path
* fix pathSeparator in UI
Signed-off-by: Deluan <deluan@navidrome.org>
* fix local artist artwork (#3695)
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: parse vorbis performers
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: clean function into smaller functions
Signed-off-by: Deluan <deluan@navidrome.org>
* fix translations for en and pt
Signed-off-by: Deluan <deluan@navidrome.org>
* add trace log to show annotations reassignment
Signed-off-by: Deluan <deluan@navidrome.org>
* add trace log to show annotations reassignment
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: allow performers without instrument/subrole
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: metadata clean function again
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: optimize split function
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: split function is now a method of TagConf
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: humanize Artist total size
Signed-off-by: Deluan <deluan@navidrome.org>
* add album version to album details
Signed-off-by: Deluan <deluan@navidrome.org>
* don't display album-level tags in SongInfo
Signed-off-by: Deluan <deluan@navidrome.org>
* fix genre clicking in Album Page
Signed-off-by: Deluan <deluan@navidrome.org>
* don't use mbids in Last.fm api calls.
From https://discord.com/channels/671335427726114836/704303730660737113/1337574018143879248:
With MBID:
```
GET https://ws.audioscrobbler.com/2.0/?api_key=XXXX&artist=Van+Morrison&format=json&lang=en&mbid=a41ac10f-0a56-4672-9161-b83f9b223559&method=artist.getInfo
{
artist: {
name: "Bee Gees",
mbid: "bf0f7e29-dfe1-416c-b5c6-f9ebc19ea810",
url: "https://www.last.fm/music/Bee+Gees",
}
```
Without MBID:
```
GET https://ws.audioscrobbler.com/2.0/?api_key=XXXX&artist=Van+Morrison&format=json&lang=en&method=artist.getInfo
{
artist: {
name: "Van Morrison",
mbid: "a41ac10f-0a56-4672-9161-b83f9b223559",
url: "https://www.last.fm/music/Van+Morrison",
}
```
Signed-off-by: Deluan <deluan@navidrome.org>
* better logging for when the artist folder is not found
Signed-off-by: Deluan <deluan@navidrome.org>
* fix various issues with artist image resolution
Signed-off-by: Deluan <deluan@navidrome.org>
* hide "Additional Tags" header if there are none.
Signed-off-by: Deluan <deluan@navidrome.org>
* simplify tag rendering
Signed-off-by: Deluan <deluan@navidrome.org>
* enhance logging for artist folder detection
Signed-off-by: Deluan <deluan@navidrome.org>
* make folderID consistent for relative and absolute folderPaths
Signed-off-by: Deluan <deluan@navidrome.org>
* handle more folder paths scenarios
Signed-off-by: Deluan <deluan@navidrome.org>
* filter out other roles when SubsonicArtistParticipations = true
Signed-off-by: Deluan <deluan@navidrome.org>
* fix "Cannot read properties of undefined"
Signed-off-by: Deluan <deluan@navidrome.org>
* fix lyrics and comments being truncated (#3701)
* fix lyrics and comments being truncated
* specifically test for lyrics and comment length
* reorder assertions
Signed-off-by: Deluan <deluan@navidrome.org>
---------
Signed-off-by: Deluan <deluan@navidrome.org>
Co-authored-by: Deluan <deluan@navidrome.org>
* fix(server): Expose library_path for playlist (#3705)
Allows showing absolute path for UI, and makes "report real path" work for playlists (Subsonic)
* fix BFR on Windows (#3704)
* fix potential reflected cross-site scripting vulnerability
Signed-off-by: Deluan <deluan@navidrome.org>
* hack to make it work on Windows
* ignore windows executables
* try fixing the pipeline
Signed-off-by: Deluan <deluan@navidrome.org>
* allow MusicFolder in other drives
* move windows local drive logic to local storage implementation
---------
Signed-off-by: Deluan <deluan@navidrome.org>
* increase pagination sizes for missing files
Signed-off-by: Deluan <deluan@navidrome.org>
* reduce level of "already scanning" watcher log message
Signed-off-by: Deluan <deluan@navidrome.org>
* only count folders with audio files in it
See https://github.com/navidrome/navidrome/discussions/3676#discussioncomment-11990930
Signed-off-by: Deluan <deluan@navidrome.org>
* add album version and catalog number to search
Signed-off-by: Deluan <deluan@navidrome.org>
* add `organization` alias for `recordlabel`
Signed-off-by: Deluan <deluan@navidrome.org>
* remove mbid from Last.fm agent
Signed-off-by: Deluan <deluan@navidrome.org>
* feat: support inspect in ui (#3726)
* inspect in ui
* address round 1
* add catalogNum to AlbumInfo
Signed-off-by: Deluan <deluan@navidrome.org>
* remove dependency on metadata_old (deprecated) package
Signed-off-by: Deluan <deluan@navidrome.org>
* add `RawTags` to model
Signed-off-by: Deluan <deluan@navidrome.org>
* support parsing MBIDs for roles (from the https://github.com/kgarner7/picard-all-mbids plugin) (#3698)
* parse standard roles, vorbis/m4a work for now
* fix djmixer
* working roles, use DJ-mix
* add performers to file
* map mbids
* add a few more tests
* add test
Signed-off-by: Deluan <deluan@navidrome.org>
* try to simplify the performers logic
Signed-off-by: Deluan <deluan@navidrome.org>
* stylistic changes
---------
Signed-off-by: Deluan <deluan@navidrome.org>
Co-authored-by: Deluan <deluan@navidrome.org>
* remove param mutation
Signed-off-by: Deluan <deluan@navidrome.org>
* run automated SQLite optimizations
Signed-off-by: Deluan <deluan@navidrome.org>
* fix playlists import/export on Windows
* fix import playlists
* fix export playlists
* better handling of Windows volumes
Signed-off-by: Deluan <deluan@navidrome.org>
* handle more album ID reassignments
Signed-off-by: Deluan <deluan@navidrome.org>
* allow adding/overriding tags in the config file
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(ui): Fix playlist track id, handle missing tracks better (#3734)
- Use `mediaFileId` instead of `id` for playlist tracks
- Only fetch if the file is not missing
- If extractor fails to get the file, also error (rather than panic)
* optimize DB after each scan.
Signed-off-by: Deluan <deluan@navidrome.org>
* remove sortable from AlbumSongs columns
Signed-off-by: Deluan <deluan@navidrome.org>
* simplify query to get missing tracks
Signed-off-by: Deluan <deluan@navidrome.org>
* mark Scanner.Extractor as deprecated
Signed-off-by: Deluan <deluan@navidrome.org>
---------
Signed-off-by: Deluan <deluan@navidrome.org>
Signed-off-by: Henrik Nordvik <henrikno@gmail.com>
Co-authored-by: Caio Cotts <caio@cotts.com.br>
Co-authored-by: Henrik Nordvik <henrikno@gmail.com>
Co-authored-by: Kendall Garner <17521368+kgarner7@users.noreply.github.com>
2025-02-19 17:35:17 -08:00
|
|
|
args := m.Called(id, refreshSmartPlaylists, includeMissing)
|
2023-03-26 19:34:12 -04:00
|
|
|
return args.Get(0).(*model.Playlist), args.Error(1)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type mockMediaStreamer struct {
|
|
|
|
|
mock.Mock
|
2026-03-09 22:22:58 -04:00
|
|
|
stream.MediaStreamer
|
2023-03-26 19:34:12 -04:00
|
|
|
}
|
|
|
|
|
|
2026-03-09 22:22:58 -04:00
|
|
|
func (m *mockMediaStreamer) NewStream(ctx context.Context, mf *model.MediaFile, req stream.Request) (*stream.Stream, error) {
|
feat(subsonic): implement OpenSubsonic Transcoding extension (#4990)
* feat(subsonic): implement transcode decision logic and codec handling for media files
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(subsonic): update codec limitation structure and decision logic for improved clarity
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(transcoding): update bitrate handling to use kilobits per second (kbps) across transcode decision logic
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor(transcoding): simplify container alias handling in matchesContainer function
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(transcoding): enforce POST method for GetTranscodeDecision and handle non-POST requests
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(transcoding): add enums for protocol, comparison operators, limitations, and codec profiles in transcode decision logic
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor(transcoding): streamline limitation checks and applyLimitation logic for improved readability and maintainability
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor(transcoding): replace strings.EqualFold with direct comparison for protocol and limitation checks
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor(transcoding): rename token methods to CreateTranscodeParams and ParseTranscodeParams for clarity
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor(transcoding): enhance logging for transcode decision process and client info conversion
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor(transcoding): rename TranscodeDecision to Decider and update related methods for clarity
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor(transcoding): enhance transcoding config lookup logic for audio codecs
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor(transcoding): enhance transcoding options with sample rate support and improve command handling
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor(transcoding): add bit depth support for audio transcoding and enhance related logic
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor(transcoding): enhance AAC command handling and support for audio channels in streaming
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor(transcoding): streamline transcoding logic by consolidating stream parameter handling and enhancing alias mapping
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor(transcoding): update default command handling and add codec support for transcoding
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: implement noopDecider for transcoding decision handling in tests
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: address review findings for OpenSubsonic transcoding PR
Fix multiple issues identified during code review of the transcoding
extension: add missing return after error in shared stream handler
preventing nil pointer panic, replace dead r.Body nil check with
MaxBytesReader size limit, distinguish not-found from other DB errors,
fix bpsToKbps integer truncation with rounding, add "pcm" to
isLosslessFormat for consistency with model.IsLossless(), add
sampleRate/bitDepth/channels to streaming log, fix outdated test
comment, and add tests for conversion functions and GetTranscodeStream
parameter passing.
* feat(transcoding): add sourceUpdatedAt to decision and validate transcode parameters
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: small issues
Updated mock AAC transcoding command to use the new default (ipod with
fragmented MP4) matching the migration, ensuring tests exercise the same
buildDynamicArgs code path as production. Improved archiver test mock to
match on the whole StreamRequest struct instead of decomposing fields,
making it resilient to future field additions. Added named constants for
JWT claim keys in the transcode token and wrapped ParseTranscodeParams
errors with ErrTokenInvalid for consistency. Documented the IsLossless
BitDepth fallback heuristic as temporary until Codec column is populated.
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(transcoding): adapt transcode claims to struct-based auth.Claims
Updated transcode token handling to use the struct-based auth.Claims
introduced on master, replacing the previous map[string]any approach.
Extended auth.Claims with transcoding-specific fields (MediaID, DirectPlay,
UpdatedAt, Channels, SampleRate, BitDepth) and added float64 fallback in
ClaimsFromToken for numeric claims that lose their Go type during JWT
string serialization. Also added the missing lyrics parameter to all
subsonic.New() calls in test files.
* feat(model): add ProbeData field and UpdateProbeData repository method
Add probe_data TEXT column to media_file for caching ffprobe results.
Add UpdateProbeData to MediaFileRepository interface and implementations.
Use hash:"ignore" tag so probe data doesn't affect MediaFile fingerprints.
* feat(ffmpeg): add ProbeAudioStream for authoritative audio metadata
Add ProbeAudioStream to FFmpeg interface, using ffprobe to extract
codec, profile, bitrate, sample rate, bit depth, and channels.
Parse bits_per_raw_sample as fallback for FLAC/ALAC bit depth.
Normalize "unknown" profile to empty string.
All parseProbeOutput tests use real ffprobe JSON from actual files.
* feat(transcoding): integrate ffprobe into transcode decisions
Add ensureProbed to probe media files on first transcode decision,
caching results in probe_data. Build SourceStream from probe data
with fallback to tag-based metadata.
Refactor decision logic to pass StreamDetails instead of MediaFile,
enabling codec profile limitations (e.g., audioProfile) to use
probe data. Add normalizeProbeCodec to map ffprobe codec names
(dsd_lsbf_planar, pcm_s16le) to internal names (dsd, pcm).
NewDecider now accepts ffmpeg.FFmpeg; wire_gen.go regenerated.
* feat(transcoding): add DevEnableMediaFileProbe config flag
Add DevEnableMediaFileProbe (default true) to allow disabling ffprobe-
based media file probing as a safety fallback. When disabled, the
decider uses tag-based metadata from the scanner instead.
* test(transcode): add ensureProbed unit tests
Test probing when ProbeData is empty, skipping when already set,
error propagation from ffprobe, and DevEnableMediaFileProbe flag.
* refactor(ffmpeg): use command constant and select_streams for ProbeAudioStream
Move ffprobe arguments to a probeAudioStreamCmd constant, following the
same pattern as extractImageCmd and probeCmd. Add -select_streams a:0 to
only probe the first audio stream, avoiding unnecessary parsing of video
and artwork streams. Derive the ffprobe binary path safely using
filepath.Dir/Base instead of replacing within the full path string.
* refactor(transcode): decouple transcode token claims from auth.Claims
Remove six transcode-specific fields (MediaID, DirectPlay, UpdatedAt,
Channels, SampleRate, BitDepth) from auth.Claims, which is shared with
session and share tokens. Transcode tokens are signed parameter-passing
tokens, not authentication tokens, so coupling them to auth created
misleading dependencies.
The transcode package now owns its own JWT claim serialization via
Decision.toClaimsMap() and paramsFromToken(), using generic
auth.EncodeToken/DecodeAndVerifyToken wrappers that keep TokenAuth
encapsulated. Wire format (JWT claim keys) is unchanged, so in-flight
tokens remain compatible.
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor(transcode): simplify code after review
Extract getIntClaim helper to eliminate repeated int/int64/float64 JWT
claim extraction pattern in paramsFromToken and ClaimsFromToken. Rewrite
checkIntLimitation as a one-liner delegating to applyIntLimitation.
Return probe result from ensureProbed to avoid redundant JSON round-trip.
Extract toResponseStreamDetails helper and mediaTypeSong constant in
the API layer, and use transcode.ProtocolHTTP constant instead of
hardcoded string.
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(ffmpeg): enhance bit_rate parsing logic for audio streams
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(transcode): improve code review findings across transcode implementation
- Fix parseProbeData to return nil on JSON unmarshal failure instead of
a zero-valued struct, preventing silent degradation of source stream details
- Use probe-resolved codec for lossless detection in buildSourceStream
instead of the potentially stale scanner data
- Remove MediaFile.IsLossless() (dead code) and consolidate lossless
detection in isLosslessFormat(), using codec name only — bit depth is
not reliable since lossy codecs like ADPCM report non-zero values
- Add "wavpack" to lossless codec list (ffprobe codec_name for WavPack)
- Guard bpsToKbps against negative input values
- Fix misleading comment in buildTemplateArgs about conditional injection
- Avoid leaking internal error details in Subsonic API responses
- Add missing test for ErrNotFound branch in GetTranscodeDecision
- Add TODO for hardcoded protocol in toResponseStreamDetails
* refactor(transcode): streamline transcoding command lookup and format resolution
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(transcode): implement server-side transcoding override for player formats
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(transcode): honor bit depth and channel constraints in transcoding selection
selectTranscodingOptions only checked sample rate when deciding whether
same-format transcoding was needed, ignoring requested bit depth and
channel reductions. This caused the streamer to return raw audio when
the transcode decision requested downmix or bit-depth conversion.
* refactor(transcode): unify streaming decision engine via MakeDecision
Move transcoding decision-making out of mediaStreamer and into the
subsonic Stream/Download handlers, using transcode.Decider.MakeDecision
as the single decision engine. This eliminates selectTranscodingOptions
and the mismatch between decision and streaming code paths (decision
used LookupTranscodeCommand with built-in fallbacks, while streaming
used FindByFormat which only checked the DB).
- Add DecisionOptions with SkipProbe to MakeDecision so the legacy
streaming path never calls ffprobe
- Add buildLegacyClientInfo to translate legacy stream params (format,
maxBitRate, DefaultDownsamplingFormat) into a synthetic ClientInfo
- Add resolveStreamRequest on the subsonic Router to resolve legacy
params into a fully specified StreamRequest via MakeDecision
- Simplify DoStream to a dumb executor that receives pre-resolved params
- Remove selectTranscodingOptions entirely
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor(transcode): move MediaStreamer into core/transcode and unify StreamRequest
Moved MediaStreamer, Stream, TranscodingCache and related types from
core/media_streamer.go into core/transcode/, eliminating the duplicate
StreamRequest type. The transcode.StreamRequest now carries all fields
(ID, Format, BitRate, SampleRate, BitDepth, Channels, Offset) and
ResolveStream returns a fully-populated value, removing manual field
copying at every call site. Also moved buildLegacyClientInfo into the
transcode package alongside ResolveStream, and unexported
ParseTranscodeParams since it was only used internally by
ValidateTranscodeParams.
* refactor(transcode): rename Decider methods and unexport Params type
Rename ResolveStream → ResolveRequest and ValidateTranscodeParams →
ResolveRequestFromToken for clarity and consistency. The new
ResolveRequestFromToken returns a StreamRequest directly (instead of
the intermediate Params type), eliminating manual Params→StreamRequest
conversion in callers. Unexport Params to params since it is now only
used internally for JWT token parsing.
* test(transcode): remove redundant tests and use constants
Remove tests that duplicate coverage from integration-level tests
(toClaimsMap, paramsFromToken round-trips, applyServerOverride direct
call, duplicate 410 handler test). Replace raw "http" strings with
ProtocolHTTP constant. Consolidate lossy -sample_fmt tests into
DescribeTable.
* refactor(transcode): split oversized files into focused modules
Split transcode.go and transcode_test.go into focused files by concern:
- decider.go: decision engine (MakeDecision, direct play/transcode evaluation, probe)
- token.go: JWT token encode/decode (params, toClaimsMap, paramsFromToken, CreateTranscodeParams, ResolveRequestFromToken)
- legacy_client.go: legacy Subsonic bridge (buildLegacyClientInfo, ResolveRequest)
- codec_test.go: isLosslessFormat and normalizeProbeCodec tests
- token_test.go: token round-trip and ResolveRequestFromToken tests
Moved the Decider interface from types.go to decider.go to keep it near
its implementation, and cleaned up types.go to contain only pure type
definitions and constants. No public API changes.
* refactor(transcode): reorder parameters in applyServerOverride function
Signed-off-by: Deluan <deluan@navidrome.org>
* test(e2e): add NewTestStream function and implement spyStreamer for testing
Signed-off-by: Deluan <deluan@navidrome.org>
---------
Signed-off-by: Deluan <deluan@navidrome.org>
2026-03-08 23:57:49 -04:00
|
|
|
args := m.Called(ctx, mf, req)
|
2023-03-26 19:34:12 -04:00
|
|
|
if args.Error(1) != nil {
|
|
|
|
|
return nil, args.Error(1)
|
|
|
|
|
}
|
2026-03-09 22:22:58 -04:00
|
|
|
return &stream.Stream{ReadCloser: args.Get(0).(io.ReadCloser)}, nil
|
2023-03-26 19:34:12 -04:00
|
|
|
}
|
|
|
|
|
|
fix(share): reuse cached transcodes for share streams and zip downloads (#6262)
* fix(share): reuse cached transcodes when streaming from share links
Public share streams built the stream request with only the share's format
and bit rate, leaving sample rate, bit depth and channels at zero. Regular
playback resolves those through the transcode decider (e.g. 48000 Hz for
Opus), and they are part of the transcoding cache key, so a track already
transcoded during normal playback was transcoded again into a separate,
identical cache entry when played through a share link.
The public router now resolves share stream requests with the same
TranscodeDecider.ResolveRequest used by the Subsonic stream endpoint, so
both paths produce the same request and share cache entries.
Fixes #6261
* fix(archiver): reuse cached transcodes when zipping downloads
Zip downloads (album, artist, playlist and share) built the stream request
with only the format and bit rate, leaving sample rate, bit depth and
channels at zero. Those are part of the transcoding cache key, so a track
already transcoded for playback was transcoded again into a separate cache
entry when downloaded in a zip, and vice versa.
The archiver now resolves each request with TranscodeDecider.ResolveRequest,
the same as single-song downloads and streams. This also applies the
decider's defaults, so a zip requested without a bit rate uses the target
format's default bit rate instead of leaving it to ffmpeg.
* fix(archiver): name zip entries after the resolved transcoding format
The transcode decider can pick a different format than the one requested
(for example a player's forced transcoding, or a fallback to the default
downsampling format when the requested one can't be produced). Zip entry
names and the playlist M3U were still built from the requested format, so
an entry could end in .mp3 or .flac while holding Opus data.
Each track's request is now resolved before its entry name is built, and
the name uses the resolved format.
2026-10-03 08:58:54 -07:00
|
|
|
// fakeDecider echoes the legacy format/bitrate unless a resolved request is set.
|
|
|
|
|
type fakeDecider struct {
|
|
|
|
|
stream.TranscodeDecider
|
|
|
|
|
resolved *stream.Request
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (f *fakeDecider) ResolveRequest(_ context.Context, _ *model.MediaFile, format string, bitRate int, offset int) stream.Request {
|
|
|
|
|
if f.resolved != nil {
|
|
|
|
|
return *f.resolved
|
|
|
|
|
}
|
|
|
|
|
return stream.Request{Format: format, BitRate: bitRate, Offset: offset}
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-26 19:34:12 -04:00
|
|
|
type mockShare struct {
|
|
|
|
|
mock.Mock
|
|
|
|
|
core.Share
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (m *mockShare) Load(ctx context.Context, id string) (*model.Share, error) {
|
|
|
|
|
args := m.Called(ctx, id)
|
|
|
|
|
return args.Get(0).(*model.Share), args.Error(1)
|
|
|
|
|
}
|