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:
@@ -3,34 +3,122 @@ import PropTypes from 'prop-types'
|
||||
import { Link } from 'react-admin'
|
||||
import { withWidth } from '@material-ui/core'
|
||||
import { useGetHandleArtistClick } from './useGetHandleArtistClick'
|
||||
import { intersperse } from '../utils/index.js'
|
||||
import { useDispatch } from 'react-redux'
|
||||
import { closeExtendedInfoDialog } from '../actions/dialogs.js'
|
||||
|
||||
export const ArtistLinkField = withWidth()(({
|
||||
record,
|
||||
className,
|
||||
width,
|
||||
source,
|
||||
}) => {
|
||||
const ALink = withWidth()((props) => {
|
||||
const { artist, width, ...rest } = props
|
||||
const artistLink = useGetHandleArtistClick(width)
|
||||
const dispatch = useDispatch()
|
||||
|
||||
const id = record[source + 'Id']
|
||||
return (
|
||||
<>
|
||||
{id ? (
|
||||
<Link
|
||||
to={artistLink(id)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className={className}
|
||||
>
|
||||
{record[source]}
|
||||
</Link>
|
||||
) : (
|
||||
record[source]
|
||||
)}
|
||||
</>
|
||||
<Link
|
||||
key={artist.id}
|
||||
to={artistLink(artist.id)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
dispatch(closeExtendedInfoDialog())
|
||||
}}
|
||||
{...rest}
|
||||
>
|
||||
{artist.name}
|
||||
</Link>
|
||||
)
|
||||
})
|
||||
|
||||
const parseAndReplaceArtists = (
|
||||
displayAlbumArtist,
|
||||
albumArtists,
|
||||
className,
|
||||
) => {
|
||||
let result = []
|
||||
let lastIndex = 0
|
||||
|
||||
albumArtists?.forEach((artist) => {
|
||||
const index = displayAlbumArtist.indexOf(artist.name, lastIndex)
|
||||
if (index !== -1) {
|
||||
// Add text before the artist name
|
||||
if (index > lastIndex) {
|
||||
result.push(displayAlbumArtist.slice(lastIndex, index))
|
||||
}
|
||||
// Add the artist link
|
||||
result.push(<ALink artist={artist} className={className} />)
|
||||
lastIndex = index + artist.name.length
|
||||
}
|
||||
})
|
||||
|
||||
if (lastIndex === 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
// Add any remaining text after the last artist name
|
||||
if (lastIndex < displayAlbumArtist.length) {
|
||||
result.push(displayAlbumArtist.slice(lastIndex))
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export const ArtistLinkField = ({ record, className, limit, source }) => {
|
||||
const role = source.toLowerCase()
|
||||
const artists = record['participants']
|
||||
? record['participants'][role]
|
||||
: [{ name: record[source], id: record[source + 'Id'] }]
|
||||
|
||||
// When showing artists for a track, add any remixers to the list of artists
|
||||
if (
|
||||
role === 'artist' &&
|
||||
record['participants'] &&
|
||||
record['participants']['remixer']
|
||||
) {
|
||||
record['participants']['remixer'].forEach((remixer) => {
|
||||
artists.push(remixer)
|
||||
})
|
||||
}
|
||||
|
||||
if (role === 'albumartist') {
|
||||
const artistsLinks = parseAndReplaceArtists(
|
||||
record[source],
|
||||
artists,
|
||||
className,
|
||||
)
|
||||
if (artistsLinks.length > 0) {
|
||||
return <div className={className}>{artistsLinks}</div>
|
||||
}
|
||||
}
|
||||
|
||||
// Dedupe artists, only shows the first 3
|
||||
const seen = new Set()
|
||||
const dedupedArtists = []
|
||||
let limitedShow = false
|
||||
|
||||
for (const artist of artists ?? []) {
|
||||
if (!seen.has(artist.id)) {
|
||||
seen.add(artist.id)
|
||||
|
||||
if (dedupedArtists.length < limit) {
|
||||
dedupedArtists.push(artist)
|
||||
} else {
|
||||
limitedShow = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const artistsList = dedupedArtists.map((artist) => (
|
||||
<ALink artist={artist} className={className} key={artist?.id} />
|
||||
))
|
||||
|
||||
if (limitedShow) {
|
||||
artistsList.push(<span>...</span>)
|
||||
}
|
||||
|
||||
return <>{intersperse(artistsList, ' • ')}</>
|
||||
}
|
||||
|
||||
ArtistLinkField.propTypes = {
|
||||
limit: PropTypes.number,
|
||||
record: PropTypes.object,
|
||||
className: PropTypes.string,
|
||||
source: PropTypes.string,
|
||||
@@ -38,5 +126,6 @@ ArtistLinkField.propTypes = {
|
||||
|
||||
ArtistLinkField.defaultProps = {
|
||||
addLabel: true,
|
||||
limit: 3,
|
||||
source: 'albumArtist',
|
||||
}
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import List from '@material-ui/core/List'
|
||||
import ListItem from '@material-ui/core/ListItem'
|
||||
import ListItemIcon from '@material-ui/core/ListItemIcon'
|
||||
import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction'
|
||||
import ListItemText from '@material-ui/core/ListItemText'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import { sanitizeListRestProps } from 'react-admin'
|
||||
import { ArtistContextMenu, RatingField } from './index'
|
||||
import config from '../config'
|
||||
|
||||
const useStyles = makeStyles(
|
||||
{
|
||||
listItem: {
|
||||
padding: '10px',
|
||||
},
|
||||
title: {
|
||||
paddingRight: '10px',
|
||||
width: '80%',
|
||||
},
|
||||
rightIcon: {
|
||||
top: '26px',
|
||||
},
|
||||
},
|
||||
{ name: 'RaArtistSimpleList' },
|
||||
)
|
||||
|
||||
export const ArtistSimpleList = ({
|
||||
linkType,
|
||||
className,
|
||||
classes: classesOverride,
|
||||
data,
|
||||
hasBulkActions,
|
||||
ids,
|
||||
loading,
|
||||
selectedIds,
|
||||
total,
|
||||
...rest
|
||||
}) => {
|
||||
const classes = useStyles({ classes: classesOverride })
|
||||
return (
|
||||
(loading || total > 0) && (
|
||||
<List className={className} {...sanitizeListRestProps(rest)}>
|
||||
{ids.map(
|
||||
(id) =>
|
||||
data[id] && (
|
||||
<span key={id} onClick={() => linkType(id)}>
|
||||
<ListItem className={classes.listItem} button={true}>
|
||||
<ListItemText
|
||||
primary={
|
||||
<>
|
||||
<div className={classes.title}>{data[id].name}</div>
|
||||
{config.enableStarRating && (
|
||||
<RatingField
|
||||
record={data[id]}
|
||||
source={'rating'}
|
||||
resource={'artist'}
|
||||
size={'small'}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<ListItemSecondaryAction className={classes.rightIcon}>
|
||||
<ListItemIcon>
|
||||
<ArtistContextMenu record={data[id]} />
|
||||
</ListItemIcon>
|
||||
</ListItemSecondaryAction>
|
||||
</ListItem>
|
||||
</span>
|
||||
),
|
||||
)}
|
||||
</List>
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
ArtistSimpleList.propTypes = {
|
||||
className: PropTypes.string,
|
||||
classes: PropTypes.object,
|
||||
data: PropTypes.object,
|
||||
hasBulkActions: PropTypes.bool.isRequired,
|
||||
ids: PropTypes.array,
|
||||
selectedIds: PropTypes.arrayOf(PropTypes.any).isRequired,
|
||||
}
|
||||
|
||||
ArtistSimpleList.defaultProps = {
|
||||
hasBulkActions: false,
|
||||
selectedIds: [],
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import IconButton from '@material-ui/core/IconButton'
|
||||
import Menu from '@material-ui/core/Menu'
|
||||
import MenuItem from '@material-ui/core/MenuItem'
|
||||
import MoreVertIcon from '@material-ui/icons/MoreVert'
|
||||
import { MdQuestionMark } from 'react-icons/md'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import { useDataProvider, useNotify, useTranslate } from 'react-admin'
|
||||
import clsx from 'clsx'
|
||||
@@ -33,6 +34,25 @@ const useStyles = makeStyles({
|
||||
},
|
||||
})
|
||||
|
||||
const MoreButton = ({ record, onClick, info, ...rest }) => {
|
||||
const handleClick = record.missing
|
||||
? (e) => {
|
||||
e.preventDefault()
|
||||
info.action(record)
|
||||
e.stopPropagation()
|
||||
}
|
||||
: onClick
|
||||
return (
|
||||
<IconButton onClick={handleClick} size={'small'} {...rest}>
|
||||
{record?.missing ? (
|
||||
<MdQuestionMark fontSize={'large'} />
|
||||
) : (
|
||||
<MoreVertIcon fontSize={'small'} />
|
||||
)}
|
||||
</IconButton>
|
||||
)
|
||||
}
|
||||
|
||||
const ContextMenu = ({
|
||||
resource,
|
||||
showLove,
|
||||
@@ -158,24 +178,29 @@ const ContextMenu = ({
|
||||
|
||||
const open = Boolean(anchorEl)
|
||||
|
||||
if (!record) {
|
||||
return null
|
||||
}
|
||||
|
||||
const present = !record.missing
|
||||
|
||||
return (
|
||||
<span className={clsx(classes.noWrap, className)}>
|
||||
<LoveButton
|
||||
record={record}
|
||||
resource={resource}
|
||||
visible={config.enableFavourites && showLove}
|
||||
visible={config.enableFavourites && showLove && present}
|
||||
color={color}
|
||||
/>
|
||||
<IconButton
|
||||
<MoreButton
|
||||
record={record}
|
||||
onClick={handleClick}
|
||||
info={options.info}
|
||||
aria-label="more"
|
||||
aria-controls="context-menu"
|
||||
aria-haspopup="true"
|
||||
className={classes.menu}
|
||||
onClick={handleClick}
|
||||
size={'small'}
|
||||
>
|
||||
<MoreVertIcon fontSize={'small'} />
|
||||
</IconButton>
|
||||
/>
|
||||
<Menu
|
||||
id="context-menu"
|
||||
anchorEl={anchorEl}
|
||||
|
||||
@@ -46,7 +46,7 @@ export const LoveButton = ({
|
||||
<Button
|
||||
onClick={handleToggleLove}
|
||||
size={'small'}
|
||||
disabled={disabled || loading}
|
||||
disabled={disabled || loading || record?.missing}
|
||||
className={classes.love}
|
||||
{...rest}
|
||||
>
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import { TableRow, TableCell } from '@material-ui/core'
|
||||
import { humanize } from 'inflection'
|
||||
import { useTranslate } from 'react-admin'
|
||||
|
||||
import en from '../i18n/en.json'
|
||||
import { ArtistLinkField } from './index'
|
||||
|
||||
export const ParticipantsInfo = ({ classes, record }) => {
|
||||
const translate = useTranslate()
|
||||
const existingRoles = en?.resources?.artist?.roles ?? {}
|
||||
|
||||
const roles = []
|
||||
|
||||
if (record.participants) {
|
||||
for (const name of Object.keys(record.participants)) {
|
||||
if (name === 'albumartist' || name === 'artist') {
|
||||
continue
|
||||
}
|
||||
roles.push([name, record.participants[name].length])
|
||||
}
|
||||
}
|
||||
|
||||
if (roles.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{roles.length > 0 && (
|
||||
<TableRow key={`${record.id}-separator`}>
|
||||
<TableCell scope="row" className={classes.tableCell}></TableCell>
|
||||
<TableCell align="left">
|
||||
<h4>{translate(`resources.song.fields.participants`)}</h4>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
{roles.map(([role, count]) => (
|
||||
<TableRow key={`${record.id}-${role}`}>
|
||||
<TableCell scope="row" className={classes.tableCell}>
|
||||
{role in existingRoles
|
||||
? translate(`resources.artist.roles.${role}`, {
|
||||
smart_count: count,
|
||||
})
|
||||
: humanize(role)}
|
||||
:
|
||||
</TableCell>
|
||||
<TableCell align="left">
|
||||
<ArtistLinkField source={role} record={record} limit={Infinity} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import { useRecordContext } from 'react-admin'
|
||||
import config from '../config'
|
||||
|
||||
export const PathField = (props) => {
|
||||
const record = useRecordContext(props)
|
||||
return (
|
||||
<span>
|
||||
{record.libraryPath}
|
||||
{config.separator}
|
||||
{record.path}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
PathField.propTypes = {
|
||||
label: PropTypes.string,
|
||||
record: PropTypes.object,
|
||||
}
|
||||
|
||||
PathField.defaultProps = {
|
||||
addLabel: true,
|
||||
}
|
||||
@@ -54,6 +54,7 @@ export const RatingField = ({
|
||||
)}
|
||||
value={rating}
|
||||
size={size}
|
||||
disabled={record?.missing}
|
||||
emptyIcon={<StarBorderIcon fontSize="inherit" />}
|
||||
onChange={(e, newValue) => handleRating(e, newValue)}
|
||||
/>
|
||||
|
||||
@@ -14,7 +14,11 @@ export const SizeField = ({ source, ...rest }) => {
|
||||
const classes = useStyles()
|
||||
const record = useRecordContext(rest)
|
||||
if (!record) return null
|
||||
return <span className={classes.root}>{formatBytes(record[source])}</span>
|
||||
return (
|
||||
<span className={classes.root}>
|
||||
{record[source] ? formatBytes(record[source]) : '0 MB'}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
SizeField.propTypes = {
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import React, { useState } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { useDispatch } from 'react-redux'
|
||||
import { useTranslate } from 'react-admin'
|
||||
import { useNotify, usePermissions, useTranslate } from 'react-admin'
|
||||
import { IconButton, Menu, MenuItem } from '@material-ui/core'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import MoreVertIcon from '@material-ui/icons/MoreVert'
|
||||
import { MdQuestionMark } from 'react-icons/md'
|
||||
import clsx from 'clsx'
|
||||
import {
|
||||
playNext,
|
||||
@@ -19,6 +20,7 @@ import {
|
||||
import { LoveButton } from './LoveButton'
|
||||
import config from '../config'
|
||||
import { formatBytes } from '../utils'
|
||||
import { httpClient } from '../dataProvider'
|
||||
|
||||
const useStyles = makeStyles({
|
||||
noWrap: {
|
||||
@@ -26,6 +28,24 @@ const useStyles = makeStyles({
|
||||
},
|
||||
})
|
||||
|
||||
const MoreButton = ({ record, onClick, info }) => {
|
||||
const handleClick = record.missing
|
||||
? (e) => {
|
||||
info.action(record)
|
||||
e.stopPropagation()
|
||||
}
|
||||
: onClick
|
||||
return (
|
||||
<IconButton onClick={handleClick} size={'small'}>
|
||||
{record?.missing ? (
|
||||
<MdQuestionMark fontSize={'large'} />
|
||||
) : (
|
||||
<MoreVertIcon fontSize={'small'} />
|
||||
)}
|
||||
</IconButton>
|
||||
)
|
||||
}
|
||||
|
||||
export const SongContextMenu = ({
|
||||
resource,
|
||||
record,
|
||||
@@ -36,7 +56,10 @@ export const SongContextMenu = ({
|
||||
const classes = useStyles()
|
||||
const dispatch = useDispatch()
|
||||
const translate = useTranslate()
|
||||
const notify = useNotify()
|
||||
const [anchorEl, setAnchorEl] = useState(null)
|
||||
const { permissions } = usePermissions()
|
||||
|
||||
const options = {
|
||||
playNow: {
|
||||
enabled: true,
|
||||
@@ -85,7 +108,27 @@ export const SongContextMenu = ({
|
||||
info: {
|
||||
enabled: true,
|
||||
label: translate('resources.song.actions.info'),
|
||||
action: (record) => dispatch(openExtendedInfoDialog(record)),
|
||||
action: async (record) => {
|
||||
let fullRecord = record
|
||||
if (permissions === 'admin' && !record.missing) {
|
||||
try {
|
||||
let id = record.mediaFileId ?? record.id
|
||||
const data = await httpClient(`/api/inspect?id=${id}`)
|
||||
fullRecord = { ...record, rawTags: data.json.rawTags }
|
||||
} catch (error) {
|
||||
notify(
|
||||
translate('ra.notification.http_error') + ': ' + error.message,
|
||||
{
|
||||
type: 'warning',
|
||||
multiLine: true,
|
||||
duration: 0,
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
dispatch(openExtendedInfoDialog(fullRecord))
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -109,16 +152,20 @@ export const SongContextMenu = ({
|
||||
|
||||
const open = Boolean(anchorEl)
|
||||
|
||||
if (!record) {
|
||||
return null
|
||||
}
|
||||
|
||||
const present = !record.missing
|
||||
|
||||
return (
|
||||
<span className={clsx(classes.noWrap, className)}>
|
||||
<LoveButton
|
||||
record={record}
|
||||
resource={resource}
|
||||
visible={config.enableFavourites && showLove}
|
||||
visible={config.enableFavourites && showLove && present}
|
||||
/>
|
||||
<IconButton onClick={handleClick} size={'small'}>
|
||||
<MoreVertIcon fontSize={'small'} />
|
||||
</IconButton>
|
||||
<MoreButton record={record} onClick={handleClick} info={options.info} />
|
||||
<Menu
|
||||
id={'menu' + record.id}
|
||||
anchorEl={anchorEl}
|
||||
|
||||
@@ -41,6 +41,10 @@ const useStyles = makeStyles({
|
||||
},
|
||||
},
|
||||
},
|
||||
missingRow: {
|
||||
cursor: 'inherit',
|
||||
opacity: 0.3,
|
||||
},
|
||||
headerStyle: {
|
||||
'& thead': {
|
||||
boxShadow: '0px 3px 3px rgba(0, 0, 0, 0.15)',
|
||||
@@ -195,6 +199,13 @@ export const SongDatagridRow = ({
|
||||
return null
|
||||
}
|
||||
|
||||
const rowClick = record.missing ? undefined : rest.rowClick
|
||||
|
||||
const computedClasses = clsx(
|
||||
className,
|
||||
classes.row,
|
||||
record.missing && classes.missingRow,
|
||||
)
|
||||
const childCount = fields.length
|
||||
return (
|
||||
<>
|
||||
@@ -220,7 +231,8 @@ export const SongDatagridRow = ({
|
||||
ref={dragSongRef}
|
||||
record={record}
|
||||
{...rest}
|
||||
className={clsx(className, classes.row)}
|
||||
rowClick={rowClick}
|
||||
className={computedClasses}
|
||||
>
|
||||
{fields}
|
||||
</PureDatagridRow>
|
||||
@@ -262,7 +274,12 @@ const SongDatagridBody = ({
|
||||
} else {
|
||||
idsToPlay = ids.filter((id) => data[id].releaseDate === releaseDate)
|
||||
}
|
||||
dispatch(playTracks(data, idsToPlay))
|
||||
dispatch(
|
||||
playTracks(
|
||||
data,
|
||||
idsToPlay?.filter((id) => !data[id].missing),
|
||||
),
|
||||
)
|
||||
},
|
||||
[dispatch, data, ids],
|
||||
)
|
||||
@@ -343,6 +360,7 @@ export const SongDatagrid = ({
|
||||
return (
|
||||
<Datagrid
|
||||
className={classes.headerStyle}
|
||||
isRowSelectable={(r) => !r?.missing}
|
||||
{...rest}
|
||||
body={
|
||||
<SongDatagridBody
|
||||
|
||||
+119
-17
@@ -1,4 +1,4 @@
|
||||
import React from 'react'
|
||||
import React, { useState } from 'react'
|
||||
import Table from '@material-ui/core/Table'
|
||||
import TableBody from '@material-ui/core/TableBody'
|
||||
import TableCell from '@material-ui/core/TableCell'
|
||||
@@ -14,10 +14,18 @@ import {
|
||||
useRecordContext,
|
||||
} from 'react-admin'
|
||||
import inflection from 'inflection'
|
||||
import { BitrateField, SizeField } from './index'
|
||||
import {
|
||||
ArtistLinkField,
|
||||
BitrateField,
|
||||
ParticipantsInfo,
|
||||
PathField,
|
||||
SizeField,
|
||||
} from './index'
|
||||
import { MultiLineTextField } from './MultiLineTextField'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import config from '../config'
|
||||
import { AlbumLinkField } from '../song/AlbumLinkField'
|
||||
import { Tab, Tabs } from '@material-ui/core'
|
||||
|
||||
const useStyles = makeStyles({
|
||||
gain: {
|
||||
@@ -34,13 +42,32 @@ export const SongInfo = (props) => {
|
||||
const classes = useStyles({ gain: config.enableReplayGain })
|
||||
const translate = useTranslate()
|
||||
const record = useRecordContext(props)
|
||||
const [tab, setTab] = useState(0)
|
||||
|
||||
// These are already displayed in other fields or are album-level tags
|
||||
const excludedTags = [
|
||||
'genre',
|
||||
'disctotal',
|
||||
'tracktotal',
|
||||
'releasetype',
|
||||
'recordlabel',
|
||||
'media',
|
||||
'albumversion',
|
||||
]
|
||||
const data = {
|
||||
path: <TextField source="path" />,
|
||||
album: <TextField source="album" />,
|
||||
path: <PathField />,
|
||||
album: (
|
||||
<AlbumLinkField source="album" sortByOrder={'ASC'} record={record} />
|
||||
),
|
||||
discSubtitle: <TextField source="discSubtitle" />,
|
||||
albumArtist: <TextField source="albumArtist" />,
|
||||
albumArtist: (
|
||||
<ArtistLinkField source="albumArtist" record={record} limit={Infinity} />
|
||||
),
|
||||
artist: (
|
||||
<ArtistLinkField source="artist" record={record} limit={Infinity} />
|
||||
),
|
||||
genre: (
|
||||
<FunctionField render={(r) => r.genres?.map((g) => g.name).join(', ')} />
|
||||
<FunctionField render={(r) => r.genres?.map((g) => g.name).join(' • ')} />
|
||||
),
|
||||
compilation: <BooleanField source="compilation" />,
|
||||
bitRate: <BitrateField source="bitRate" />,
|
||||
@@ -52,6 +79,15 @@ export const SongInfo = (props) => {
|
||||
comment: <MultiLineTextField source="comment" />,
|
||||
}
|
||||
|
||||
const roles = []
|
||||
|
||||
for (const name of Object.keys(record.participants)) {
|
||||
if (name === 'albumartist' || name === 'artist') {
|
||||
continue
|
||||
}
|
||||
roles.push([name, record.participants[name].length])
|
||||
}
|
||||
|
||||
const optionalFields = ['discSubtitle', 'comment', 'bpm', 'genre']
|
||||
optionalFields.forEach((field) => {
|
||||
!record[field] && delete data[field]
|
||||
@@ -69,23 +105,89 @@ export const SongInfo = (props) => {
|
||||
)
|
||||
}
|
||||
|
||||
const tags = Object.entries(record.tags ?? {}).filter(
|
||||
(tag) => !excludedTags.includes(tag[0]),
|
||||
)
|
||||
|
||||
return (
|
||||
<TableContainer>
|
||||
<Table aria-label="song details" size="small">
|
||||
<TableBody>
|
||||
{Object.keys(data).map((key) => {
|
||||
return (
|
||||
<TableRow key={`${record.id}-${key}`}>
|
||||
<TableCell scope="row" className={classes.tableCell}>
|
||||
{translate(`resources.song.fields.${key}`, {
|
||||
_: inflection.humanize(inflection.underscore(key)),
|
||||
})}
|
||||
:
|
||||
{record.rawTags && (
|
||||
<Tabs value={tab} onChange={(_, value) => setTab(value)}>
|
||||
<Tab
|
||||
label={translate(`resources.song.fields.mappedTags`)}
|
||||
id="mapped-tags-tab"
|
||||
aria-controls="mapped-tags-body"
|
||||
/>
|
||||
<Tab
|
||||
label={translate(`resources.song.fields.rawTags`)}
|
||||
id="raw-tags-tab"
|
||||
aria-controls="raw-tags-body"
|
||||
/>
|
||||
</Tabs>
|
||||
)}
|
||||
<div
|
||||
hidden={tab === 1}
|
||||
id="mapped-tags-body"
|
||||
aria-labelledby={record.rawTags ? 'mapped-tags-tab' : undefined}
|
||||
>
|
||||
{Object.keys(data).map((key) => {
|
||||
return (
|
||||
<TableRow key={`${record.id}-${key}`}>
|
||||
<TableCell scope="row" className={classes.tableCell}>
|
||||
{translate(`resources.song.fields.${key}`, {
|
||||
_: inflection.humanize(inflection.underscore(key)),
|
||||
})}
|
||||
:
|
||||
</TableCell>
|
||||
<TableCell align="left">{data[key]}</TableCell>
|
||||
</TableRow>
|
||||
)
|
||||
})}
|
||||
<ParticipantsInfo classes={classes} record={record} />
|
||||
{tags.length > 0 && (
|
||||
<TableRow key={`${record.id}-separator`}>
|
||||
<TableCell
|
||||
scope="row"
|
||||
className={classes.tableCell}
|
||||
></TableCell>
|
||||
<TableCell align="left">
|
||||
<h4>{translate(`resources.song.fields.tags`)}</h4>
|
||||
</TableCell>
|
||||
<TableCell align="left">{data[key]}</TableCell>
|
||||
</TableRow>
|
||||
)
|
||||
})}
|
||||
)}
|
||||
{tags.map(([name, values]) => (
|
||||
<TableRow key={`${record.id}-tag-${name}`}>
|
||||
<TableCell scope="row" className={classes.tableCell}>
|
||||
{name}:
|
||||
</TableCell>
|
||||
<TableCell align="left">{values.join(' • ')}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</div>
|
||||
{record.rawTags && (
|
||||
<div
|
||||
hidden={tab === 0}
|
||||
id="raw-tags-body"
|
||||
aria-labelledby="raw-tags-tab"
|
||||
>
|
||||
<TableRow key={`${record.id}-raw-path`}>
|
||||
<TableCell scope="row" className={classes.tableCell}>
|
||||
{translate(`resources.song.fields.path`)}:
|
||||
</TableCell>
|
||||
<TableCell align="left">{data.path}</TableCell>
|
||||
</TableRow>
|
||||
{Object.entries(record.rawTags).map(([key, value]) => (
|
||||
<TableRow key={`${record.id}-raw-${key}`}>
|
||||
<TableCell scope="row" className={classes.tableCell}>
|
||||
{key}:
|
||||
</TableCell>
|
||||
<TableCell align="left">{value.join(' • ')}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
|
||||
@@ -21,6 +21,9 @@ const useStyles = makeStyles({
|
||||
text: {
|
||||
verticalAlign: 'text-top',
|
||||
},
|
||||
subtitle: {
|
||||
opacity: 0.5,
|
||||
},
|
||||
})
|
||||
|
||||
export const SongTitleField = ({ showTrackNumbers, ...props }) => {
|
||||
@@ -33,11 +36,21 @@ export const SongTitleField = ({ showTrackNumbers, ...props }) => {
|
||||
const isCurrent =
|
||||
currentId && (currentId === record.id || currentId === record.mediaFileId)
|
||||
|
||||
const subtitle = record?.tags?.['subtitle']
|
||||
|
||||
const trackName = (r) => {
|
||||
const name = r.title
|
||||
if (r.trackNumber && showTrackNumbers) {
|
||||
return r.trackNumber.toString().padStart(2, '0') + ' ' + name
|
||||
}
|
||||
if (subtitle) {
|
||||
return (
|
||||
<>
|
||||
{name}
|
||||
<span className={classes.subtitle}>{' (' + subtitle + ')'}</span>
|
||||
</>
|
||||
)
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
|
||||
@@ -32,7 +32,6 @@ export * from './useToggleLove'
|
||||
export * from './useTraceUpdate'
|
||||
export * from './Writable'
|
||||
export * from './SongSimpleList'
|
||||
export * from './ArtistSimpleList'
|
||||
export * from './RatingField'
|
||||
export * from './useRating'
|
||||
export * from './useSelectedFields'
|
||||
@@ -40,3 +39,5 @@ export * from './ToggleFieldsMenu'
|
||||
export * from './QualityInfo'
|
||||
export * from './formatRange.js'
|
||||
export * from './playlistUtils.js'
|
||||
export * from './PathField.jsx'
|
||||
export * from './ParticipantsInfo'
|
||||
|
||||
Reference in New Issue
Block a user