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>
This commit is contained in:
Deluan Quintão
2025-02-19 17:35:17 -08:00
committed by GitHub
parent 46a963a02a
commit c795bcfcf7
329 changed files with 16586 additions and 5852 deletions
@@ -10,16 +10,7 @@
"id": "1",
"isDir": false,
"title": "title",
"isVideo": false,
"bpm": 0,
"comment": "",
"sortName": "",
"mediaType": "",
"musicBrainzId": "",
"genres": [],
"replayGain": {},
"channelCount": 0,
"samplingRate": 0
"isVideo": false
}
]
}
@@ -1,7 +1,5 @@
<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.8.0" type="navidrome" serverVersion="v0.0.0" openSubsonic="true">
<albumList>
<album id="1" isDir="false" title="title" isVideo="false" bpm="0" comment="" sortName="" mediaType="" musicBrainzId="" channelCount="0" samplingRate="0">
<replayGain></replayGain>
</album>
<album id="1" isDir="false" title="title" isVideo="false"></album>
</albumList>
</subsonic-response>
@@ -9,7 +9,7 @@
"name": "album",
"artist": "artist",
"genre": "rock",
"userRating": 0,
"userRating": 4,
"genres": [
{
"name": "rock"
@@ -45,6 +45,35 @@
"month": 5,
"day": 10
},
"releaseTypes": [
"album",
"live"
],
"recordLabels": [
{
"name": "label1"
},
{
"name": "label2"
}
],
"moods": [
"happy",
"sad"
],
"artists": [
{
"id": "1",
"name": "artist1"
},
{
"id": "2",
"name": "artist2"
}
],
"displayArtist": "artist1 \u0026 artist2",
"explicitStatus": "clean",
"version": "Deluxe Edition",
"song": [
{
"id": "1",
@@ -86,8 +115,54 @@
"baseGain": 5,
"fallbackGain": 6
},
"channelCount": 0,
"samplingRate": 0
"channelCount": 2,
"samplingRate": 44100,
"bitDepth": 16,
"moods": [
"happy",
"sad"
],
"artists": [
{
"id": "1",
"name": "artist1"
},
{
"id": "2",
"name": "artist2"
}
],
"displayArtist": "artist1 \u0026 artist2",
"albumArtists": [
{
"id": "1",
"name": "album artist1"
},
{
"id": "2",
"name": "album artist2"
}
],
"displayAlbumArtist": "album artist1 \u0026 album artist2",
"contributors": [
{
"role": "role1",
"artist": {
"id": "1",
"name": "artist1"
}
},
{
"role": "role2",
"subRole": "subrole4",
"artist": {
"id": "2",
"name": "artist2"
}
}
],
"displayComposer": "composer 1 \u0026 composer 2",
"explicitStatus": "clean"
}
]
}
@@ -1,5 +1,5 @@
<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.8.0" type="navidrome" serverVersion="v0.0.0" openSubsonic="true">
<album id="1" name="album" artist="artist" genre="rock" userRating="0" musicBrainzId="1234" isCompilation="true" sortName="sorted album">
<album id="1" name="album" artist="artist" genre="rock" userRating="4" musicBrainzId="1234" isCompilation="true" sortName="sorted album" displayArtist="artist1 &amp; artist2" explicitStatus="clean" version="Deluxe Edition">
<genres name="rock"></genres>
<genres name="progressive"></genres>
<discTitles disc="1" title="disc 1"></discTitles>
@@ -7,10 +7,30 @@
<discTitles disc="3" title=""></discTitles>
<originalReleaseDate year="1994" month="2" day="4"></originalReleaseDate>
<releaseDate year="2000" month="5" day="10"></releaseDate>
<song id="1" isDir="true" title="title" album="album" artist="artist" track="1" year="1985" genre="Rock" coverArt="1" size="8421341" contentType="audio/flac" suffix="flac" starred="2016-03-02T20:30:00Z" transcodedContentType="audio/mpeg" transcodedSuffix="mp3" duration="146" bitRate="320" isVideo="false" bpm="127" comment="a comment" sortName="sorted song" mediaType="song" musicBrainzId="4321" channelCount="0" samplingRate="0">
<releaseTypes>album</releaseTypes>
<releaseTypes>live</releaseTypes>
<recordLabels name="label1"></recordLabels>
<recordLabels name="label2"></recordLabels>
<moods>happy</moods>
<moods>sad</moods>
<artists id="1" name="artist1"></artists>
<artists id="2" name="artist2"></artists>
<song id="1" isDir="true" title="title" album="album" artist="artist" track="1" year="1985" genre="Rock" coverArt="1" size="8421341" contentType="audio/flac" suffix="flac" starred="2016-03-02T20:30:00Z" transcodedContentType="audio/mpeg" transcodedSuffix="mp3" duration="146" bitRate="320" isVideo="false" bpm="127" comment="a comment" sortName="sorted song" mediaType="song" musicBrainzId="4321" channelCount="2" samplingRate="44100" bitDepth="16" displayArtist="artist1 &amp; artist2" displayAlbumArtist="album artist1 &amp; album artist2" displayComposer="composer 1 &amp; composer 2" explicitStatus="clean">
<genres name="rock"></genres>
<genres name="progressive"></genres>
<replayGain trackGain="1" albumGain="2" trackPeak="3" albumPeak="4" baseGain="5" fallbackGain="6"></replayGain>
<moods>happy</moods>
<moods>sad</moods>
<artists id="1" name="artist1"></artists>
<artists id="2" name="artist2"></artists>
<albumArtists id="1" name="album artist1"></albumArtists>
<albumArtists id="2" name="album artist2"></albumArtists>
<contributors role="role1">
<artist id="1" name="artist1"></artist>
</contributors>
<contributors role="role2" subRole="subrole4">
<artist id="2" name="artist2"></artist>
</contributors>
</song>
</album>
</subsonic-response>
@@ -6,14 +6,6 @@
"openSubsonic": true,
"album": {
"id": "",
"name": "",
"userRating": 0,
"genres": [],
"musicBrainzId": "",
"isCompilation": false,
"sortName": "",
"discTitles": [],
"originalReleaseDate": {},
"releaseDate": {}
"name": ""
}
}
@@ -1,6 +1,3 @@
<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.8.0" type="navidrome" serverVersion="v0.0.0" openSubsonic="true">
<album id="" name="" userRating="0" musicBrainzId="" isCompilation="false" sortName="">
<originalReleaseDate></originalReleaseDate>
<releaseDate></releaseDate>
</album>
<album id="" name=""></album>
</subsonic-response>
@@ -0,0 +1,26 @@
{
"status": "ok",
"version": "1.8.0",
"type": "navidrome",
"serverVersion": "v0.0.0",
"openSubsonic": true,
"album": {
"id": "",
"name": "",
"userRating": 0,
"genres": [],
"musicBrainzId": "",
"isCompilation": false,
"sortName": "",
"discTitles": [],
"originalReleaseDate": {},
"releaseDate": {},
"releaseTypes": [],
"recordLabels": [],
"moods": [],
"artists": [],
"displayArtist": "",
"explicitStatus": "",
"version": ""
}
}
@@ -0,0 +1,3 @@
<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.8.0" type="navidrome" serverVersion="v0.0.0" openSubsonic="true">
<album id="" name=""></album>
</subsonic-response>
@@ -0,0 +1,32 @@
{
"status": "ok",
"version": "1.8.0",
"type": "navidrome",
"serverVersion": "v0.0.0",
"openSubsonic": true,
"artists": {
"index": [
{
"name": "A",
"artist": [
{
"id": "111",
"name": "aaa",
"albumCount": 2,
"starred": "2016-03-02T20:30:00Z",
"userRating": 3,
"artistImageUrl": "https://lastfm.freetls.fastly.net/i/u/300x300/2a96cbd8b46e442fc41c2b86b821562f.png",
"musicBrainzId": "1234",
"sortName": "sort name",
"roles": [
"role1",
"role2"
]
}
]
}
],
"lastModified": 1,
"ignoredArticles": "A"
}
}
@@ -0,0 +1,10 @@
<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.8.0" type="navidrome" serverVersion="v0.0.0" openSubsonic="true">
<artists lastModified="1" ignoredArticles="A">
<index name="A">
<artist id="111" name="aaa" albumCount="2" starred="2016-03-02T20:30:00Z" userRating="3" artistImageUrl="https://lastfm.freetls.fastly.net/i/u/300x300/2a96cbd8b46e442fc41c2b86b821562f.png" musicBrainzId="1234" sortName="sort name">
<roles>role1</roles>
<roles>role2</roles>
</artist>
</index>
</artists>
</subsonic-response>
@@ -17,7 +17,11 @@
"userRating": 3,
"artistImageUrl": "https://lastfm.freetls.fastly.net/i/u/300x300/2a96cbd8b46e442fc41c2b86b821562f.png",
"musicBrainzId": "1234",
"sortName": "sort name"
"sortName": "sort name",
"roles": [
"role1",
"role2"
]
}
]
}
@@ -1,7 +1,10 @@
<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.8.0" type="navidrome" serverVersion="v0.0.0" openSubsonic="true">
<artists lastModified="1" ignoredArticles="A">
<index name="A">
<artist id="111" name="aaa" albumCount="2" starred="2016-03-02T20:30:00Z" userRating="3" artistImageUrl="https://lastfm.freetls.fastly.net/i/u/300x300/2a96cbd8b46e442fc41c2b86b821562f.png" musicBrainzId="1234" sortName="sort name"></artist>
<artist id="111" name="aaa" albumCount="2" starred="2016-03-02T20:30:00Z" userRating="3" artistImageUrl="https://lastfm.freetls.fastly.net/i/u/300x300/2a96cbd8b46e442fc41c2b86b821562f.png" musicBrainzId="1234" sortName="sort name">
<roles>role1</roles>
<roles>role2</roles>
</artist>
</index>
</artists>
</subsonic-response>
@@ -15,9 +15,7 @@
"albumCount": 2,
"starred": "2016-03-02T20:30:00Z",
"userRating": 3,
"artistImageUrl": "https://lastfm.freetls.fastly.net/i/u/300x300/2a96cbd8b46e442fc41c2b86b821562f.png",
"musicBrainzId": "",
"sortName": ""
"artistImageUrl": "https://lastfm.freetls.fastly.net/i/u/300x300/2a96cbd8b46e442fc41c2b86b821562f.png"
}
]
}
@@ -1,7 +1,7 @@
<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.8.0" type="navidrome" serverVersion="v0.0.0" openSubsonic="true">
<artists lastModified="1" ignoredArticles="A">
<index name="A">
<artist id="111" name="aaa" albumCount="2" starred="2016-03-02T20:30:00Z" userRating="3" artistImageUrl="https://lastfm.freetls.fastly.net/i/u/300x300/2a96cbd8b46e442fc41c2b86b821562f.png" musicBrainzId="" sortName=""></artist>
<artist id="111" name="aaa" albumCount="2" starred="2016-03-02T20:30:00Z" userRating="3" artistImageUrl="https://lastfm.freetls.fastly.net/i/u/300x300/2a96cbd8b46e442fc41c2b86b821562f.png"></artist>
</index>
</artists>
</subsonic-response>
@@ -5,7 +5,7 @@
"serverVersion": "v0.0.0",
"openSubsonic": true,
"artistInfo": {
"biography": "Black Sabbath is an English \u003ca target='_blank' href=\"http://www.last.fm/tag/heavy%20metal\" class=\"bbcode_tag\" rel=\"tag\"\u003eheavy metal\u003c/a\u003e band",
"biography": "Black Sabbath is an English \u003ca target='_blank' href=\"https://www.last.fm/tag/heavy%20metal\" class=\"bbcode_tag\" rel=\"tag\"\u003eheavy metal\u003c/a\u003e band",
"musicBrainzId": "5182c1d9-c7d2-4dad-afa0-ccfeada921a8",
"lastFmUrl": "https://www.last.fm/music/Black+Sabbath",
"smallImageUrl": "https://userserve-ak.last.fm/serve/64/27904353.jpg",
@@ -1,6 +1,6 @@
<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.8.0" type="navidrome" serverVersion="v0.0.0" openSubsonic="true">
<artistInfo>
<biography>Black Sabbath is an English &lt;a target=&#39;_blank&#39; href=&#34;http://www.last.fm/tag/heavy%20metal&#34; class=&#34;bbcode_tag&#34; rel=&#34;tag&#34;&gt;heavy metal&lt;/a&gt; band</biography>
<biography>Black Sabbath is an English &lt;a target=&#39;_blank&#39; href=&#34;https://www.last.fm/tag/heavy%20metal&#34; class=&#34;bbcode_tag&#34; rel=&#34;tag&#34;&gt;heavy metal&lt;/a&gt; band</biography>
<musicBrainzId>5182c1d9-c7d2-4dad-afa0-ccfeada921a8</musicBrainzId>
<lastFmUrl>https://www.last.fm/music/Black+Sabbath</lastFmUrl>
<smallImageUrl>https://userserve-ak.last.fm/serve/64/27904353.jpg</smallImageUrl>
@@ -11,16 +11,7 @@
"id": "1",
"isDir": false,
"title": "title",
"isVideo": false,
"bpm": 0,
"comment": "",
"sortName": "",
"mediaType": "",
"musicBrainzId": "",
"genres": [],
"replayGain": {},
"channelCount": 0,
"samplingRate": 0
"isVideo": false
},
"position": 123,
"username": "user2",
@@ -1,9 +1,7 @@
<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.8.0" type="navidrome" serverVersion="v0.0.0" openSubsonic="true">
<bookmarks>
<bookmark position="123" username="user2" comment="a comment" created="0001-01-01T00:00:00Z" changed="0001-01-01T00:00:00Z">
<entry id="1" isDir="false" title="title" isVideo="false" bpm="0" comment="" sortName="" mediaType="" musicBrainzId="" channelCount="0" samplingRate="0">
<replayGain></replayGain>
</entry>
<entry id="1" isDir="false" title="title" isVideo="false"></entry>
</bookmark>
</bookmarks>
</subsonic-response>
@@ -47,7 +47,67 @@
"fallbackGain": 6
},
"channelCount": 2,
"samplingRate": 44100
"samplingRate": 44100,
"bitDepth": 16,
"moods": [
"happy",
"sad"
],
"artists": [
{
"id": "1",
"name": "artist1"
},
{
"id": "2",
"name": "artist2"
}
],
"displayArtist": "artist 1 \u0026 artist 2",
"albumArtists": [
{
"id": "1",
"name": "album artist1"
},
{
"id": "2",
"name": "album artist2"
}
],
"displayAlbumArtist": "album artist 1 \u0026 album artist 2",
"contributors": [
{
"role": "role1",
"subRole": "subrole3",
"artist": {
"id": "1",
"name": "artist1"
}
},
{
"role": "role2",
"artist": {
"id": "2",
"name": "artist2"
}
},
{
"role": "composer",
"artist": {
"id": "3",
"name": "composer1"
}
},
{
"role": "composer",
"artist": {
"id": "4",
"name": "composer2"
}
}
],
"displayComposer": "composer 1 \u0026 composer 2",
"explicitStatus": "clean"
}
],
"id": "1",
@@ -1,9 +1,27 @@
<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.8.0" type="navidrome" serverVersion="v0.0.0" openSubsonic="true">
<directory id="1" name="N">
<child id="1" isDir="true" title="title" album="album" artist="artist" track="1" year="1985" genre="Rock" coverArt="1" size="8421341" contentType="audio/flac" suffix="flac" starred="2016-03-02T20:30:00Z" transcodedContentType="audio/mpeg" transcodedSuffix="mp3" duration="146" bitRate="320" isVideo="false" bpm="127" comment="a comment" sortName="sorted title" mediaType="song" musicBrainzId="4321" channelCount="2" samplingRate="44100">
<child id="1" isDir="true" title="title" album="album" artist="artist" track="1" year="1985" genre="Rock" coverArt="1" size="8421341" contentType="audio/flac" suffix="flac" starred="2016-03-02T20:30:00Z" transcodedContentType="audio/mpeg" transcodedSuffix="mp3" duration="146" bitRate="320" isVideo="false" bpm="127" comment="a comment" sortName="sorted title" mediaType="song" musicBrainzId="4321" channelCount="2" samplingRate="44100" bitDepth="16" displayArtist="artist 1 &amp; artist 2" displayAlbumArtist="album artist 1 &amp; album artist 2" displayComposer="composer 1 &amp; composer 2" explicitStatus="clean">
<genres name="rock"></genres>
<genres name="progressive"></genres>
<replayGain trackGain="1" albumGain="2" trackPeak="3" albumPeak="4" baseGain="5" fallbackGain="6"></replayGain>
<moods>happy</moods>
<moods>sad</moods>
<artists id="1" name="artist1"></artists>
<artists id="2" name="artist2"></artists>
<albumArtists id="1" name="album artist1"></albumArtists>
<albumArtists id="2" name="album artist2"></albumArtists>
<contributors role="role1" subRole="subrole3">
<artist id="1" name="artist1"></artist>
</contributors>
<contributors role="role2">
<artist id="2" name="artist2"></artist>
</contributors>
<contributors role="composer">
<artist id="3" name="composer1"></artist>
</contributors>
<contributors role="composer">
<artist id="4" name="composer2"></artist>
</contributors>
</child>
</directory>
</subsonic-response>
@@ -9,16 +9,7 @@
{
"id": "1",
"isDir": false,
"isVideo": false,
"bpm": 0,
"comment": "",
"sortName": "",
"mediaType": "",
"musicBrainzId": "",
"genres": [],
"replayGain": {},
"channelCount": 0,
"samplingRate": 0
"isVideo": false
}
],
"id": "",
@@ -1,7 +1,5 @@
<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.8.0" type="navidrome" serverVersion="v0.0.0" openSubsonic="true">
<directory id="" name="">
<child id="1" isDir="false" isVideo="false" bpm="0" comment="" sortName="" mediaType="" musicBrainzId="" channelCount="0" samplingRate="0">
<replayGain></replayGain>
</child>
<child id="1" isDir="false" isVideo="false"></child>
</directory>
</subsonic-response>
@@ -0,0 +1,36 @@
{
"status": "ok",
"version": "1.8.0",
"type": "navidrome",
"serverVersion": "v0.0.0",
"openSubsonic": true,
"directory": {
"child": [
{
"id": "1",
"isDir": false,
"isVideo": false,
"bpm": 0,
"comment": "",
"sortName": "",
"mediaType": "",
"musicBrainzId": "",
"genres": [],
"replayGain": {},
"channelCount": 0,
"samplingRate": 0,
"bitDepth": 0,
"moods": [],
"artists": [],
"displayArtist": "",
"albumArtists": [],
"displayAlbumArtist": "",
"contributors": [],
"displayComposer": "",
"explicitStatus": ""
}
],
"id": "",
"name": ""
}
}
@@ -0,0 +1,5 @@
<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.8.0" type="navidrome" serverVersion="v0.0.0" openSubsonic="true">
<directory id="" name="">
<child id="1" isDir="false" isVideo="false"></child>
</directory>
</subsonic-response>
@@ -10,16 +10,7 @@
"id": "1",
"isDir": false,
"title": "title",
"isVideo": false,
"bpm": 0,
"comment": "",
"sortName": "",
"mediaType": "",
"musicBrainzId": "",
"genres": [],
"replayGain": {},
"channelCount": 0,
"samplingRate": 0
"isVideo": false
}
],
"id": "1",
@@ -1,7 +1,5 @@
<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.8.0" type="navidrome" serverVersion="v0.0.0" openSubsonic="true">
<directory id="1" name="N">
<child id="1" isDir="false" title="title" isVideo="false" bpm="0" comment="" sortName="" mediaType="" musicBrainzId="" channelCount="0" samplingRate="0">
<replayGain></replayGain>
</child>
<child id="1" isDir="false" title="title" isVideo="false"></child>
</directory>
</subsonic-response>
@@ -10,16 +10,7 @@
"id": "1",
"isDir": false,
"title": "title",
"isVideo": false,
"bpm": 0,
"comment": "",
"sortName": "",
"mediaType": "",
"musicBrainzId": "",
"genres": [],
"replayGain": {},
"channelCount": 0,
"samplingRate": 0
"isVideo": false
}
],
"current": "111",
@@ -1,7 +1,5 @@
<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.8.0" type="navidrome" serverVersion="v0.0.0" openSubsonic="true">
<playQueue current="111" position="243" username="user1" changed="0001-01-01T00:00:00Z" changedBy="a_client">
<entry id="1" isDir="false" title="title" isVideo="false" bpm="0" comment="" sortName="" mediaType="" musicBrainzId="" channelCount="0" samplingRate="0">
<replayGain></replayGain>
</entry>
<entry id="1" isDir="false" title="title" isVideo="false"></entry>
</playQueue>
</subsonic-response>
@@ -15,16 +15,7 @@
"album": "album",
"artist": "artist",
"duration": 120,
"isVideo": false,
"bpm": 0,
"comment": "",
"sortName": "",
"mediaType": "",
"musicBrainzId": "",
"genres": [],
"replayGain": {},
"channelCount": 0,
"samplingRate": 0
"isVideo": false
},
{
"id": "2",
@@ -33,16 +24,7 @@
"album": "album",
"artist": "artist",
"duration": 300,
"isVideo": false,
"bpm": 0,
"comment": "",
"sortName": "",
"mediaType": "",
"musicBrainzId": "",
"genres": [],
"replayGain": {},
"channelCount": 0,
"samplingRate": 0
"isVideo": false
}
],
"id": "ABC123",
@@ -1,12 +1,8 @@
<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.8.0" type="navidrome" serverVersion="v0.0.0" openSubsonic="true">
<shares>
<share id="ABC123" url="http://localhost/p/ABC123" description="Check it out!" username="deluan" created="2016-03-02T20:30:00Z" expires="2016-03-02T20:30:00Z" lastVisited="2016-03-02T20:30:00Z" visitCount="2">
<entry id="1" isDir="false" title="title" album="album" artist="artist" duration="120" isVideo="false" bpm="0" comment="" sortName="" mediaType="" musicBrainzId="" channelCount="0" samplingRate="0">
<replayGain></replayGain>
</entry>
<entry id="2" isDir="false" title="title 2" album="album" artist="artist" duration="300" isVideo="false" bpm="0" comment="" sortName="" mediaType="" musicBrainzId="" channelCount="0" samplingRate="0">
<replayGain></replayGain>
</entry>
<entry id="1" isDir="false" title="title" album="album" artist="artist" duration="120" isVideo="false"></entry>
<entry id="2" isDir="false" title="title 2" album="album" artist="artist" duration="300" isVideo="false"></entry>
</share>
</shares>
</subsonic-response>
@@ -10,16 +10,7 @@
"id": "1",
"isDir": false,
"title": "title",
"isVideo": false,
"bpm": 0,
"comment": "",
"sortName": "",
"mediaType": "",
"musicBrainzId": "",
"genres": [],
"replayGain": {},
"channelCount": 0,
"samplingRate": 0
"isVideo": false
}
]
}
@@ -1,7 +1,5 @@
<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.8.0" type="navidrome" serverVersion="v0.0.0" openSubsonic="true">
<similarSongs>
<song id="1" isDir="false" title="title" isVideo="false" bpm="0" comment="" sortName="" mediaType="" musicBrainzId="" channelCount="0" samplingRate="0">
<replayGain></replayGain>
</song>
<song id="1" isDir="false" title="title" isVideo="false"></song>
</similarSongs>
</subsonic-response>
@@ -10,16 +10,7 @@
"id": "1",
"isDir": false,
"title": "title",
"isVideo": false,
"bpm": 0,
"comment": "",
"sortName": "",
"mediaType": "",
"musicBrainzId": "",
"genres": [],
"replayGain": {},
"channelCount": 0,
"samplingRate": 0
"isVideo": false
}
]
}
@@ -1,7 +1,5 @@
<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.8.0" type="navidrome" serverVersion="v0.0.0" openSubsonic="true">
<similarSongs2>
<song id="1" isDir="false" title="title" isVideo="false" bpm="0" comment="" sortName="" mediaType="" musicBrainzId="" channelCount="0" samplingRate="0">
<replayGain></replayGain>
</song>
<song id="1" isDir="false" title="title" isVideo="false"></song>
</similarSongs2>
</subsonic-response>
@@ -10,16 +10,7 @@
"id": "1",
"isDir": false,
"title": "title",
"isVideo": false,
"bpm": 0,
"comment": "",
"sortName": "",
"mediaType": "",
"musicBrainzId": "",
"genres": [],
"replayGain": {},
"channelCount": 0,
"samplingRate": 0
"isVideo": false
}
]
}
@@ -1,7 +1,5 @@
<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.8.0" type="navidrome" serverVersion="v0.0.0" openSubsonic="true">
<topSongs>
<song id="1" isDir="false" title="title" isVideo="false" bpm="0" comment="" sortName="" mediaType="" musicBrainzId="" channelCount="0" samplingRate="0">
<replayGain></replayGain>
</song>
<song id="1" isDir="false" title="title" isVideo="false"></song>
</topSongs>
</subsonic-response>
+114 -60
View File
@@ -57,8 +57,9 @@ type Subsonic struct {
JukeboxStatus *JukeboxStatus `xml:"jukeboxStatus,omitempty" json:"jukeboxStatus,omitempty"`
JukeboxPlaylist *JukeboxPlaylist `xml:"jukeboxPlaylist,omitempty" json:"jukeboxPlaylist,omitempty"`
// OpenSubsonic extensions
OpenSubsonicExtensions *OpenSubsonicExtensions `xml:"openSubsonicExtensions,omitempty" json:"openSubsonicExtensions,omitempty"`
LyricsList *LyricsList `xml:"lyricsList,omitempty" json:"lyricsList,omitempty"`
LyricsList *LyricsList `xml:"lyricsList,omitempty" json:"lyricsList,omitempty"`
}
const (
@@ -165,17 +166,30 @@ type Child struct {
/*
<xs:attribute name="averageRating" type="sub:AverageRating" use="optional"/> <!-- Added in 1.6.0 -->
*/
*OpenSubsonicChild `xml:",omitempty" json:",omitempty"`
}
type OpenSubsonicChild struct {
// OpenSubsonic extensions
Played *time.Time `xml:"played,attr,omitempty" json:"played,omitempty"`
Bpm int32 `xml:"bpm,attr" json:"bpm"`
Comment string `xml:"comment,attr" json:"comment"`
SortName string `xml:"sortName,attr" json:"sortName"`
MediaType MediaType `xml:"mediaType,attr" json:"mediaType"`
MusicBrainzId string `xml:"musicBrainzId,attr" json:"musicBrainzId"`
Genres ItemGenres `xml:"genres" json:"genres"`
ReplayGain ReplayGain `xml:"replayGain" json:"replayGain"`
ChannelCount int32 `xml:"channelCount,attr" json:"channelCount"`
SamplingRate int32 `xml:"samplingRate,attr" json:"samplingRate"`
Played *time.Time `xml:"played,attr,omitempty" json:"played,omitempty"`
BPM int32 `xml:"bpm,attr,omitempty" json:"bpm"`
Comment string `xml:"comment,attr,omitempty" json:"comment"`
SortName string `xml:"sortName,attr,omitempty" json:"sortName"`
MediaType MediaType `xml:"mediaType,attr,omitempty" json:"mediaType"`
MusicBrainzId string `xml:"musicBrainzId,attr,omitempty" json:"musicBrainzId"`
Genres Array[ItemGenre] `xml:"genres,omitempty" json:"genres"`
ReplayGain ReplayGain `xml:"replayGain,omitempty" json:"replayGain"`
ChannelCount int32 `xml:"channelCount,attr,omitempty" json:"channelCount"`
SamplingRate int32 `xml:"samplingRate,attr,omitempty" json:"samplingRate"`
BitDepth int32 `xml:"bitDepth,attr,omitempty" json:"bitDepth"`
Moods Array[string] `xml:"moods,omitempty" json:"moods"`
Artists Array[ArtistID3Ref] `xml:"artists,omitempty" json:"artists"`
DisplayArtist string `xml:"displayArtist,attr,omitempty" json:"displayArtist"`
AlbumArtists Array[ArtistID3Ref] `xml:"albumArtists,omitempty" json:"albumArtists"`
DisplayAlbumArtist string `xml:"displayAlbumArtist,attr,omitempty" json:"displayAlbumArtist"`
Contributors Array[Contributor] `xml:"contributors,omitempty" json:"contributors"`
DisplayComposer string `xml:"displayComposer,attr,omitempty" json:"displayComposer"`
ExplicitStatus string `xml:"explicitStatus,attr,omitempty" json:"explicitStatus"`
}
type Songs struct {
@@ -208,44 +222,65 @@ type Directory struct {
*/
}
type ArtistID3 struct {
Id string `xml:"id,attr" json:"id"`
Name string `xml:"name,attr" json:"name"`
CoverArt string `xml:"coverArt,attr,omitempty" json:"coverArt,omitempty"`
AlbumCount int32 `xml:"albumCount,attr,omitempty" json:"albumCount,omitempty"`
Starred *time.Time `xml:"starred,attr,omitempty" json:"starred,omitempty"`
UserRating int32 `xml:"userRating,attr,omitempty" json:"userRating,omitempty"`
ArtistImageUrl string `xml:"artistImageUrl,attr,omitempty" json:"artistImageUrl,omitempty"`
// ArtistID3Ref is a reference to an artist, a simplified version of ArtistID3. This is used to resolve the
// documentation conflict in OpenSubsonic: https://github.com/opensubsonic/open-subsonic-api/discussions/120
type ArtistID3Ref struct {
Id string `xml:"id,attr" json:"id"`
Name string `xml:"name,attr" json:"name"`
}
type ArtistID3 struct {
Id string `xml:"id,attr" json:"id"`
Name string `xml:"name,attr" json:"name"`
CoverArt string `xml:"coverArt,attr,omitempty" json:"coverArt,omitempty"`
AlbumCount int32 `xml:"albumCount,attr,omitempty" json:"albumCount,omitempty"`
Starred *time.Time `xml:"starred,attr,omitempty" json:"starred,omitempty"`
UserRating int32 `xml:"userRating,attr,omitempty" json:"userRating,omitempty"`
ArtistImageUrl string `xml:"artistImageUrl,attr,omitempty" json:"artistImageUrl,omitempty"`
*OpenSubsonicArtistID3 `xml:",omitempty" json:",omitempty"`
}
type OpenSubsonicArtistID3 struct {
// OpenSubsonic extensions
MusicBrainzId string `xml:"musicBrainzId,attr" json:"musicBrainzId"`
SortName string `xml:"sortName,attr" json:"sortName"`
MusicBrainzId string `xml:"musicBrainzId,attr,omitempty" json:"musicBrainzId"`
SortName string `xml:"sortName,attr,omitempty" json:"sortName"`
Roles Array[string] `xml:"roles,omitempty" json:"roles"`
}
type AlbumID3 struct {
Id string `xml:"id,attr" json:"id"`
Name string `xml:"name,attr" json:"name"`
Artist string `xml:"artist,attr,omitempty" json:"artist,omitempty"`
ArtistId string `xml:"artistId,attr,omitempty" json:"artistId,omitempty"`
CoverArt string `xml:"coverArt,attr,omitempty" json:"coverArt,omitempty"`
SongCount int32 `xml:"songCount,attr,omitempty" json:"songCount,omitempty"`
Duration int32 `xml:"duration,attr,omitempty" json:"duration,omitempty"`
PlayCount int64 `xml:"playCount,attr,omitempty" json:"playCount,omitempty"`
Created *time.Time `xml:"created,attr,omitempty" json:"created,omitempty"`
Starred *time.Time `xml:"starred,attr,omitempty" json:"starred,omitempty"`
Year int32 `xml:"year,attr,omitempty" json:"year,omitempty"`
Genre string `xml:"genre,attr,omitempty" json:"genre,omitempty"`
Id string `xml:"id,attr" json:"id"`
Name string `xml:"name,attr" json:"name"`
Artist string `xml:"artist,attr,omitempty" json:"artist,omitempty"`
ArtistId string `xml:"artistId,attr,omitempty" json:"artistId,omitempty"`
CoverArt string `xml:"coverArt,attr,omitempty" json:"coverArt,omitempty"`
SongCount int32 `xml:"songCount,attr,omitempty" json:"songCount,omitempty"`
Duration int32 `xml:"duration,attr,omitempty" json:"duration,omitempty"`
PlayCount int64 `xml:"playCount,attr,omitempty" json:"playCount,omitempty"`
Created *time.Time `xml:"created,attr,omitempty" json:"created,omitempty"`
Starred *time.Time `xml:"starred,attr,omitempty" json:"starred,omitempty"`
Year int32 `xml:"year,attr,omitempty" json:"year,omitempty"`
Genre string `xml:"genre,attr,omitempty" json:"genre,omitempty"`
*OpenSubsonicAlbumID3 `xml:",omitempty" json:",omitempty"`
}
type OpenSubsonicAlbumID3 struct {
// OpenSubsonic extensions
Played *time.Time `xml:"played,attr,omitempty" json:"played,omitempty"`
UserRating int32 `xml:"userRating,attr" json:"userRating"`
Genres ItemGenres `xml:"genres" json:"genres"`
MusicBrainzId string `xml:"musicBrainzId,attr" json:"musicBrainzId"`
IsCompilation bool `xml:"isCompilation,attr" json:"isCompilation"`
SortName string `xml:"sortName,attr" json:"sortName"`
DiscTitles DiscTitles `xml:"discTitles" json:"discTitles"`
OriginalReleaseDate ItemDate `xml:"originalReleaseDate" json:"originalReleaseDate"`
ReleaseDate ItemDate `xml:"releaseDate" json:"releaseDate"`
Played *time.Time `xml:"played,attr,omitempty" json:"played,omitempty"`
UserRating int32 `xml:"userRating,attr,omitempty" json:"userRating"`
Genres Array[ItemGenre] `xml:"genres,omitempty" json:"genres"`
MusicBrainzId string `xml:"musicBrainzId,attr,omitempty" json:"musicBrainzId"`
IsCompilation bool `xml:"isCompilation,attr,omitempty" json:"isCompilation"`
SortName string `xml:"sortName,attr,omitempty" json:"sortName"`
DiscTitles Array[DiscTitle] `xml:"discTitles,omitempty" json:"discTitles"`
OriginalReleaseDate ItemDate `xml:"originalReleaseDate,omitempty" json:"originalReleaseDate"`
ReleaseDate ItemDate `xml:"releaseDate,omitempty" json:"releaseDate"`
ReleaseTypes Array[string] `xml:"releaseTypes,omitempty" json:"releaseTypes"`
RecordLabels Array[RecordLabel] `xml:"recordLabels,omitempty" json:"recordLabels"`
Moods Array[string] `xml:"moods,omitempty" json:"moods"`
Artists Array[ArtistID3Ref] `xml:"artists,omitempty" json:"artists"`
DisplayArtist string `xml:"displayArtist,attr,omitempty" json:"displayArtist"`
ExplicitStatus string `xml:"explicitStatus,attr,omitempty" json:"explicitStatus"`
Version string `xml:"version,attr,omitempty" json:"version"`
}
type ArtistWithAlbumsID3 struct {
@@ -497,13 +532,6 @@ type ItemGenre struct {
Name string `xml:"name,attr" json:"name"`
}
// ItemGenres holds a list of genres (OpenSubsonic). If it is null, it must be marshalled as an empty array.
type ItemGenres []ItemGenre
func (i ItemGenres) MarshalJSON() ([]byte, error) {
return marshalJSONArray(i)
}
type ReplayGain struct {
TrackGain float64 `xml:"trackGain,omitempty,attr" json:"trackGain,omitempty"`
AlbumGain float64 `xml:"albumGain,omitempty,attr" json:"albumGain,omitempty"`
@@ -513,15 +541,48 @@ type ReplayGain struct {
FallbackGain float64 `xml:"fallbackGain,omitempty,attr" json:"fallbackGain,omitempty"`
}
func (r ReplayGain) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
if r.TrackGain == 0 && r.AlbumGain == 0 && r.TrackPeak == 0 && r.AlbumPeak == 0 && r.BaseGain == 0 && r.FallbackGain == 0 {
return nil
}
type replayGain ReplayGain
return e.EncodeElement(replayGain(r), start)
}
type DiscTitle struct {
Disc int32 `xml:"disc,attr" json:"disc"`
Title string `xml:"title,attr" json:"title"`
}
type DiscTitles []DiscTitle
type ItemDate struct {
Year int32 `xml:"year,attr,omitempty" json:"year,omitempty"`
Month int32 `xml:"month,attr,omitempty" json:"month,omitempty"`
Day int32 `xml:"day,attr,omitempty" json:"day,omitempty"`
}
func (d DiscTitles) MarshalJSON() ([]byte, error) {
return marshalJSONArray(d)
func (d ItemDate) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
if d.Year == 0 && d.Month == 0 && d.Day == 0 {
return nil
}
type itemDate ItemDate
return e.EncodeElement(itemDate(d), start)
}
type RecordLabel struct {
Name string `xml:"name,attr" json:"name"`
}
type Contributor struct {
Role string `xml:"role,attr" json:"role"`
SubRole string `xml:"subRole,attr,omitempty" json:"subRole,omitempty"`
Artist ArtistID3Ref `xml:"artist" json:"artist"`
}
// Array is a generic type for marshalling slices to JSON. It is used to avoid marshalling empty slices as null.
type Array[T any] []T
func (a Array[T]) MarshalJSON() ([]byte, error) {
return marshalJSONArray(a)
}
// marshalJSONArray marshals a slice of any type to JSON. If the slice is empty, it is marshalled as an
@@ -530,12 +591,5 @@ func marshalJSONArray[T any](v []T) ([]byte, error) {
if len(v) == 0 {
return json.Marshal([]T{})
}
a := v
return json.Marshal(a)
}
type ItemDate struct {
Year int32 `xml:"year,attr,omitempty" json:"year,omitempty"`
Month int32 `xml:"month,attr,omitempty" json:"month,omitempty"`
Day int32 `xml:"day,attr,omitempty" json:"day,omitempty"`
return json.Marshal(v)
}
+89 -12
View File
@@ -159,7 +159,7 @@ var _ = Describe("Responses", func() {
})
})
Context("with data and MBID and Sort Name", func() {
Context("with OpenSubsonic data", func() {
BeforeEach(func() {
artists := make([]ArtistID3, 1)
t := time.Date(2016, 03, 2, 20, 30, 0, 0, time.UTC)
@@ -170,9 +170,13 @@ var _ = Describe("Responses", func() {
UserRating: 3,
AlbumCount: 2,
ArtistImageUrl: "https://lastfm.freetls.fastly.net/i/u/300x300/2a96cbd8b46e442fc41c2b86b821562f.png",
MusicBrainzId: "1234",
SortName: "sort name",
}
artists[0].OpenSubsonicArtistID3 = &OpenSubsonicArtistID3{
MusicBrainzId: "1234",
SortName: "sort name",
Roles: []string{"role1", "role2"},
}
index := make([]IndexID3, 1)
index[0] = IndexID3{Name: "A", Artists: artists}
response.Artist.Index = index
@@ -198,6 +202,14 @@ var _ = Describe("Responses", func() {
It("should match .JSON", func() {
Expect(json.MarshalIndent(response, "", " ")).To(MatchSnapshot())
})
It("should match OpenSubsonic .XML", func() {
response.Directory.Child[0].OpenSubsonicChild = &OpenSubsonicChild{}
Expect(xml.MarshalIndent(response, "", " ")).To(MatchSnapshot())
})
It("should match OpenSubsonic .JSON", func() {
response.Directory.Child[0].OpenSubsonicChild = &OpenSubsonicChild{}
Expect(json.MarshalIndent(response, "", " ")).To(MatchSnapshot())
})
})
Context("with data", func() {
BeforeEach(func() {
@@ -208,10 +220,32 @@ var _ = Describe("Responses", func() {
Id: "1", IsDir: true, Title: "title", Album: "album", Artist: "artist", Track: 1,
Year: 1985, Genre: "Rock", CoverArt: "1", Size: 8421341, ContentType: "audio/flac",
Suffix: "flac", TranscodedContentType: "audio/mpeg", TranscodedSuffix: "mp3",
Duration: 146, BitRate: 320, Starred: &t, Genres: []ItemGenre{{Name: "rock"}, {Name: "progressive"}},
Comment: "a comment", Bpm: 127, MediaType: MediaTypeSong, MusicBrainzId: "4321", ChannelCount: 2,
SamplingRate: 44100, SortName: "sorted title",
ReplayGain: ReplayGain{TrackGain: 1, AlbumGain: 2, TrackPeak: 3, AlbumPeak: 4, BaseGain: 5, FallbackGain: 6},
Duration: 146, BitRate: 320, Starred: &t,
}
child[0].OpenSubsonicChild = &OpenSubsonicChild{
Genres: []ItemGenre{{Name: "rock"}, {Name: "progressive"}},
Comment: "a comment", MediaType: MediaTypeSong, MusicBrainzId: "4321", SortName: "sorted title",
BPM: 127, ChannelCount: 2, SamplingRate: 44100, BitDepth: 16,
Moods: []string{"happy", "sad"},
ReplayGain: ReplayGain{TrackGain: 1, AlbumGain: 2, TrackPeak: 3, AlbumPeak: 4, BaseGain: 5, FallbackGain: 6},
DisplayArtist: "artist 1 & artist 2",
Artists: []ArtistID3Ref{
{Id: "1", Name: "artist1"},
{Id: "2", Name: "artist2"},
},
DisplayAlbumArtist: "album artist 1 & album artist 2",
AlbumArtists: []ArtistID3Ref{
{Id: "1", Name: "album artist1"},
{Id: "2", Name: "album artist2"},
},
DisplayComposer: "composer 1 & composer 2",
Contributors: []Contributor{
{Role: "role1", SubRole: "subrole3", Artist: ArtistID3Ref{Id: "1", Name: "artist1"}},
{Role: "role2", Artist: ArtistID3Ref{Id: "2", Name: "artist2"}},
{Role: "composer", Artist: ArtistID3Ref{Id: "3", Name: "composer1"}},
{Role: "composer", Artist: ArtistID3Ref{Id: "4", Name: "composer2"}},
},
ExplicitStatus: "clean",
}
response.Directory.Child = child
})
@@ -236,27 +270,69 @@ var _ = Describe("Responses", func() {
It("should match .JSON", func() {
Expect(json.MarshalIndent(response, "", " ")).To(MatchSnapshot())
})
It("should match OpenSubsonic .XML", func() {
response.AlbumWithSongsID3.OpenSubsonicAlbumID3 = &OpenSubsonicAlbumID3{}
Expect(xml.MarshalIndent(response, "", " ")).To(MatchSnapshot())
})
It("should match OpenSubsonic .JSON", func() {
response.AlbumWithSongsID3.OpenSubsonicAlbumID3 = &OpenSubsonicAlbumID3{}
Expect(json.MarshalIndent(response, "", " ")).To(MatchSnapshot())
})
})
Context("with data", func() {
BeforeEach(func() {
album := AlbumID3{
Id: "1", Name: "album", Artist: "artist", Genre: "rock",
}
album.OpenSubsonicAlbumID3 = &OpenSubsonicAlbumID3{
Genres: []ItemGenre{{Name: "rock"}, {Name: "progressive"}},
UserRating: 4,
MusicBrainzId: "1234", IsCompilation: true, SortName: "sorted album",
DiscTitles: DiscTitles{{Disc: 1, Title: "disc 1"}, {Disc: 2, Title: "disc 2"}, {Disc: 3}},
DiscTitles: Array[DiscTitle]{{Disc: 1, Title: "disc 1"}, {Disc: 2, Title: "disc 2"}, {Disc: 3}},
OriginalReleaseDate: ItemDate{Year: 1994, Month: 2, Day: 4},
ReleaseDate: ItemDate{Year: 2000, Month: 5, Day: 10},
ReleaseTypes: []string{"album", "live"},
RecordLabels: []RecordLabel{{Name: "label1"}, {Name: "label2"}},
Moods: []string{"happy", "sad"},
DisplayArtist: "artist1 & artist2",
Artists: []ArtistID3Ref{
{Id: "1", Name: "artist1"},
{Id: "2", Name: "artist2"},
},
ExplicitStatus: "clean",
Version: "Deluxe Edition",
}
t := time.Date(2016, 03, 2, 20, 30, 0, 0, time.UTC)
songs := []Child{{
Id: "1", IsDir: true, Title: "title", Album: "album", Artist: "artist", Track: 1,
Year: 1985, Genre: "Rock", CoverArt: "1", Size: 8421341, ContentType: "audio/flac",
Suffix: "flac", TranscodedContentType: "audio/mpeg", TranscodedSuffix: "mp3",
Duration: 146, BitRate: 320, Starred: &t, Genres: []ItemGenre{{Name: "rock"}, {Name: "progressive"}},
Comment: "a comment", Bpm: 127, MediaType: MediaTypeSong, MusicBrainzId: "4321", SortName: "sorted song",
ReplayGain: ReplayGain{TrackGain: 1, AlbumGain: 2, TrackPeak: 3, AlbumPeak: 4, BaseGain: 5, FallbackGain: 6},
Duration: 146, BitRate: 320, Starred: &t,
}}
songs[0].OpenSubsonicChild = &OpenSubsonicChild{
Genres: []ItemGenre{{Name: "rock"}, {Name: "progressive"}},
Comment: "a comment", MediaType: MediaTypeSong, MusicBrainzId: "4321", SortName: "sorted song",
Moods: []string{"happy", "sad"},
ReplayGain: ReplayGain{TrackGain: 1, AlbumGain: 2, TrackPeak: 3, AlbumPeak: 4, BaseGain: 5, FallbackGain: 6},
BPM: 127, ChannelCount: 2, SamplingRate: 44100, BitDepth: 16,
DisplayArtist: "artist1 & artist2",
Artists: []ArtistID3Ref{
{Id: "1", Name: "artist1"},
{Id: "2", Name: "artist2"},
},
DisplayAlbumArtist: "album artist1 & album artist2",
AlbumArtists: []ArtistID3Ref{
{Id: "1", Name: "album artist1"},
{Id: "2", Name: "album artist2"},
},
Contributors: []Contributor{
{Role: "role1", Artist: ArtistID3Ref{Id: "1", Name: "artist1"}},
{Role: "role2", SubRole: "subrole4", Artist: ArtistID3Ref{Id: "2", Name: "artist2"}},
},
DisplayComposer: "composer 1 & composer 2",
ExplicitStatus: "clean",
}
response.AlbumWithSongsID3.AlbumID3 = album
response.AlbumWithSongsID3.Song = songs
})
@@ -515,8 +591,9 @@ var _ = Describe("Responses", func() {
Context("with data", func() {
BeforeEach(func() {
response.ArtistInfo.Biography = `Black Sabbath is an English <a target='_blank' href="http://www.last.fm/tag/heavy%20metal" class="bbcode_tag" rel="tag">heavy metal</a> band`
response.ArtistInfo.Biography = `Black Sabbath is an English <a target='_blank' href="https://www.last.fm/tag/heavy%20metal" class="bbcode_tag" rel="tag">heavy metal</a> band`
response.ArtistInfo.MusicBrainzID = "5182c1d9-c7d2-4dad-afa0-ccfeada921a8"
response.ArtistInfo.LastFmUrl = "https://www.last.fm/music/Black+Sabbath"
response.ArtistInfo.SmallImageUrl = "https://userserve-ak.last.fm/serve/64/27904353.jpg"
response.ArtistInfo.MediumImageUrl = "https://userserve-ak.last.fm/serve/126/27904353.jpg"