From 59234491a7f5d3f51b0847fc1036ebf2e63d63c1 Mon Sep 17 00:00:00 2001 From: Lucki Date: Thu, 22 Aug 2024 16:06:12 +0200 Subject: [PATCH 01/22] Replace `Track.artist` and `Track.extraArtists` with `Track.artists` --- packages/app/app/actions/downloads.ts | 5 +- packages/app/app/actions/lyrics.ts | 2 +- packages/app/app/actions/queue.test.ts | 2 +- packages/app/app/actions/queue.ts | 10 +-- packages/app/app/actions/search.ts | 5 +- .../ArtistView/PopularTracks/index.tsx | 2 +- .../components/Dashboard/ChartsTab/index.tsx | 2 +- .../Downloads/DownloadsItem/index.tsx | 4 +- .../LibraryView/LibraryFolderTree/index.js | 2 +- .../app/app/components/LyricsView/index.tsx | 4 +- .../QueueMenu/QueueMenuMore/index.tsx | 12 +--- .../app/app/components/PlayQueue/index.tsx | 4 +- .../SearchResults/PlaylistResults/index.js | 2 +- .../SearchResults/TracksResults/index.js | 5 +- .../components/TagView/TagTopTracks/index.tsx | 8 +-- packages/app/app/components/TrackRow/index.js | 10 +-- .../AlbumViewContainer.test.tsx | 34 +++++----- .../containers/AlbumViewContainer/hooks.ts | 7 +- .../ArtistViewContainer.test.tsx | 22 +++---- .../DashboardContainer.test.tsx | 6 +- .../FavoritesContainer.tracks.test.tsx | 28 ++++---- .../LibraryViewContainer.test.tsx | 6 +- .../ListeningHistoryContainer/index.tsx | 2 +- .../LyricsContainer/LyricsContainer.test.tsx | 2 +- .../PlayQueueContainer.test.tsx | 22 +++---- .../PlayerBarContainer.test.tsx | 16 ++--- .../containers/PlayerBarContainer/hooks.ts | 12 ++-- .../PlaylistViewContainer.test.tsx | 28 ++++---- .../app/containers/QueuePopupButtons/index.js | 6 +- .../containers/SoundContainer/autoradio.js | 11 ++-- .../app/containers/SoundContainer/index.js | 8 +-- .../SpotifyPlaylistAdapter.test.tsx | 4 +- .../StreamVerificationContainer/index.tsx | 14 ++-- .../app/containers/TrackPopupButtons/index.js | 4 +- .../TrackPopupContainer.test.tsx | 10 ++- .../containers/TrackPopupContainer/hooks.ts | 10 +-- .../containers/TrackTableContainer/index.tsx | 3 - packages/app/app/reducers/dashboard.ts | 2 +- packages/app/app/reducers/queue.ts | 2 +- packages/app/app/selectors/favorites.ts | 12 ++-- packages/app/app/utils.ts | 6 +- packages/app/test/storeBuilders.ts | 66 +++++++++---------- packages/core/src/helpers/playlist/index.ts | 2 +- packages/core/src/helpers/playlist/types.ts | 2 +- packages/core/src/plugins/meta/audius.ts | 2 +- packages/core/src/plugins/meta/bandcamp.ts | 6 +- packages/core/src/plugins/meta/discogs.ts | 2 +- .../core/src/plugins/meta/itunesmusic.test.ts | 2 +- packages/core/src/plugins/meta/itunesmusic.ts | 4 +- .../src/plugins/meta/itunespodcast.test.ts | 2 +- .../core/src/plugins/meta/itunespodcast.ts | 2 +- packages/core/src/plugins/meta/musicbrainz.ts | 4 +- .../core/src/plugins/meta/spotify.test.ts | 8 +-- packages/core/src/plugins/meta/spotify.ts | 2 +- packages/core/src/plugins/plugins.types.ts | 6 +- packages/core/src/rest/Deezer.ts | 2 +- packages/core/src/rest/Spotify.ts | 2 +- packages/core/src/structs/Track.ts | 11 ++-- packages/core/src/types/index.ts | 4 +- packages/main/src/controllers/download.ts | 4 +- .../main/src/services/@linux/system-api.ts | 3 +- packages/main/src/utils/tracks.ts | 4 +- .../MiniPlayer/MiniTrackInfo/index.tsx | 4 +- .../ui/lib/components/MiniPlayer/index.tsx | 4 +- .../ui/lib/components/PlayerBar/index.tsx | 4 +- .../ui/lib/components/QueueItem/index.tsx | 4 +- .../ui/lib/components/TrackInfo/index.tsx | 6 +- packages/ui/lib/components/TrackRow/index.tsx | 4 +- .../ui/lib/components/TrackTable/index.tsx | 4 +- packages/ui/lib/types/index.ts | 4 +- packages/ui/lib/utils.ts | 8 +-- .../stories/components/albumGrid.stories.tsx | 2 +- .../components/albumPreview.stories.tsx | 6 +- .../stories/components/miniPlayer.stories.tsx | 2 +- .../stories/components/playerBar.stories.tsx | 2 +- .../stories/components/playlists.stories.tsx | 6 +- .../stories/components/queueItem.stories.tsx | 10 +-- .../stories/components/trackPopup.stories.tsx | 2 +- .../ui/stories/components/trackRow.stories.js | 4 +- .../stories/components/trackTable.stories.tsx | 19 +++--- packages/ui/test/miniPlayer.test.tsx | 2 +- packages/ui/test/playerBar.test.tsx | 2 +- packages/ui/test/trackTable.test.tsx | 9 +-- 83 files changed, 277 insertions(+), 327 deletions(-) diff --git a/packages/app/app/actions/downloads.ts b/packages/app/app/actions/downloads.ts index f3d36f1d7a..bbedabd0d8 100644 --- a/packages/app/app/actions/downloads.ts +++ b/packages/app/app/actions/downloads.ts @@ -1,4 +1,5 @@ import _ from 'lodash'; +import { isEqual } from 'lodash'; import { store, StreamProvider } from '@nuclear/core'; import { getTrackItem } from '@nuclear/ui'; import { safeAddUuid } from './helpers'; @@ -35,8 +36,8 @@ export const addToDownloads = createStandardAction(DownloadActionTypes.ADD_TO_DO let downloads: Download[] = store.get('downloads'); const existingTrack = downloads.find(({track}) => { - const {name, artist} = track; - return artist === clonedTrack.artist && name === clonedTrack.name; + const {name, artists} = track; + return isEqual(artists, clonedTrack.artists) && name === clonedTrack.name; }); if (!existingTrack ){ diff --git a/packages/app/app/actions/lyrics.ts b/packages/app/app/actions/lyrics.ts index 2f1e2391fb..6f8923a355 100644 --- a/packages/app/app/actions/lyrics.ts +++ b/packages/app/app/actions/lyrics.ts @@ -23,7 +23,7 @@ export function lyricsSearch(track: Track) { const selectedProvider = getState().plugin.selected.lyricsProviders; const lyricsProvider = _.find(providers, {sourceName: selectedProvider}); - lyricsProvider.search(track.artist, track.name) + lyricsProvider.search(track.artists?.[0], track.name) .then(result => { dispatch(lyricsSearchSuccess(result)); }) diff --git a/packages/app/app/actions/queue.test.ts b/packages/app/app/actions/queue.test.ts index 58a58602f6..fdf23919d5 100644 --- a/packages/app/app/actions/queue.test.ts +++ b/packages/app/app/actions/queue.test.ts @@ -24,7 +24,7 @@ describe('Queue actions tests', () => { const trackIndex = 123; const queueItems: QueueItem[] = []; queueItems[trackIndex] = { - artist: 'Artist Name', + artists: ['Artist Name'], name: 'Track Name', local: false, streams: null diff --git a/packages/app/app/actions/queue.ts b/packages/app/app/actions/queue.ts index 3a05083963..7a5b99003e 100644 --- a/packages/app/app/actions/queue.ts +++ b/packages/app/app/actions/queue.ts @@ -4,7 +4,7 @@ import { createStandardAction } from 'typesafe-actions'; import { v4 } from 'uuid'; import { rest, StreamProvider } from '@nuclear/core'; -import { getTrackArtist } from '@nuclear/ui'; +import { getTrackArtists } from '@nuclear/ui'; import { Track } from '@nuclear/ui/lib/types'; import { safeAddUuid } from './helpers'; @@ -46,7 +46,7 @@ const localTrackToQueueItem = (track: LocalTrack, local: LocalLibraryState): Que export const toQueueItem = (track: Track): QueueItem => ({ ...track, - artist: isString(track.artist) ? track.artist : track.artist.name, + artists: track.artists, name: track.title ? track.title : track.name, streams: track.streams ?? [] }); @@ -72,7 +72,7 @@ export const resolveTrackStreams = async ( return track.streams.filter((stream) => stream.source === 'Local'); } else { return selectedStreamProvider.search({ - artist: getTrackArtist(track), + artist: getTrackArtists(track)?.[0], track: track.name }); } @@ -186,7 +186,7 @@ export const findStreamsForTrack = (index: number) => async (dispatch, getState) try { const StreamMappingsService = new rest.NuclearStreamMappingsService(process.env.NUCLEAR_VERIFICATION_SERVICE_URL); const topStream = await StreamMappingsService.getTopStream( - track.artist, + track.artists?.[0], track.name, selectedStreamProvider.sourceName, settings?.userId @@ -226,7 +226,7 @@ export const findStreamsForTrack = (index: number) => async (dispatch, getState) } } catch (e) { logger.error( - `An error has occurred when searching for streams with ${selectedStreamProvider.sourceName} for "${track.artist} - ${track.name}."` + `An error has occurred when searching for streams with ${selectedStreamProvider.sourceName} for "${track.artists?.[0]} - ${track.name}."` ); logger.error(e); dispatch( diff --git a/packages/app/app/actions/search.ts b/packages/app/app/actions/search.ts index 9ff426481b..5c2cdcfbcd 100644 --- a/packages/app/app/actions/search.ts +++ b/packages/app/app/actions/search.ts @@ -7,7 +7,7 @@ import { error } from './toasts'; import { Search } from './actionTypes'; import { History } from 'history'; import { RootState } from '../reducers'; -import { AlbumDetails, ArtistDetails, SearchResultsAlbum, SearchResultsArtist, SearchResultsPodcast, SearchResultsSource } from '@nuclear/core/src/plugins/plugins.types'; +import { AlbumDetails, ArtistDetails, SearchResultsAlbum, SearchResultsArtist, SearchResultsPodcast, SearchResultsSource, SearchResultsTrack } from '@nuclear/core/src/plugins/plugins.types'; import { createStandardAction } from 'typesafe-actions'; import { LastfmTrackMatch, LastfmTrackMatchInternal } from '@nuclear/core/src/rest/Lastfm.types'; import { YoutubeResult } from '@nuclear/core/src/rest/Youtube'; @@ -80,7 +80,7 @@ export const SearchActions = { } }; }), - trackSearchSuccess: createStandardAction(Search.TRACK_SEARCH_SUCCESS)(), + trackSearchSuccess: createStandardAction(Search.TRACK_SEARCH_SUCCESS)(), artistSearchSuccess: createStandardAction(Search.ARTIST_SEARCH_SUCCESS)(), artistInfoStart: createStandardAction(Search.ARTIST_INFO_SEARCH_START)(), artistInfoSuccess: createStandardAction(Search.ARTIST_INFO_SEARCH_SUCCESS).map((artistId: string, info: ArtistDetails) => { @@ -181,6 +181,7 @@ const getTrackThumbnail = (track: LastfmTrackMatch) => { export const mapLastFMTrackToInternal = (track: LastfmTrackMatch) => ({ ...track, + artists: [track.artist], thumbnail: getTrackThumbnail(track) }); diff --git a/packages/app/app/components/ArtistView/PopularTracks/index.tsx b/packages/app/app/components/ArtistView/PopularTracks/index.tsx index 044c490940..0edfe31b0c 100644 --- a/packages/app/app/components/ArtistView/PopularTracks/index.tsx +++ b/packages/app/app/components/ArtistView/PopularTracks/index.tsx @@ -61,7 +61,7 @@ const PopularTracks: React.FC = ({ .slice(0, tracks.length > 15 ? 15 : tracks.length) .forEach(track => { addToQueue({ - artist: artist.name, + artists: track.artists, name: track.title, thumbnail: track.thumb }); diff --git a/packages/app/app/components/Dashboard/ChartsTab/index.tsx b/packages/app/app/components/Dashboard/ChartsTab/index.tsx index d6f753393a..bb611cac5a 100644 --- a/packages/app/app/components/Dashboard/ChartsTab/index.tsx +++ b/packages/app/app/components/Dashboard/ChartsTab/index.tsx @@ -12,7 +12,7 @@ type ChartsTabProps = { } const mapDeezerTopTrackToTrack = (topTrack: InternalTopTrack): Track => ({ - artist: topTrack.artist, + artists: topTrack.artists, title: topTrack.title, album: topTrack.album?.title, duration: topTrack.duration, diff --git a/packages/app/app/components/Downloads/DownloadsItem/index.tsx b/packages/app/app/components/Downloads/DownloadsItem/index.tsx index 39625dffe9..ec15f68c20 100644 --- a/packages/app/app/components/Downloads/DownloadsItem/index.tsx +++ b/packages/app/app/components/Downloads/DownloadsItem/index.tsx @@ -59,9 +59,7 @@ const DownloadsItem: React.FC = ({ pauseDownload, removeDownload }) => { - const artistName = _.isString(_.get(item, 'track.artist')) - ? _.get(item, 'track.artist') - : _.get(item, 'track.artist.name'); + const artistName = item.track.artists?.[0]; const onResumeClick = useCallback(() => resumeDownload(item.track.uuid), [item, resumeDownload]); const onPauseClick = useCallback(() => pauseDownload(item.track.uuid), [item, pauseDownload]); const onRemoveClick = useCallback(() => removeDownload(item.track.uuid), [item, removeDownload]); diff --git a/packages/app/app/components/LibraryView/LibraryFolderTree/index.js b/packages/app/app/components/LibraryView/LibraryFolderTree/index.js index cae2dca020..d4a3384d83 100644 --- a/packages/app/app/components/LibraryView/LibraryFolderTree/index.js +++ b/packages/app/app/components/LibraryView/LibraryFolderTree/index.js @@ -51,7 +51,7 @@ const useTreeData = (tracks, localFolders) => { path: track.path, name: track.name, album: track.album, - artist: _.isString(track.artist) ? track.artist : track.artist.name + artists: track.artists }; pathToEntryMap[track.path] = newEntry; folderEntry.children.push(newEntry); diff --git a/packages/app/app/components/LyricsView/index.tsx b/packages/app/app/components/LyricsView/index.tsx index 87d248faf7..a42bea48bd 100644 --- a/packages/app/app/components/LyricsView/index.tsx +++ b/packages/app/app/components/LyricsView/index.tsx @@ -6,7 +6,7 @@ import LyricsHeader from './LyricsHeader'; import styles from './styles.scss'; import { QueueItem } from '../../reducers/queue'; -import { getTrackArtist } from '@nuclear/ui'; +import { getTrackArtists } from '@nuclear/ui'; type LyricsViewProps = { lyricsSearchResult: string @@ -40,7 +40,7 @@ export const LyricsView: React.FC = ({ <>
{lyricsSearchResult || t('not-found')} diff --git a/packages/app/app/components/PlayQueue/QueueMenu/QueueMenuMore/index.tsx b/packages/app/app/components/PlayQueue/QueueMenu/QueueMenuMore/index.tsx index db05722566..2666adb50e 100644 --- a/packages/app/app/components/PlayQueue/QueueMenu/QueueMenuMore/index.tsx +++ b/packages/app/app/components/PlayQueue/QueueMenu/QueueMenuMore/index.tsx @@ -2,8 +2,8 @@ import React, { useCallback } from 'react'; import cx from 'classnames'; import { Dropdown, Icon } from 'semantic-ui-react'; import { useTranslation } from 'react-i18next'; -import { isArtistObject, Playlist, PlaylistHelper } from '@nuclear/core'; -import { getTrackArtist } from '@nuclear/ui'; +import { Playlist, PlaylistHelper } from '@nuclear/core'; +import { getTrackArtists } from '@nuclear/ui'; import { Track } from '@nuclear/ui/lib/types'; import styles from './styles.scss'; @@ -15,10 +15,6 @@ export const addTrackToPlaylist = ( track: Track ) => { if (track && track.name) { - if (isArtistObject(track.artist)) { - track.artist = getTrackArtist(track); - } - updatePlaylist({ ...playlist, tracks: [...playlist.tracks, PlaylistHelper.extractTrackData(track)] @@ -32,10 +28,6 @@ export const addQueueToPlaylist = ( tracks: Track[] ) => { const newTracks = tracks.map((track) => { - if (isArtistObject(track.artist)) { - track.artist = getTrackArtist(track); - } - return PlaylistHelper.extractTrackData(track); }); diff --git a/packages/app/app/components/PlayQueue/index.tsx b/packages/app/app/components/PlayQueue/index.tsx index 3ba385792b..ca8488fc81 100644 --- a/packages/app/app/components/PlayQueue/index.tsx +++ b/packages/app/app/components/PlayQueue/index.tsx @@ -113,9 +113,7 @@ const PlayQueue: React.FC = ({ const onAddToDownloads = (track: QueueItemType) => { const clonedTrack = safeAddUuid(track); - const artistName = _.isString(track?.artist) - ? track?.artist - : track?.artist?.name; + const artistName = track.artists?.[0]; addToDownloads(plugins.plugins.streamProviders, clonedTrack); info( t('download-toast-title'), diff --git a/packages/app/app/components/SearchResults/PlaylistResults/index.js b/packages/app/app/components/SearchResults/PlaylistResults/index.js index 8b6a16e66f..e5c98747b0 100644 --- a/packages/app/app/components/SearchResults/PlaylistResults/index.js +++ b/packages/app/app/components/SearchResults/PlaylistResults/index.js @@ -17,7 +17,7 @@ class PlaylistResults extends React.Component { addTrack(track) { if (typeof track !== 'undefined') { this.props.addToQueue({ - artist: track.artist, + artists: track.artists, name: track.name, thumbnail: track.thumbnail ?? _.get(track, 'image[1][\'#text\']', artPlaceholder) }); diff --git a/packages/app/app/components/SearchResults/TracksResults/index.js b/packages/app/app/components/SearchResults/TracksResults/index.js index 28d8cf1727..869d7405ee 100644 --- a/packages/app/app/components/SearchResults/TracksResults/index.js +++ b/packages/app/app/components/SearchResults/TracksResults/index.js @@ -22,12 +22,9 @@ const TracksResults = ({ tracks, limit }) => { if ( track && _.hasIn(track, 'name') && (_.hasIn(track, 'image') || _.hasIn(track, 'thumbnail')) && - _.hasIn(track, 'artist') + _.hasIn(track, 'artists') ) { const newTrack = _.cloneDeep(track); - if (!newTrack.artist.name) { - _.set(newTrack, 'artist.name', newTrack.artist); - } return ( void; + addToQueue: (track: { artists: string[]; name: string; thumbnail: string }) => void; } const TagTopTracks: React.FC = ({ tracks, addToQueue }) => { @@ -33,7 +31,7 @@ const TagTopTracks: React.FC = ({ tracks, addToQueue }) => { onClick={() => { tracks.map((track) => { addToQueue({ - artist: track.artist.name, + artists: track.artists, name: track.name, thumbnail: track.image[1]['#text'] }); diff --git a/packages/app/app/components/TrackRow/index.js b/packages/app/app/components/TrackRow/index.js index da236623fa..443894833d 100644 --- a/packages/app/app/components/TrackRow/index.js +++ b/packages/app/app/components/TrackRow/index.js @@ -7,7 +7,7 @@ import numeral from 'numeral'; import { Icon } from 'semantic-ui-react'; import { head } from 'lodash'; -import { formatDuration, getTrackArtist, getTrackTitle } from '@nuclear/ui'; +import { formatDuration, getTrackArtists, getTrackTitle } from '@nuclear/ui'; import * as QueueActions from '../../actions/queue'; @@ -39,7 +39,7 @@ class TrackRow extends React.Component { playTrack() { this.props.actions.playTrack(this.props.streamProviders, { - artist: this.props.track.artist.name, + artists: this.props.track.artists?.[0], name: this.props.track.name, thumbnail: this.getTrackThumbnail(), local: this.props.track.local, @@ -67,7 +67,7 @@ class TrackRow extends React.Component { canAddToFavorites() { return _.findIndex(this.props.favoriteTracks, (currentTrack) => { - return currentTrack.name === this.props.track.name && currentTrack.artist.name === this.props.track.artist.name; + return currentTrack.name === this.props.track.name && _.isEqual(currentTrack.artists, this.props.track.artists); }) < 0; } @@ -99,7 +99,7 @@ class TrackRow extends React.Component { } {displayTrackNumber && {track.position}} - {displayArtist && {track.artist.name}} + {displayArtist && {track.artists?.[0]}} {track.name} {displayAlbum && this.renderAlbum(track)} {displayDuration && this.renderDuration(track)} @@ -123,7 +123,7 @@ class TrackRow extends React.Component { { const state = store.getState(); expect(state.queue.queueItems).toEqual([ expect.objectContaining({ - artist: 'test artist', + artists: ['test artist'], name: 'test track 1' }) ]); @@ -62,7 +62,7 @@ describe('Album view container', () => { const state = store.getState(); expect(state.queue.queueItems).toEqual([ expect.objectContaining({ - artist: 'test artist', + artists: ['test artist'], name: 'test track 1' }) ]); @@ -80,11 +80,11 @@ describe('Album view container', () => { const state = store.getState(); expect(state.queue.queueItems).toEqual([ expect.objectContaining({ - artist: 'test artist', + artists: ['test artist'], name: 'test track 2' }), expect.objectContaining({ - artist: 'test artist', + artists: ['test artist'], name: 'test track 1' }) ]); @@ -103,7 +103,7 @@ describe('Album view container', () => { completion: 0, status: 'Waiting', track: expect.objectContaining({ - artist: 'test artist', + artists: ['test artist'], name: 'test track 1' }) } @@ -151,15 +151,15 @@ describe('Album view container', () => { const state = store.getState(); expect(state?.queue?.queueItems).toEqual([ expect.objectContaining({ - artist: 'test artist', + artists: ['test artist'], name: 'test track 1' }), expect.objectContaining({ - artist: 'test artist', + artists: ['test artist'], name: 'test track 2' }), expect.objectContaining({ - artist: 'test artist', + artists: ['test artist'], name: 'test track 3' }) ]); @@ -174,15 +174,15 @@ describe('Album view container', () => { expect(state?.queue?.queueItems).toEqual([ expect.objectContaining({ - artist: 'test artist', + artists: ['test artist'], name: 'test track 1' }), expect.objectContaining({ - artist: 'test artist', + artists: ['test artist'], name: 'test track 2' }), expect.objectContaining({ - artist: 'test artist', + artists: ['test artist'], name: 'test track 3' }) ]); @@ -200,7 +200,7 @@ describe('Album view container', () => { const currentState = store.getState(); return expect(currentState?.queue?.queueItems).toEqual([ expect.objectContaining({ - artist: 'test artist', + artists: ['test artist'], name: 'test track 1', streams: [{ stream: 'test-stream-url', @@ -210,11 +210,11 @@ describe('Album view container', () => { }] }), expect.objectContaining({ - artist: 'test artist', + artists: ['test artist'], name: 'test track 2' }), expect.objectContaining({ - artist: 'test artist', + artists: ['test artist'], name: 'test track 3' }) ]); @@ -231,7 +231,7 @@ describe('Album view container', () => { completion: 0, status: 'Waiting', track: expect.objectContaining({ - artist: 'test artist', + artists: ['test artist'], name: 'test track 1', uuid: 'track-1-id' }) @@ -241,7 +241,7 @@ describe('Album view container', () => { status: 'Waiting', track: expect.objectContaining({ uuid: 'track-2-id', - artist: 'test artist', + artists: ['test artist'], name: 'test track 2' }) }, @@ -250,7 +250,7 @@ describe('Album view container', () => { status: 'Waiting', track: expect.objectContaining({ uuid: 'track-3-id', - artist: 'test artist', + artists: ['test artist'], name: 'test track 3' }) } diff --git a/packages/app/app/containers/AlbumViewContainer/hooks.ts b/packages/app/app/containers/AlbumViewContainer/hooks.ts index f7654adc42..a4efa7cf13 100644 --- a/packages/app/app/containers/AlbumViewContainer/hooks.ts +++ b/packages/app/app/containers/AlbumViewContainer/hooks.ts @@ -39,10 +39,7 @@ export const useAlbumViewProps = () => { thumbnail: album.coverImage, duration: parseInt(track.duration) !== track.duration ? stringDurationToSeconds(track.duration) - : track.duration, - artist: { - name: album.artist - } + : track.duration })); } @@ -70,7 +67,7 @@ export const useAlbumViewProps = () => { const addAlbumToQueue = useCallback(async () => { await album?.tracklist.forEach(async track => { dispatch(QueueActions.addToQueue({ - artist: album?.artist, + artists: track.artists, name: track.title, thumbnail: album.coverImage, streams: [] diff --git a/packages/app/app/containers/ArtistViewContainer/ArtistViewContainer.test.tsx b/packages/app/app/containers/ArtistViewContainer/ArtistViewContainer.test.tsx index 87e044b928..cbb30c57e7 100644 --- a/packages/app/app/containers/ArtistViewContainer/ArtistViewContainer.test.tsx +++ b/packages/app/app/containers/ArtistViewContainer/ArtistViewContainer.test.tsx @@ -80,7 +80,7 @@ describe('Artist view container', () => { const state = store.getState(); expect(state.queue.queueItems).toEqual([ expect.objectContaining({ - artist: 'test artist', + artists: ['test artist'], name: 'test artist top track 1' }) ]); @@ -94,9 +94,7 @@ describe('Artist view container', () => { .build(); initialState.search.artistDetails['test-artist-id'].topTracks = [{ - artist: { - name: 'test artist' - }, + artists: ['test artist'], title: 'test artist top track 1' }]; const { component, store } = mountComponent(initialState); @@ -107,7 +105,7 @@ describe('Artist view container', () => { const state = store.getState(); expect(state.queue.queueItems).toEqual([ expect.objectContaining({ - artist: 'test artist', + artists: ['test artist'], title: 'test artist top track 1' }) ]); @@ -124,7 +122,7 @@ describe('Artist view container', () => { const state = store.getState(); expect(state.queue.queueItems).toEqual([ expect.objectContaining({ - artist: 'test artist', + artists: ['test artist'], name: 'test artist top track 1' }) ]); @@ -144,7 +142,7 @@ describe('Artist view container', () => { completion: 0, status: 'Waiting', track: expect.objectContaining({ - artist: 'test artist', + artists: ['test artist'], name: 'test artist top track 1' }) } @@ -163,7 +161,7 @@ describe('Artist view container', () => { expect(state.playlists.localPlaylists.data[0].tracks).toEqual([ expect.objectContaining({ - artist: { name: 'test artist' }, + artists: ['test artist'], title: 'test artist top track 1' }) ]); @@ -181,7 +179,7 @@ describe('Artist view container', () => { state = store.getState(); expect(state.favorites.tracks).toEqual([ expect.objectContaining({ - artist: 'test artist', + artists: ['test artist'], name: 'test artist top track 1' }) ]); @@ -194,15 +192,15 @@ describe('Artist view container', () => { const state = store.getState(); expect(state?.queue?.queueItems).toEqual([ expect.objectContaining({ - artist: 'test artist', + artists: ['test artist'], name: 'test artist top track 1' }), expect.objectContaining({ - artist: 'test artist', + artists: ['test artist'], name: 'test artist top track 2' }), expect.objectContaining({ - artist: 'test artist', + artists: ['test artist'], name: 'test artist top track 3' }) ]); diff --git a/packages/app/app/containers/DashboardContainer/DashboardContainer.test.tsx b/packages/app/app/containers/DashboardContainer/DashboardContainer.test.tsx index 59766dd723..d17b3c7190 100644 --- a/packages/app/app/containers/DashboardContainer/DashboardContainer.test.tsx +++ b/packages/app/app/containers/DashboardContainer/DashboardContainer.test.tsx @@ -79,7 +79,7 @@ describe('Dashboard container', () => { const state = store.getState(); expect(state.queue.queueItems).toEqual([ expect.objectContaining({ - artist: 'top track artist 1', + artists: ['top track artist 1'], name: 'top track 1', thumbnail: 'top track album cover 1' }) @@ -97,12 +97,12 @@ describe('Dashboard container', () => { const state = store.getState(); expect(state.queue.queueItems).toEqual([ expect.objectContaining({ - artist: 'top track artist 1', + artists: ['top track artist 1'], name: 'top track 1', thumbnail: 'top track album cover 1' }), expect.objectContaining({ - artist: 'top track artist 2', + artists: ['top track artist 2'], name: 'top track 2', thumbnail: 'top track album cover 2' }) diff --git a/packages/app/app/containers/FavoritesContainer/FavoritesContainer.tracks.test.tsx b/packages/app/app/containers/FavoritesContainer/FavoritesContainer.tracks.test.tsx index 1cac43dd00..edcd7046ed 100644 --- a/packages/app/app/containers/FavoritesContainer/FavoritesContainer.tracks.test.tsx +++ b/packages/app/app/containers/FavoritesContainer/FavoritesContainer.tracks.test.tsx @@ -83,10 +83,10 @@ describe('Favorite tracks view container', () => { it('should be able to sort favorite tracks by title, ascending', async () => { withFavorites( [ - { name: 'DEF', artist: 'A' }, - { name: 'ABC', artist: 'A' }, - { name: 'GHI', artist: 'A' }, - { name: 'abc', artist: 'A' } + { name: 'DEF', artists: ['A'] }, + { name: 'ABC', artists: ['A'] }, + { name: 'GHI', artists: ['A'] }, + { name: 'abc', artists: ['A'] } ] ); const { component } = mountComponent(); @@ -102,10 +102,10 @@ describe('Favorite tracks view container', () => { it('should be able to sort favorite tracks by title, descending', async () => { withFavorites([ - { name: 'DEF', artist: 'A' }, - { name: 'ABC', artist: 'A' }, - { name: 'GHI', artist: 'A' }, - { name: 'abc', artist: 'A' } + { name: 'DEF', artists: ['A'] }, + { name: 'ABC', artists: ['A'] }, + { name: 'GHI', artists: ['A'] }, + { name: 'abc', artists: ['A'] } ]); const { component } = mountComponent(); @@ -123,10 +123,10 @@ describe('Favorite tracks view container', () => { it('should be able to sort favorite tracks by artist', async () => { withFavorites([ - { name: 'A', artist: 'DEF' }, - { name: 'B', artist: 'ABC' }, - { name: 'C', artist: 'GHI' }, - { name: 'D', artist: 'abc' } + { name: 'A', artists: ['DEF'] }, + { name: 'B', artists: ['ABC'] }, + { name: 'C', artists: ['GHI'] }, + { name: 'D', artists: ['abc'] } ]); const { component } = mountComponent(); @@ -189,7 +189,7 @@ describe('Favorite tracks view container', () => { it('should play a favorited local library track from a local stream', async () => { withFavorites([{ uuid: 'local-track-1', - artist: 'test artist 1', + artists: ['test artist 1'], name: 'test track 1', local: true }]); @@ -199,7 +199,7 @@ describe('Favorite tracks view container', () => { const state = store.getState(); expect(state.queue.queueItems[0]).toEqual(expect.objectContaining({ uuid: expect.stringMatching(uuidRegex), - artist: 'test artist 1', + artists: ['test artist 1'], name: 'test track 1', local: true, streams: [{ diff --git a/packages/app/app/containers/LibraryViewContainer/LibraryViewContainer.test.tsx b/packages/app/app/containers/LibraryViewContainer/LibraryViewContainer.test.tsx index 12f2f7e615..574a6f25bd 100644 --- a/packages/app/app/containers/LibraryViewContainer/LibraryViewContainer.test.tsx +++ b/packages/app/app/containers/LibraryViewContainer/LibraryViewContainer.test.tsx @@ -67,7 +67,7 @@ describe('Library view container', () => { expect(state.queue.queueItems).toStrictEqual([ expect.objectContaining({ uuid: expect.stringMatching(uuidRegex), - artist: 'local artist 1', + artists: ['local artist 1'], name: 'local track 1', duration: 300, local: true @@ -99,7 +99,7 @@ describe('Library view container', () => { .withConnectivity() .withLocal([{ uuid: 'local-track-1', - artist: 'local artist 1', + artists: ['local artist 1'], name: 'local track 1', duration: 250, path: '/path/to/local/track/1', @@ -122,7 +122,7 @@ describe('Library view container', () => { expect(state.queue.queueItems).toStrictEqual([ expect.objectContaining({ uuid: expect.stringMatching(uuidRegex), - artist: 'local artist 1', + artists: ['local artist 1'], name: 'local track 1', duration: 250, streams: [{ diff --git a/packages/app/app/containers/ListeningHistoryContainer/index.tsx b/packages/app/app/containers/ListeningHistoryContainer/index.tsx index 0f495fcfe3..00e9b7face 100644 --- a/packages/app/app/containers/ListeningHistoryContainer/index.tsx +++ b/packages/app/app/containers/ListeningHistoryContainer/index.tsx @@ -11,7 +11,7 @@ import { queue as queueSelector } from '../../selectors/queue'; // Importing them causes issues with the dev server type ListeningHistoryEntry = { uuid: string; - artist: string; + artists: string[]; title: string; createdAt: Date; } diff --git a/packages/app/app/containers/LyricsContainer/LyricsContainer.test.tsx b/packages/app/app/containers/LyricsContainer/LyricsContainer.test.tsx index 8d45237601..2ceed1d421 100644 --- a/packages/app/app/containers/LyricsContainer/LyricsContainer.test.tsx +++ b/packages/app/app/containers/LyricsContainer/LyricsContainer.test.tsx @@ -51,7 +51,7 @@ describe('Lyrics container', () => { queueItems: [ { name: 'test track', - artist: 'test artist' + artists: ['test artist'] } ] } diff --git a/packages/app/app/containers/PlayQueueContainer/PlayQueueContainer.test.tsx b/packages/app/app/containers/PlayQueueContainer/PlayQueueContainer.test.tsx index 2a1791aad1..b03596e863 100644 --- a/packages/app/app/containers/PlayQueueContainer/PlayQueueContainer.test.tsx +++ b/packages/app/app/containers/PlayQueueContainer/PlayQueueContainer.test.tsx @@ -111,7 +111,7 @@ describe('Play Queue container', () => { expect.arrayContaining([ expect.objectContaining({ uuid: expect.any(String), - artist: 'test artist 1', + artists: ['test artist 1'], name: 'test track 1', thumbnail: 'https://test-track-thumb-url' }) @@ -136,7 +136,7 @@ describe('Play Queue container', () => { expect.arrayContaining([ { uuid: expect.any(String), - artist: expect.stringMatching('test artist 1'), + artists: [expect.stringMatching('test artist 1')], name: expect.stringMatching('test track 1'), thumbnail: 'https://test-track-thumb-url' } @@ -159,7 +159,7 @@ describe('Play Queue container', () => { expect(state.playlists.localPlaylists.data[0].tracks).toEqual([ expect.objectContaining({ - artist: 'test artist 1', + artists: ['test artist 1'], name: 'test track 1' }) ]); @@ -182,15 +182,15 @@ describe('Play Queue container', () => { expect(state.playlists.localPlaylists.data[1].name).toBe('my new playlist'); expect(state.playlists.localPlaylists.data[1].tracks).toEqual([ expect.objectContaining({ - artist: 'test artist 1', + artists: ['test artist 1'], name: 'test track 1' }), expect.objectContaining({ - artist: 'test artist 2', + artists: ['test artist 2'], name: 'test track 2' }), expect.objectContaining({ - artist: 'test artist 3', + artists: ['test artist 3'], name: 'test track 3' }) ]); @@ -210,15 +210,15 @@ describe('Play Queue container', () => { expect(state.playlists.localPlaylists.data[0].name).toBe('test playlist'); expect(state.playlists.localPlaylists.data[0].tracks).toEqual([ expect.objectContaining({ - artist: 'test artist 1', + artists: ['test artist 1'], name: 'test track 1' }), expect.objectContaining({ - artist: 'test artist 2', + artists: ['test artist 2'], name: 'test track 2' }), expect.objectContaining({ - artist: 'test artist 3', + artists: ['test artist 3'], name: 'test track 3' }) ]); @@ -246,11 +246,11 @@ describe('Play Queue container', () => { const state = store.getState(); expect(state.queue.queueItems).toEqual([ expect.objectContaining({ - artist: 'test artist 2', + artists: ['test artist 2'], name: 'test track 2' }), expect.objectContaining({ - artist: 'test artist 3', + artists: ['test artist 3'], name: 'test track 3' }) ]); diff --git a/packages/app/app/containers/PlayerBarContainer/PlayerBarContainer.test.tsx b/packages/app/app/containers/PlayerBarContainer/PlayerBarContainer.test.tsx index 56b491de39..15a2908004 100644 --- a/packages/app/app/containers/PlayerBarContainer/PlayerBarContainer.test.tsx +++ b/packages/app/app/containers/PlayerBarContainer/PlayerBarContainer.test.tsx @@ -124,7 +124,7 @@ describe('PlayerBar container', () => { queue: { currentSong: 0, queueItems: [{ - artist: 'test artist 1', + artists: ['test artist 1'], name: 'test track 1', streams: [{ duration: 300, @@ -150,7 +150,7 @@ describe('PlayerBar container', () => { queue: { currentSong: 1, queueItems: [{ - artist: 'test artist 1', + artists: ['test artist 1'], name: 'test track 1', streams: [{ duration: 300, @@ -159,7 +159,7 @@ describe('PlayerBar container', () => { stream: 'stream URL 1' }] }, { - artist: 'test artist 2', + artists: ['test artist 2'], name: 'test track 2', streams: [{ duration: 300, @@ -185,7 +185,7 @@ describe('PlayerBar container', () => { queue: { currentSong: 1, queueItems: [{ - artist: 'test artist 1', + artists: ['test artist 1'], name: 'test track 1', streams: [{ duration: 300, @@ -194,7 +194,7 @@ describe('PlayerBar container', () => { stream: 'stream URL 1' }] }, { - artist: 'test artist 2', + artists: ['test artist 2'], name: 'test track 2', streams: [{ duration: 300, @@ -225,7 +225,7 @@ describe('PlayerBar container', () => { queue: { currentSong: 0, queueItems: [{ - artist: 'test artist 1', + artists: ['test artist 1'], name: 'test track 1', streams: [{ duration: 300, @@ -253,7 +253,7 @@ describe('PlayerBar container', () => { queueItems: [ { uuid: 'uuid1', - artist: 'test artist name', + artists: ['test artist name'], name: 'track without streams' } ] @@ -285,7 +285,7 @@ describe('PlayerBar container', () => { queueItems: [ { uuid: 'uuid1', - artist: 'test artist name', + artists: ['test artist name'], name: 'track without streams' } ] diff --git a/packages/app/app/containers/PlayerBarContainer/hooks.ts b/packages/app/app/containers/PlayerBarContainer/hooks.ts index c21168d50d..16beeb600b 100644 --- a/packages/app/app/containers/PlayerBarContainer/hooks.ts +++ b/packages/app/app/containers/PlayerBarContainer/hooks.ts @@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next'; import { useDispatch, useSelector } from 'react-redux'; import { useHistory } from 'react-router-dom'; import _, { isEmpty } from 'lodash'; -import { getTrackArtist } from '@nuclear/ui'; +import { getTrackArtists } from '@nuclear/ui'; import { SemanticICONS } from 'semantic-ui-react/dist/commonjs/generic'; import { normalizeTrack } from '../../utils'; @@ -114,10 +114,10 @@ export const useTrackInfoProps = () => { const currentSong = _.get(queue.queueItems, queue.currentSong); const track = currentSong?.name; - const artist = getTrackArtist(currentSong); + const artists = getTrackArtists(currentSong); const cover = currentSong?.thumbnail; - const favorite = useSelector(s => getFavoriteTrack(s, artist, track)); + const favorite = useSelector(s => getFavoriteTrack(s, artists, track)); const isFavorite = !_.isNil(favorite); const addToFavorites = useCallback( () => dispatch(favoritesActions.addFavoriteTrack(normalizeTrack(currentSong))), @@ -134,13 +134,13 @@ export const useTrackInfoProps = () => { ); const onArtistClick = useCallback( - () => dispatch(artistInfoSearchByName(artist, history)), - [dispatch, artist, history] + () => dispatch(artistInfoSearchByName(artists?.[0], history)), + [dispatch, artists, history] ); return { track, - artist, + artists, onTrackClick, onArtistClick, cover, diff --git a/packages/app/app/containers/PlaylistViewContainer/PlaylistViewContainer.test.tsx b/packages/app/app/containers/PlaylistViewContainer/PlaylistViewContainer.test.tsx index 2526a96fe2..a7be57cbfe 100644 --- a/packages/app/app/containers/PlaylistViewContainer/PlaylistViewContainer.test.tsx +++ b/packages/app/app/containers/PlaylistViewContainer/PlaylistViewContainer.test.tsx @@ -35,11 +35,11 @@ describe('Playlist view container', () => { const state = store.getState(); expect(state.queue.queueItems).toEqual([ expect.objectContaining({ - artist: 'test artist 1', + artists: ['test artist 1'], name: 'test track' }), expect.objectContaining({ - artist: 'test artist 2', + artists: ['test artist 2'], name: 'test track 22' }) ]); @@ -54,11 +54,11 @@ describe('Playlist view container', () => { const state = store.getState(); expect(state.queue.queueItems).toEqual([ expect.objectContaining({ - artist: 'test artist 1', + artists: ['test artist 1'], name: 'test track' }), expect.objectContaining({ - artist: 'test artist 2', + artists: ['test artist 2'], name: 'test track 22' }) ]); @@ -133,7 +133,7 @@ describe('Playlist view container', () => { const state = store.getState(); expect(state.queue.queueItems).toEqual([ expect.objectContaining({ - artist: 'test artist 2', + artists: ['test artist 2'], name: 'test track 22' }) ]); @@ -146,7 +146,7 @@ describe('Playlist view container', () => { const state = store.getState(); expect(state.queue.queueItems).toEqual([ expect.objectContaining({ - artist: 'test artist 2', + artists: ['test artist 2'], name: 'test track 22' }) ]); @@ -161,7 +161,7 @@ describe('Playlist view container', () => { const state = store.getState(); expect(state.favorites.tracks).toEqual([ expect.objectContaining({ - artist: 'test artist 2', + artists: ['test artist 2'], name: 'test track 22' }) ]); @@ -178,7 +178,7 @@ describe('Playlist view container', () => { completion: 0, status: 'Waiting', track: expect.objectContaining({ - artist: 'test artist 2', + artists: ['test artist 2'], name: 'test track 22' }) }) @@ -193,7 +193,7 @@ describe('Playlist view container', () => { expect(state.playlists.localPlaylists.data[0].tracks).toHaveLength(1); expect(state.playlists.localPlaylists.data[0].tracks).toEqual([ expect.objectContaining({ - artist: 'test artist 2', + artists: ['test artist 2'], name: 'test track 22' }) ]); @@ -223,17 +223,17 @@ describe('Playlist view container', () => { lastModified: 1000198000000, tracks: [{ uuid: 'test-track-uuid-1', - artist: 'test artist 1', + artists: ['test artist 1'], name: 'test track 1', stream: undefined }, { uuid: 'test-track-uuid-1', - artist: 'test artist 1', + artists: ['test artist 1'], name: 'test track 1', stream: undefined }, { uuid: 'test-track-uuid-3', - artist: 'test artist 2', + artists: ['test artist 2'], name: 'test track 2', stream: undefined }]}]) @@ -247,12 +247,12 @@ describe('Playlist view container', () => { expect(state.playlists.localPlaylists.data[0].tracks).toEqual([ expect.objectContaining({ uuid: 'test-track-uuid-1', - artist: 'test artist 1', + artists: ['test artist 1'], name: 'test track 1' }), expect.objectContaining({ uuid: 'test-track-uuid-3', - artist: 'test artist 2', + artists: ['test artist 2'], name: 'test track 2' }) ]); diff --git a/packages/app/app/containers/QueuePopupButtons/index.js b/packages/app/app/containers/QueuePopupButtons/index.js index 0a322bd2fb..d27c2fc210 100644 --- a/packages/app/app/containers/QueuePopupButtons/index.js +++ b/packages/app/app/containers/QueuePopupButtons/index.js @@ -137,7 +137,7 @@ export default compose( favoritesActions.addFavoriteTrack(normalizedTrack); toastActions.info( 'Favorite track added', - `${track.artist} - ${track.name} has been added to favorites.`, + `${track.artists?.[0]} - ${track.name} has been added to favorites.`, , settings ); @@ -154,7 +154,7 @@ export default compose( downloadsActions.addToDownloads(streamProviders, clonedTrack); toastActions.info( 'Track added to downloads', - `${track.artist} - ${track.name} has been added to downloads.`, + `${track.artists?.[0]} - ${track.name} has been added to downloads.`, , settings ); @@ -168,7 +168,7 @@ export default compose( addTrackToPlaylist(updatePlaylist, playlist, track); toastActions.info( 'Track added to playlist', - `${track.artist} - ${track.name} has been added to playlist ${playlist.name}.`, + `${track.artists?.[0]} - ${track.name} has been added to playlist ${playlist.name}.`, , settings ); diff --git a/packages/app/app/containers/SoundContainer/autoradio.js b/packages/app/app/containers/SoundContainer/autoradio.js index 99e5f1176e..0307f21493 100644 --- a/packages/app/app/containers/SoundContainer/autoradio.js +++ b/packages/app/app/containers/SoundContainer/autoradio.js @@ -137,7 +137,7 @@ function getNewTrack (getter, track) { if (getter === 'track') { getTrack = getSimilarTracks(track); } else { - getTrack = getTracksFromSimilarArtist(track.artist); + getTrack = getTracksFromSimilarArtist(track.artists?.[0]); } return getTrack .then(similarTracks => { @@ -145,10 +145,11 @@ function getNewTrack (getter, track) { }); } +// `track` format here is directly from the lastfm api function isTrackInQueue (track) { const queue = props.queue.queueItems; for (const i in queue) { - if (queue[i].artist === track.artist.name && queue[i].name === track.name) { + if (queue[i].artists.includes(track.artist.name) && queue[i].name === track.name) { return true; } } @@ -156,7 +157,7 @@ function isTrackInQueue (track) { } function getSimilarTracks (currentSong, limit = 100) { - return lastfm.getSimilarTracks(currentSong.artist, currentSong.name, limit) + return lastfm.getSimilarTracks(currentSong.artists?.[0], currentSong.name, limit) .then(tracks => tracks.json()) .then(trackJson => { return _.get(trackJson, 'similartracks.track', []); @@ -193,10 +194,10 @@ function getArtistTopTracks (artist) { }); } -function addToQueue (artist, track) { +function addToQueue (artists, track) { return new Promise((resolve) => { props.actions.addToQueue({ - artist: artist.name, + artists, name: track.name, thumbnail: track.thumbnail ?? track.image[0]['#text'] ?? track.thumb }); diff --git a/packages/app/app/containers/SoundContainer/index.js b/packages/app/app/containers/SoundContainer/index.js index 29e849886a..43f39daa4e 100644 --- a/packages/app/app/containers/SoundContainer/index.js +++ b/packages/app/app/containers/SoundContainer/index.js @@ -101,7 +101,7 @@ class SoundContainer extends React.Component { if (this.props.settings.listeningHistory) { ipcRenderer.send(IpcEvents.POST_LISTENING_HISTORY_ENTRY, { - artist: currentSong.artist, + artist: currentSong.artists?.[0], title: currentSong.title ?? currentSong.name }); } @@ -141,7 +141,7 @@ class SoundContainer extends React.Component { .then(selectedArtist => this.getArtistTopTracks(selectedArtist)) .then(topTracks => this.getRandomElement(topTracks.toptracks.track)) .then(track => { - return this.addToQueue(track.artist, track); + return this.addToQueue(track.artists, track); }); } @@ -167,7 +167,7 @@ class SoundContainer extends React.Component { addToQueue(artist, track) { return new Promise((resolve) => { this.props.actions.addToQueue({ - artist: artist.name, + artists: [artist.name], name: track.name, thumbnail: track.thumbnail ?? track.image[0]['#text'] ?? track.thumb }); @@ -241,7 +241,7 @@ class SoundContainer extends React.Component { /> )); diff --git a/packages/app/app/containers/SpotifyPlaylistAdapter/SpotifyPlaylistAdapter.test.tsx b/packages/app/app/containers/SpotifyPlaylistAdapter/SpotifyPlaylistAdapter.test.tsx index abda492ca6..9f76ab46bf 100644 --- a/packages/app/app/containers/SpotifyPlaylistAdapter/SpotifyPlaylistAdapter.test.tsx +++ b/packages/app/app/containers/SpotifyPlaylistAdapter/SpotifyPlaylistAdapter.test.tsx @@ -49,7 +49,7 @@ describe('Spotfiy playlist adapter', () => { uuid: '1', name: 'Test track', album: 'Test album', - artist: 'Test artist', + artists: ['Test artist'], duration: 120, thumbnail: 'thumb.jpg', stream: null @@ -57,7 +57,7 @@ describe('Spotfiy playlist adapter', () => { uuid: '2', name: 'Another test track', album: 'Another test album', - artist: 'Another test artist', + artists: ['Another test artist'], duration: 180, thumbnail: undefined, stream: null diff --git a/packages/app/app/containers/StreamVerificationContainer/index.tsx b/packages/app/app/containers/StreamVerificationContainer/index.tsx index 339fb98cce..a198d12dbc 100644 --- a/packages/app/app/containers/StreamVerificationContainer/index.tsx +++ b/packages/app/app/containers/StreamVerificationContainer/index.tsx @@ -4,7 +4,7 @@ import { useDispatch, useSelector } from 'react-redux'; import { v4 } from 'uuid'; import logger from 'electron-timber'; -import { getTrackArtist, StreamVerification } from '@nuclear/ui'; +import { getTrackArtists, StreamVerification } from '@nuclear/ui'; import { StreamVerificationProps } from '@nuclear/ui/lib/components/StreamVerification'; import { rest } from '@nuclear/core'; @@ -33,14 +33,14 @@ export const StreamVerificationContainer: React.FC = () => { setVerificationStatus('unknown'); if (currentTrack) { StreamMappingsService.getTopStream( - getTrackArtist(currentTrack), + getTrackArtists(currentTrack)?.[0], currentTrack.name, selectedStreamProvider, settings?.userId ).then(res => { if (isResponseBody(res) && res.body.stream_id === head(currentTrack.streams)?.id) { if (res.body.score === undefined) { - logger.error(`Failed to verify stream: ${currentTrack.name} by ${getTrackArtist(currentTrack)}`); + logger.error(`Failed to verify stream: ${currentTrack.name} by ${getTrackArtists(currentTrack)?.[0]}`); setVerificationStatus('unverified'); } else if (res.body.self_verified) { setVerificationStatus('verified_by_user'); @@ -72,7 +72,7 @@ export const StreamVerificationContainer: React.FC = () => { if (currentTrack && verificationStatus !== 'verified_by_user') { setLoading(true); StreamMappingsService.postStreamMapping({ - artist: getTrackArtist(currentTrack), + artist: getTrackArtists(currentTrack)?.[0], title: currentTrack.name, source: selectedStreamProvider, stream_id: head(currentTrack.streams).id, @@ -80,7 +80,7 @@ export const StreamVerificationContainer: React.FC = () => { }).then(() => { setVerificationStatus('verified_by_user'); }).catch((e) => { - logger.error(`Failed to verify stream: ${currentTrack.name} by ${getTrackArtist(currentTrack)}`); + logger.error(`Failed to verify stream: ${currentTrack.name} by ${getTrackArtists(currentTrack)?.[0]}`); logger.error(e); setVerificationStatus('unknown'); }).finally(() => { @@ -93,7 +93,7 @@ export const StreamVerificationContainer: React.FC = () => { if (currentTrack && verificationStatus === 'verified_by_user') { setLoading(true); StreamMappingsService.deleteStreamMapping({ - artist: getTrackArtist(currentTrack), + artist: getTrackArtists(currentTrack)?.[0], title: currentTrack.name, source: selectedStreamProvider, stream_id: head(currentTrack.streams).id, @@ -101,7 +101,7 @@ export const StreamVerificationContainer: React.FC = () => { }).then(() => { setVerificationStatus('unverified'); }).catch(() => { - logger.error(`Failed to unverify stream: ${currentTrack.name} by ${getTrackArtist(currentTrack)}`); + logger.error(`Failed to unverify stream: ${currentTrack.name} by ${getTrackArtists(currentTrack)?.[0]}`); setVerificationStatus('verified_by_user'); }).finally(() => { setLoading(false); diff --git a/packages/app/app/containers/TrackPopupButtons/index.js b/packages/app/app/containers/TrackPopupButtons/index.js index 89987d8bf0..67bb23f596 100644 --- a/packages/app/app/containers/TrackPopupButtons/index.js +++ b/packages/app/app/containers/TrackPopupButtons/index.js @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import _ from 'lodash'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; -import { PopupButton, getThumbnail, getTrackItem, getTrackTitle, getTrackArtist } from '@nuclear/ui'; +import { PopupButton, getThumbnail, getTrackItem, getTrackTitle, getTrackArtists } from '@nuclear/ui'; import * as DownloadsActions from '../../actions/downloads'; import * as QueueActions from '../../actions/queue'; @@ -27,7 +27,7 @@ const TrackPopupButtons = ({ }) => { const title = getTrackTitle(track); - const artist = getTrackArtist(track); + const artist = getTrackArtists(track); const handleAddToQueue = useCallback(() => queueActions.addToQueue(getTrackItem(track)), diff --git a/packages/app/app/containers/TrackPopupContainer/TrackPopupContainer.test.tsx b/packages/app/app/containers/TrackPopupContainer/TrackPopupContainer.test.tsx index 36e61c5fe1..967c013975 100644 --- a/packages/app/app/containers/TrackPopupContainer/TrackPopupContainer.test.tsx +++ b/packages/app/app/containers/TrackPopupContainer/TrackPopupContainer.test.tsx @@ -115,7 +115,7 @@ describe('Track Popup container', () => { queue: { currentSong: 1, queueItems: [{ - artist: 'test artist 1', + artists: ['test artist 1'], name: 'test track 1', streams: [{ duration: 300, @@ -123,7 +123,7 @@ describe('Track Popup container', () => { skipSegments: [] }] }, { - artist: 'test artist 2', + artists: ['test artist 2'], name: 'test track 2', streams: [{ duration: 300, @@ -144,7 +144,7 @@ describe('Track Popup container', () => { expect(state.player.seek).toBe(10); expect(state.queue.currentSong).toBe(1); expect(state.queue.queueItems[state.queue.currentSong + 1]).toMatchObject({ - artist: 'Artist', + artists: ['Artist'], name: TRACK_TITLE }); }); @@ -157,9 +157,7 @@ describe('Track Popup container', () => { Trigger} diff --git a/packages/app/app/containers/TrackPopupContainer/hooks.ts b/packages/app/app/containers/TrackPopupContainer/hooks.ts index 32820e7e3e..ee7ebf761a 100644 --- a/packages/app/app/containers/TrackPopupContainer/hooks.ts +++ b/packages/app/app/containers/TrackPopupContainer/hooks.ts @@ -4,7 +4,7 @@ import _ from 'lodash'; import { useTranslation } from 'react-i18next'; import { PlaylistHelper } from '@nuclear/core'; -import { getTrackArtist, getTrackItem } from '@nuclear/ui/lib'; +import { getTrackArtists, getTrackItem } from '@nuclear/ui/lib'; import { TrackPopupStrings } from '@nuclear/ui/lib/components/TrackPopup'; import { settingsSelector } from '../../selectors/settings'; @@ -42,7 +42,7 @@ export const useTrackPopupProps = (track, thumb) => { const toastThumb = React.createElement('img', {src: thumb}); const { t } = useTranslation('track-popup'); - const artist = getTrackArtist(track); + const artist = getTrackArtists(track); const favoriteToastTitle = t('favorite-toast-title'); const favoriteToastBody = t('favorite-toast-body', { @@ -82,9 +82,6 @@ export const useTrackPopupProps = (track, thumb) => { }); const onAddToPlaylist = useCallback((playlist) => { const clonedTrack = {...safeAddUuid(track)}; - if (clonedTrack.artist.name) { - _.set(clonedTrack, 'artist', clonedTrack.artist.name); - } dispatch(PlaylistsActions.updatePlaylist({ ...playlist, @@ -101,9 +98,6 @@ export const useTrackPopupProps = (track, thumb) => { const onCreatePlaylist = useCallback( ({ name }: { name: string }) => { const clonedTrack = {...safeAddUuid(track)}; - if (clonedTrack.artist.name) { - _.set(clonedTrack, 'artist', clonedTrack.artist.name); - } dispatch(PlaylistsActions.addPlaylist([PlaylistHelper.extractTrackData(track)], name)); }, [dispatch] diff --git a/packages/app/app/containers/TrackTableContainer/index.tsx b/packages/app/app/containers/TrackTableContainer/index.tsx index 956282da4b..21b814ca60 100644 --- a/packages/app/app/containers/TrackTableContainer/index.tsx +++ b/packages/app/app/containers/TrackTableContainer/index.tsx @@ -97,9 +97,6 @@ function TrackTableContainer ({ const onCreatePlaylist = useCallback( (track: Track, { name }: { name: string } ) => { const clonedTrack = {...safeAddUuid(track)}; - if (clonedTrack.artist.name) { - _.set(clonedTrack, 'artist', clonedTrack.artist.name); - } dispatch(playlistActions.addPlaylist([clonedTrack], name)); }, [dispatch] diff --git a/packages/app/app/reducers/dashboard.ts b/packages/app/app/reducers/dashboard.ts index 2d0bf5e089..41678e47c6 100644 --- a/packages/app/app/reducers/dashboard.ts +++ b/packages/app/app/reducers/dashboard.ts @@ -12,7 +12,7 @@ import { PlaylistTrack } from '@nuclear/core'; import { PromotedArtist } from '@nuclear/core/src/rest/Nuclear/Promotion'; export type InternalTopTrack = Pick & { - artist: string; + artists: string[]; thumbnail: string; }; diff --git a/packages/app/app/reducers/queue.ts b/packages/app/app/reducers/queue.ts index 135724633a..ce71857c2f 100644 --- a/packages/app/app/reducers/queue.ts +++ b/packages/app/app/reducers/queue.ts @@ -25,7 +25,7 @@ export type QueueItem = { details: string; }; local?: boolean; - artist: string | { name: string }; + artists: string[]; name: string; thumbnail?: string; streams?: TrackStream[]; diff --git a/packages/app/app/selectors/favorites.ts b/packages/app/app/selectors/favorites.ts index 2c1cadd7bc..eb76d19740 100644 --- a/packages/app/app/selectors/favorites.ts +++ b/packages/app/app/selectors/favorites.ts @@ -5,24 +5,20 @@ type Name = string | { name: string; }; -export function getFavoriteTrack(state, artist: Name, title: Name) { - if (!artist || !title) { +export function getFavoriteTrack(state, artists?: string[], title?: Name) { + if (!artists || !title) { return null; } - const resolvedArtist = isString(artist) - ? artist - : artist.name; - const resolvedTitle = isString(title) ? title : title.name; - const normalizedArtist = _.deburr(resolvedArtist.toLowerCase()); + const normalizedArtist = _.deburr(artists?.[0]?.toLowerCase()); const normalizedTitle = _.deburr(resolvedTitle.toLowerCase()); return _.find(state.favorites.tracks, track => { - const normalizedStoreArtist = _.deburr(_.defaultTo(track.artist.name, track.artist).toLowerCase()); + const normalizedStoreArtist = _.deburr(track.artists?.[0]?.toLowerCase()); const normalizedStoreTitle = _.deburr(track.name.toLowerCase()); return normalizedStoreArtist === normalizedArtist && normalizedStoreTitle === normalizedTitle; diff --git a/packages/app/app/utils.ts b/packages/app/app/utils.ts index e89bd1383d..2d0c92bfe9 100644 --- a/packages/app/app/utils.ts +++ b/packages/app/app/utils.ts @@ -1,5 +1,5 @@ import { Track } from '@nuclear/ui/lib/types'; -import { getTrackArtist, getTrackTitle } from '@nuclear/ui'; +import { getTrackArtists, getTrackTitle } from '@nuclear/ui'; export function formatDuration(duration) { const secNum = parseInt(duration, 10); @@ -49,9 +49,7 @@ export function createLastFMLink(artist, track) { export function normalizeTrack(track: Track){ return { - artist: { - name: getTrackArtist(track) - }, + artists: getTrackArtists(track), name: getTrackTitle(track), thumbnail: track.thumbnail, streams: track.streams diff --git a/packages/app/test/storeBuilders.ts b/packages/app/test/storeBuilders.ts index cf887d5369..ab1db14899 100644 --- a/packages/app/test/storeBuilders.ts +++ b/packages/app/test/storeBuilders.ts @@ -122,21 +122,21 @@ export const buildStoreState = () => { { uuid: 'track-1-id', ids: [], - artist: 'test artist', + artists: ['test artist'], title: 'test track 1', duration: 120 }, { uuid: 'track-2-id', ids: [], - artist: 'test artist', + artists: ['test artist'], title: 'test track 2', duration: 63 }, { uuid: 'track-3-id', ids: [], - artist: 'test artist', + artists: ['test artist'], title: 'test track 3', duration: 7 } @@ -172,7 +172,7 @@ export const buildStoreState = () => { tracklist: [ { uuid: 'track-1', - artist: 'test artist 1', + artists: ['test artist 1'], title: 'test track 1', duration: 10 } @@ -191,7 +191,7 @@ export const buildStoreState = () => { tracklist: [ { uuid: 'track-2', - artist: 'test artist 2', + artists: ['test artist 2'], title: 'test track 2', duration: 40 } @@ -210,7 +210,7 @@ export const buildStoreState = () => { tracklist: [ { uuid: 'track-3', - artist: 'test artist', + artists: ['test artist'], title: 'test track 3', duration: 40 } @@ -234,27 +234,21 @@ export const buildStoreState = () => { thumb: 'test thumb', topTracks: [ { - artist: { - name: 'test artist' - }, + artists: ['test artist'], listeners: 771858, playcount: 6900237, thumb: '', title: 'test artist top track 1' }, { - artist: { - name: 'test artist' - }, + artists: ['test artist'], listeners: 123, playcount: 6969, thumb: '', title: 'test artist top track 2' }, { - artist: { - name: 'test artist' - }, + artists: ['test artist'], listeners: 9, playcount: 1, thumb: '', @@ -389,7 +383,7 @@ export const buildStoreState = () => { tracks: [ { uuid: 'test-track-1', - artist: 'test artist 1', + artists: ['test artist 1'], name: 'test track', thumbnail: 'test thumbnail', stream: { @@ -401,7 +395,7 @@ export const buildStoreState = () => { }, { uuid: 'test-track-2', - artist: 'test artist 2', + artists: ['test artist 2'], name: 'test track 22', thumbnail: 'test thumbnail 2', stream: { @@ -419,7 +413,7 @@ export const buildStoreState = () => { tracks: [ { uuid: 'test-track-1', - artist: 'test artist 1', + artists: ['test artist 1'], name: 'test track', thumbnail: 'test thumbnail', stream: @@ -432,7 +426,7 @@ export const buildStoreState = () => { }, { uuid: 'test-track-22', - artist: 'test artist 2', + artists: ['test artist 2'], name: 'test track 22', thumbnail: 'test thumbnail 2', stream: { @@ -457,14 +451,14 @@ export const buildStoreState = () => { tracks: [ { position: 2, - artist: 'test artist 1', + artists: ['test artist 1'], name: 'test track 1', thumbnail: 'https://test-track-thumb-url', uuid: 'uuid1' }, { position: 1, - artist: 'test artist 2', + artists: ['test artist 2'], name: 'test track 2', thumbnail: 'https://test-track-thumb-url', stream: undefined, @@ -519,7 +513,7 @@ export const buildStoreState = () => { id: 1, position: 1, title: 'top track 1', - artist: 'top track artist 1', + artists: ['top track artist 1'], thumbnail: 'top track thumbnail 1', duration: 100, album: { @@ -534,7 +528,7 @@ export const buildStoreState = () => { id: 2, position: 2, title: 'top track 2', - artist: 'top track artist 2', + artists: ['top track artist 2'], thumbnail: 'top track thumbnail 2', duration: 78, album: { @@ -615,12 +609,12 @@ export const buildStoreState = () => { tracklist: [{ uuid: '1', name: 'track 1', - artist: 'artist 1', + artists: ['artist 1'], stream: undefined }, { uuid: '2', name: 'track 2', - artist: 'artist 2', + artists: ['artist 2'], stream: undefined }] } @@ -651,7 +645,7 @@ export const buildStoreState = () => { completion: 1, track: { uuid: '1', - artist: 'test artist 1', + artists: ['test artist 1'], name: 'finished track' } }, @@ -660,7 +654,7 @@ export const buildStoreState = () => { completion: 0.1, track: { uuid: '2', - artist: 'test artist 2', + artists: ['test artist 2'], name: 'track with errorx' } }, @@ -669,7 +663,7 @@ export const buildStoreState = () => { completion: 0.3, track: { uuid: '3', - artist: 'test artist 3', + artists: ['test artist 3'], name: 'paused track' } }, @@ -678,7 +672,7 @@ export const buildStoreState = () => { completion: 0.5, track: { uuid: '4', - artist: 'test artist 4', + artists: ['test artist 4'], name: 'started track' } }, @@ -687,7 +681,7 @@ export const buildStoreState = () => { completion: 0, track: { uuid: '5', - artist: 'test artist 5', + artists: ['test artist 5'], name: 'waiting track' } } @@ -714,7 +708,7 @@ export const buildStoreState = () => { queue: { queueItems: [ { - artist: 'test artist 1', + artists: ['test artist 1'], name: 'test track 1', thumbnail: 'https://test-track-thumb-url', streams: [{ @@ -752,7 +746,7 @@ export const buildStoreState = () => { error: false }, { - artist: 'test artist 2', + artists: ['test artist 2'], name: 'test track 2', thumbnail: 'https://test-track-thumb-url', streams: [{ @@ -770,7 +764,7 @@ export const buildStoreState = () => { error: false }, { - artist: 'test artist 3', + artists: ['test artist 3'], name: 'test track 3', thumbnail: undefined, streams: [{ @@ -853,7 +847,7 @@ export const buildStoreState = () => { listType: 'simple-list', tracks: tracks ?? [{ uuid: 'local-track-1', - artist: 'local artist 1', + artists: ['local artist 1'], name: 'local track 1', album: 'local album 1', thumbnail: 'local track thumbnail 1', @@ -866,7 +860,7 @@ export const buildStoreState = () => { }, { uuid: 'local-track-2', - artist: 'local artist 1', + artists: ['local artist 1'], name: 'local track 2', album: 'local album 1', thumbnail: 'local track thumbnail 2', @@ -878,7 +872,7 @@ export const buildStoreState = () => { local: true }, { uuid: 'local-track-3', - artist: 'local artist 2', + artists: ['local artist 2'], name: 'local track 3', album: 'local album 2', thumbnail: 'local track thumbnail 3', diff --git a/packages/core/src/helpers/playlist/index.ts b/packages/core/src/helpers/playlist/index.ts index d1b4932fb3..4b58dbe0c3 100644 --- a/packages/core/src/helpers/playlist/index.ts +++ b/packages/core/src/helpers/playlist/index.ts @@ -18,7 +18,7 @@ const extractTrackData = (track, streamSource: string = null): PlaylistTrack => return track && (track.name || track.title) && (!track.type || track.type === 'track') ? { - artist: track.artist, + artists: track.artists, name: track.name || track.title, album: track.album, thumbnail: track.thumbnail, diff --git a/packages/core/src/helpers/playlist/types.ts b/packages/core/src/helpers/playlist/types.ts index 400c909dbf..bd904832da 100644 --- a/packages/core/src/helpers/playlist/types.ts +++ b/packages/core/src/helpers/playlist/types.ts @@ -8,7 +8,7 @@ export class Playlist { export class PlaylistTrack { uuid: string; - artist: string; + artists: string[]; name: string; album?: string; thumbnail?: string; diff --git a/packages/core/src/plugins/meta/audius.ts b/packages/core/src/plugins/meta/audius.ts index 7ec486127e..42009a96d5 100644 --- a/packages/core/src/plugins/meta/audius.ts +++ b/packages/core/src/plugins/meta/audius.ts @@ -101,7 +101,7 @@ class AudiusMetaProvider extends MetaProvider { thumb: track.artwork ? track.artwork['480x480'] : '', playcount: 0, listeners: track.listeners, - artist: { name: AudiusInfo.name } + artists: [AudiusInfo.name] })), similar: similarArtists, source: SearchResultsSource.Audius diff --git a/packages/core/src/plugins/meta/bandcamp.ts b/packages/core/src/plugins/meta/bandcamp.ts index d5e84696df..49f373826d 100644 --- a/packages/core/src/plugins/meta/bandcamp.ts +++ b/packages/core/src/plugins/meta/bandcamp.ts @@ -62,7 +62,7 @@ class BandcampMetaProvider extends MetaProvider { .map(result => ({ id: btoa(result.url), title: result.name, - artist: result.artist, + artists: [result.artist], source: SearchResultsSource.Bandcamp })); } @@ -119,7 +119,7 @@ class BandcampMetaProvider extends MetaProvider { thumb: bandcampArtistDetails.coverImage, playcount: track.playcount, listeners: track.listeners, - artist: track.artist + artists: [track.artist.name] })), source: SearchResultsSource.Bandcamp }); @@ -138,7 +138,7 @@ class BandcampMetaProvider extends MetaProvider { coverImage: album.imageUrl, type: albumType as AlbumType, tracklist: album.tracks.map((track, index) => new Track({ - artist: album.artist, + artists: [album.artist], title: track.name, duration: track.duration, thumbnail: album.imageUrl, diff --git a/packages/core/src/plugins/meta/discogs.ts b/packages/core/src/plugins/meta/discogs.ts index 12707a3fa5..b60ade1f77 100644 --- a/packages/core/src/plugins/meta/discogs.ts +++ b/packages/core/src/plugins/meta/discogs.ts @@ -194,7 +194,7 @@ class DiscogsMetaProvider extends MetaProvider { thumb: coverImage, playcount: track.playcount, listeners: track.listeners, - artist: track.artist + artists: [track.artist.name] })), similar: similarArtists, source: SearchResultsSource.Discogs diff --git a/packages/core/src/plugins/meta/itunesmusic.test.ts b/packages/core/src/plugins/meta/itunesmusic.test.ts index 0d2baf468e..d590c645b4 100644 --- a/packages/core/src/plugins/meta/itunesmusic.test.ts +++ b/packages/core/src/plugins/meta/itunesmusic.test.ts @@ -77,7 +77,7 @@ describe('iTunes music metaprovider tests', () => { const response = await itunesMeta.fetchAlbumDetails('1440650428', 'master'); expect(fetch).toHaveBeenCalledTimes(1); const track = new Track ({ - 'artist': 'The Platinum Collection (Greatest Hits I, II & III)', + 'artists': ['The Platinum Collection (Greatest Hits I, II & III)'], 'duration': 356, 'position': 1, 'thumbnail': 'https://is3-ssl.mzstatic.com/image/thumb/Music115/v4/83/23/e4/8323e48b-3467-448b-1ce0-8981d8a97437/source/60x60bb.jpg', diff --git a/packages/core/src/plugins/meta/itunesmusic.ts b/packages/core/src/plugins/meta/itunesmusic.ts index 81a07b2f72..70f7d31c2c 100644 --- a/packages/core/src/plugins/meta/itunesmusic.ts +++ b/packages/core/src/plugins/meta/itunesmusic.ts @@ -97,7 +97,7 @@ class iTunesMusicMetaProvider extends MetaProvider { thumb: artistDetails.results[1].artworkUrl100.replace('100x100bb.jpg', '250x250bb.jpg'), playcount: track.playcount, listeners: track.listeners, - artist: track.artist + artists: [track.artist.name] })), source: SearchResultsSource.iTunesMusic }); @@ -135,7 +135,7 @@ class iTunesMusicMetaProvider extends MetaProvider { year: albumInfo[0].releaseDate, type: albumType as AlbumType, tracklist: _.map(albumInfo.slice(1), (episode, index) => new Track ({ - artist: episode.collectionName, + artists: [episode.collectionName], title: episode.trackName, duration: Math.ceil(episode.trackTimeMillis/1000), thumbnail: episode.artworkUrl60, diff --git a/packages/core/src/plugins/meta/itunespodcast.test.ts b/packages/core/src/plugins/meta/itunespodcast.test.ts index 760339edff..f6f5643ab7 100644 --- a/packages/core/src/plugins/meta/itunespodcast.test.ts +++ b/packages/core/src/plugins/meta/itunespodcast.test.ts @@ -27,7 +27,7 @@ describe('iTunes podcast metaprovider tests', () => { const response = await itunesMeta.fetchAlbumDetails('Programming Throwdown'); expect(fetch).toHaveBeenCalledTimes(1); const track = new Track ({ - 'artist': 'Programming Throwdown', + 'artists': ['Programming Throwdown'], 'duration': 4554, 'position': 1, 'thumbnail': 'https://is3-ssl.mzstatic.com/image/thumb/Podcasts125/v4/83/e8/a9/83e8a9d5-df87-b19d-7050-55e4ce4df89d/mza_13511678666604160959.jpg/60x60bb.jpg', diff --git a/packages/core/src/plugins/meta/itunespodcast.ts b/packages/core/src/plugins/meta/itunespodcast.ts index 6315abf443..d4e30215a3 100644 --- a/packages/core/src/plugins/meta/itunespodcast.ts +++ b/packages/core/src/plugins/meta/itunespodcast.ts @@ -56,7 +56,7 @@ class iTunesPodcastMetaProvider extends MetaProvider { year: podcastInfo[0].releaseDate, type: AlbumType.master, tracklist: _.map(podcastInfo.slice(1), (episode, index) => new Track ({ - artist: episode.collectionName, + artists: [episode.collectionName], title: episode.trackName, duration: Math.ceil(episode.trackTimeMillis/1000), thumbnail: episode.artworkUrl60, diff --git a/packages/core/src/plugins/meta/musicbrainz.ts b/packages/core/src/plugins/meta/musicbrainz.ts index bf9dde615f..086947afc5 100644 --- a/packages/core/src/plugins/meta/musicbrainz.ts +++ b/packages/core/src/plugins/meta/musicbrainz.ts @@ -76,7 +76,7 @@ class MusicbrainzMetaProvider extends MetaProvider { .then(response => response.tracks.map(track => ({ id: track.id, title: '', - artist: '', + artists: [''], source: SearchResultsSource.Musicbrainz }))); } @@ -105,7 +105,7 @@ class MusicbrainzMetaProvider extends MetaProvider { tags: _.map(lastFmInfo.tags.tag, 'name'), onTour: lastFmInfo.ontour === '1', topTracks: _.map(lastFmTopTracks.track, (track: LastfmTrack) => ({ - artist: { name: track.artist.name }, + artists: [track.artist.name], title: track.name, playcount: track.playcount, listeners: track.listeners diff --git a/packages/core/src/plugins/meta/spotify.test.ts b/packages/core/src/plugins/meta/spotify.test.ts index 21ebe84e04..41a96b0ceb 100644 --- a/packages/core/src/plugins/meta/spotify.test.ts +++ b/packages/core/src/plugins/meta/spotify.test.ts @@ -180,13 +180,13 @@ describe('SpotifyMetaProvider', () => { images: ['thumbnail.jpg', 'image.jpg', 'large.jpg'], topTracks: [{ title: 'test track', - artist: { name: 'test artist' }, + artists: ['test artist'], thumb: undefined, playcount: 100, listeners: 100 }, { title: 'test track 2', - artist: {name: 'test artist'}, + artists: ['test artist'], thumb: undefined, playcount: 100, listeners: 100 @@ -267,13 +267,13 @@ describe('SpotifyMetaProvider', () => { images: ['thumbnail.jpg', 'image.jpg', 'large.jpg'], topTracks: [{ title: 'test track', - artist: { name: 'test artist' }, + artists: ['test artist'], thumb: undefined, playcount: 100, listeners: 100 }, { title: 'test track 2', - artist: {name: 'test artist'}, + artists: ['test artist'], thumb: undefined, playcount: 100, listeners: 100 diff --git a/packages/core/src/plugins/meta/spotify.ts b/packages/core/src/plugins/meta/spotify.ts index 478485f5db..bf5bb62c71 100644 --- a/packages/core/src/plugins/meta/spotify.ts +++ b/packages/core/src/plugins/meta/spotify.ts @@ -114,7 +114,7 @@ export class SpotifyMetaProvider extends MetaProvider { mapSpotifyTopTrack(spotifyTrack: SpotifyTrack): ArtistTopTrack { const { thumb } = getImageSet(spotifyTrack.album.images); return { - artist: {name: spotifyTrack.artists[0].name}, + artists: [spotifyTrack.artists[0].name], title: spotifyTrack.name, thumb, playcount: spotifyTrack.popularity, diff --git a/packages/core/src/plugins/plugins.types.ts b/packages/core/src/plugins/plugins.types.ts index 955528f8ac..746d75623f 100644 --- a/packages/core/src/plugins/plugins.types.ts +++ b/packages/core/src/plugins/plugins.types.ts @@ -37,7 +37,7 @@ export type SearchResultsAlbum = { type?: string; tracklist?: { uuid: string; - artist: string; + artists: string[]; title: string; duration: number; }[]; @@ -59,13 +59,13 @@ export type SearchResultsPodcast = { export type SearchResultsTrack = { id: string; title: string; - artist: string; + artists: string[]; source: SearchResultsSource; thumb?: string; } export type ArtistTopTrack = { - artist: { name: string }; + artists: string[]; title: string; thumb?: string; playcount?: number; diff --git a/packages/core/src/rest/Deezer.ts b/packages/core/src/rest/Deezer.ts index f28f51b7bb..007a2653e7 100644 --- a/packages/core/src/rest/Deezer.ts +++ b/packages/core/src/rest/Deezer.ts @@ -107,6 +107,6 @@ export const mapDeezerTrackToInternal = (track: DeezerTrack) => ({ ...track, uuid: track.id.toString(), name: track.title, - artist: isString(track.artist) ? track.artist : track.artist.name, + artists: isString(track.artist) ? [track.artist] : [track.artist.name], thumbnail: isString(track.artist) ? null : track.artist.picture_medium }); diff --git a/packages/core/src/rest/Spotify.ts b/packages/core/src/rest/Spotify.ts index 172fed0f50..0d3ffbe3b3 100644 --- a/packages/core/src/rest/Spotify.ts +++ b/packages/core/src/rest/Spotify.ts @@ -248,7 +248,7 @@ export const mapSpotifyTrack = (track: SpotifyTrack): PlaylistTrack | null => { try { return { uuid: track.id, - artist: track.artists[0].name, + artists: [track.artists[0].name], name: track.name, album: track.album.name, thumbnail: thumb, diff --git a/packages/core/src/structs/Track.ts b/packages/core/src/structs/Track.ts index c8d14adce5..73ee78c36d 100644 --- a/packages/core/src/structs/Track.ts +++ b/packages/core/src/structs/Track.ts @@ -11,7 +11,7 @@ export default class Track { ids?: { [K in SearchResultsSource]?: string }; - artist: string; + artists: string[]; title: string; name?: string; duration: string | number; @@ -19,17 +19,16 @@ export default class Track { position?: string | number; playcount?: string | number; thumbnail?: string; - extraArtists?: string[]; type?: string; local?: boolean; - constructor(data: PartialExcept = { - artist: '', + constructor(data: PartialExcept = { + artists: [''], title: '' }) { this.uuid = v4(); this.ids = data.ids || {}; - this.artist = data.artist; + this.artists = data.artists; this.title = data.title; this.name = data.title; this.duration = data.duration; @@ -39,7 +38,7 @@ export default class Track { addSearchResultData(data: SearchResultsTrack): void { this.ids = { ...this.ids, [data.source]: data.id }; - this.artist = data.artist; + this.artists = data.artists; this.title = data.title; this.name = data.title; } diff --git a/packages/core/src/types/index.ts b/packages/core/src/types/index.ts index 42304f284a..39af57fb66 100644 --- a/packages/core/src/types/index.ts +++ b/packages/core/src/types/index.ts @@ -38,7 +38,7 @@ export type PartialExcept = Partial & Pick typeof artist === 'object' && 'name' in artist; diff --git a/packages/main/src/controllers/download.ts b/packages/main/src/controllers/download.ts index 377e1a6b92..622321f0fc 100644 --- a/packages/main/src/controllers/download.ts +++ b/packages/main/src/controllers/download.ts @@ -7,7 +7,7 @@ import { createWriteStream, createReadStream } from 'fs'; import { rm } from 'fs/promises'; import { ipcController, ipcEvent } from '../utils/decorators'; -import { getTrackArtist, getTrackTitle } from '../utils/tracks'; +import { getTrackArtists, getTrackTitle } from '../utils/tracks'; import Download from '../services/download'; import Logger, { $mainLogger } from '../services/logger'; import Window from '../services/window'; @@ -63,7 +63,7 @@ class DownloadIpcCtrl { } this.downloadItems = this.downloadItems.filter((item => item.uuid === uuid)); } - const artistName = getTrackArtist(data); + const artistName = getTrackArtists(data)?.[0]; const title = getTrackTitle(data); // .replace(/[/\\?%*:|"<>]/g, '-') or equivalent invalid characters based on platform diff --git a/packages/main/src/services/@linux/system-api.ts b/packages/main/src/services/@linux/system-api.ts index 4e95d9f894..3fce4073ff 100644 --- a/packages/main/src/services/@linux/system-api.ts +++ b/packages/main/src/services/@linux/system-api.ts @@ -76,7 +76,8 @@ class LinuxMediaService extends MprisService implements NuclearApi { 'mpris:trackid': this.objectPath(`track/${index}`), 'mpris:artUrl': track.thumbnail, 'xesam:title': track.name, - 'xesam:artist': [track.artist] + // @ts-expect-error NuclearMeta is not a descendant of Track, but Track.artists should be available here anyway + 'xesam:artist': track.artists ?? [track.artist] // 'mpris:length': this.getDuration(track.streams), // 'xesam:album': '21' }; diff --git a/packages/main/src/utils/tracks.ts b/packages/main/src/utils/tracks.ts index 525cad386b..76066797a0 100644 --- a/packages/main/src/utils/tracks.ts +++ b/packages/main/src/utils/tracks.ts @@ -3,6 +3,4 @@ import _ from 'lodash'; export const getTrackTitle = (track: TrackType) => track?.name || track?.title; -export const getTrackArtist = (track: TrackType) => _.isString(track?.artist) - ? track?.artist - : track?.artist?.name; +export const getTrackArtists = (track: TrackType) => track?.artists; diff --git a/packages/ui/lib/components/MiniPlayer/MiniTrackInfo/index.tsx b/packages/ui/lib/components/MiniPlayer/MiniTrackInfo/index.tsx index 2c21a3f86a..df1496bf25 100644 --- a/packages/ui/lib/components/MiniPlayer/MiniTrackInfo/index.tsx +++ b/packages/ui/lib/components/MiniPlayer/MiniTrackInfo/index.tsx @@ -11,7 +11,7 @@ export type MiniTrackInfoProps = Omit = ({ cover = artPlaceholder as unknown as string, track, - artist, + artists, addToFavorites, removeFromFavorites, isFavorite = false, @@ -26,7 +26,7 @@ const MiniTrackInfo: React.FC = ({
{track}
-
{artist}
+
{artists}
& const MiniPlayer: React.FC = ({ cover, track, - artist, + artists, addToFavorites, removeFromFavorites, @@ -60,7 +60,7 @@ const MiniPlayer: React.FC = ({ = ({ cover, track, - artist, + artists, onTrackClick, onArtistClick, addToFavorites, @@ -93,7 +93,7 @@ const PlayerBar: React.FC = ({ = ({ {getTrackTitle(track)}
- {getTrackArtist(track)} + {getTrackArtists(track)?.[0]}
diff --git a/packages/ui/lib/components/TrackInfo/index.tsx b/packages/ui/lib/components/TrackInfo/index.tsx index ef2c92c63a..634e2626e8 100644 --- a/packages/ui/lib/components/TrackInfo/index.tsx +++ b/packages/ui/lib/components/TrackInfo/index.tsx @@ -7,7 +7,7 @@ import styles from './styles.scss'; export type TrackInfoProps = { cover?: string; track: string; - artist: string; + artists: string[]; onTrackClick: () => void; onArtistClick: () => void; addToFavorites: () => void; @@ -19,7 +19,7 @@ export type TrackInfoProps = { const TrackInfo: React.FC = ({ cover, track, - artist, + artists, onTrackClick, onArtistClick, addToFavorites, @@ -37,7 +37,7 @@ const TrackInfo: React.FC = ({ {track}
- {artist} + {artists?.[0]}
diff --git a/packages/ui/lib/components/TrackRow/index.tsx b/packages/ui/lib/components/TrackRow/index.tsx index cdccb80cc7..8d18958033 100644 --- a/packages/ui/lib/components/TrackRow/index.tsx +++ b/packages/ui/lib/components/TrackRow/index.tsx @@ -82,9 +82,7 @@ const TrackRow: React.FC = ({ displayArtist && { - _.isString(track.artist) - ? track.artist - : track.artist.name + track.artists?.[0] } } diff --git a/packages/ui/lib/components/TrackTable/index.tsx b/packages/ui/lib/components/TrackTable/index.tsx index 5eae104881..5463c723ac 100644 --- a/packages/ui/lib/components/TrackTable/index.tsx +++ b/packages/ui/lib/components/TrackTable/index.tsx @@ -95,9 +95,7 @@ function TrackTable({ displayArtist && { id: TrackTableColumn.Artist, Header: ({ column }) => , - accessor: (track) => isString(track.artist) - ? track.artist - : track.artist.name, + accessor: (track) => track.artists?.[0], Cell: TrackTableCell, enableSorting: true }, diff --git a/packages/ui/lib/types/index.ts b/packages/ui/lib/types/index.ts index 49f753f909..fbb62d5930 100644 --- a/packages/ui/lib/types/index.ts +++ b/packages/ui/lib/types/index.ts @@ -6,7 +6,7 @@ export type Track = { details: string }; local?: boolean; - artist: { name: string } | string; + artists: string[]; name?: string; title?: string; album?: string; @@ -41,7 +41,7 @@ export type SearchProviderOption = { } export type TrackItem = { - artist: string; + artists: string[]; name: string; thumbnail?: string; local?: boolean; diff --git a/packages/ui/lib/utils.ts b/packages/ui/lib/utils.ts index 0cd7c4ce08..5845c08c82 100644 --- a/packages/ui/lib/utils.ts +++ b/packages/ui/lib/utils.ts @@ -34,16 +34,14 @@ export function formatDuration(duration) { export const getTrackTitle = (track: Track) => track?.name || track?.title; -export const getTrackArtist = (track: Track) => _.isString(track?.artist) - ? track?.artist - : track?.artist?.name; +export const getTrackArtists = (track: Track) => track?.artists; export const getThumbnail = albumOrTrack => _.get(albumOrTrack, 'coverImage') || _.get(albumOrTrack, 'thumb') || _.get(albumOrTrack, 'thumbnail'); export const getTrackItem = (track: Track): TrackItem => ({ - artist: getTrackArtist(track), + artists: getTrackArtists(track), name: getTrackTitle(track), thumbnail: getThumbnail(track), local: track.local, @@ -51,7 +49,7 @@ export const getTrackItem = (track: Track): TrackItem => ({ uuid: track.uuid }); -export const areTracksEqualByName = (trackA: Track, trackB: Track) => getTrackArtist(trackA) === getTrackArtist(trackB) && getTrackTitle(trackA) === getTrackTitle(trackB); +export const areTracksEqualByName = (trackA: Track, trackB: Track) => _.isEqual(getTrackArtists(trackA), getTrackArtists(trackB)) && getTrackTitle(trackA) === getTrackTitle(trackB); export const timestampToDateString = (timestamp: number, locale: string) => new Date(timestamp).toLocaleDateString( locale, { diff --git a/packages/ui/stories/components/albumGrid.stories.tsx b/packages/ui/stories/components/albumGrid.stories.tsx index 39b4f30dcb..2f3e75b9a4 100644 --- a/packages/ui/stories/components/albumGrid.stories.tsx +++ b/packages/ui/stories/components/albumGrid.stories.tsx @@ -41,7 +41,7 @@ export const WithAlbumPreview = () => ( tracklist: _(null).range(10).map(i => ( { name: `Test track ${Math.random() * i}`, - artist: { name: 'Test Artist' }, + artists: ['Test Artist'], album: 'Test album', position: i+1, duration: 100 diff --git a/packages/ui/stories/components/albumPreview.stories.tsx b/packages/ui/stories/components/albumPreview.stories.tsx index 7f8cdf10a2..966ec52972 100644 --- a/packages/ui/stories/components/albumPreview.stories.tsx +++ b/packages/ui/stories/components/albumPreview.stories.tsx @@ -26,7 +26,7 @@ export const Basic = ({ tracks: _(null).range(10).map(i => ( { name: `Test track ${i}`, - artist: { name: 'Test Artist' }, + artists: ['Test Artist'], album: 'Test album', position: i+1, duration: 100 @@ -53,7 +53,7 @@ export const NoCover = ({ tracks: _(null).range(10).map(i => ( { name: `Test track ${i}`, - artist: { name: 'Test Artist' }, + artists: ['Test Artist'], album: 'Test album', position: i+1, duration: 100 @@ -82,7 +82,7 @@ export const WithTrackButtons = ({ tracks: _(null).range(10).map(i => ( { name: `Test track ${i}`, - artist: { name: 'Test Artist' }, + artists: ['Test Artist'], album: 'Test album', position: i+1, duration: 100 diff --git a/packages/ui/stories/components/miniPlayer.stories.tsx b/packages/ui/stories/components/miniPlayer.stories.tsx index d4f26db321..002b05982e 100644 --- a/packages/ui/stories/components/miniPlayer.stories.tsx +++ b/packages/ui/stories/components/miniPlayer.stories.tsx @@ -10,7 +10,7 @@ export default { export const Example = () => { }} diff --git a/packages/ui/stories/components/playerBar.stories.tsx b/packages/ui/stories/components/playerBar.stories.tsx index bf829e27e7..892e61f546 100644 --- a/packages/ui/stories/components/playerBar.stories.tsx +++ b/packages/ui/stories/components/playerBar.stories.tsx @@ -22,7 +22,7 @@ export const DefaultStyle = () =>
{ }} queue={{ queueItems: [] }} diff --git a/packages/ui/stories/components/playlists.stories.tsx b/packages/ui/stories/components/playlists.stories.tsx index 82df1f6a45..2c57184431 100644 --- a/packages/ui/stories/components/playlists.stories.tsx +++ b/packages/ui/stories/components/playlists.stories.tsx @@ -15,20 +15,20 @@ const tracks = [ { position: 1, thumbnail: 'https://i.imgur.com/4euOws2.jpg', - artist: 'Test Artist', + artists: ['Test Artist'], title: 'Test Title', album: 'Test Album' }, { position: 2, thumbnail: 'https://i.imgur.com/4euOws2.jpg', - artist: 'Test Artist 2', + artists: ['Test Artist 2'], name: 'Test Title 2', album: 'Test Album' }, { position: 3, thumbnail: 'https://i.imgur.com/4euOws2.jpg', - artist: 'Test Artist 3', + artists: ['Test Artist 3'], name: 'Test Title 3', album: 'Test Album' } diff --git a/packages/ui/stories/components/queueItem.stories.tsx b/packages/ui/stories/components/queueItem.stories.tsx index 1d5955a08e..dc19edc893 100644 --- a/packages/ui/stories/components/queueItem.stories.tsx +++ b/packages/ui/stories/components/queueItem.stories.tsx @@ -10,7 +10,7 @@ const commonProps = { track: { thumbnail: 'https://i.imgur.com/4euOws2.jpg', name: 'Test track name', - artist: 'Test artist', + artists: ['Test artist'], stream: {} }, isCurrent: false, @@ -35,7 +35,7 @@ storiesOf('Components/Queue item', module) track={{ thumbnail: 'https://i.imgur.com/aVNWf3j.jpg', name: 'Small thumbnail', - artist: 'Test artist' + artists: ['Test artist'] }} />
@@ -67,7 +67,7 @@ storiesOf('Components/Queue item', module) track={{ thumbnail: 'https://i.imgur.com/koC6Otx.jpg', name: 'Test track name', - artist: 'Test artist' + artists: ['Test artist'] }} isCompact /> @@ -86,7 +86,7 @@ storiesOf('Components/Queue item', module)
diff --git a/packages/ui/stories/components/trackPopup.stories.tsx b/packages/ui/stories/components/trackPopup.stories.tsx index a5e03e10d7..dad6c5a190 100644 --- a/packages/ui/stories/components/trackPopup.stories.tsx +++ b/packages/ui/stories/components/trackPopup.stories.tsx @@ -9,7 +9,7 @@ export default { }; const track = { - artist: 'Test', + artists: ['Test'], title: 'Test title', thumb: 'https://i.imgur.com/4euOws2.jpg' }; diff --git a/packages/ui/stories/components/trackRow.stories.js b/packages/ui/stories/components/trackRow.stories.js index ae9f8cd8b5..c23f930e56 100644 --- a/packages/ui/stories/components/trackRow.stories.js +++ b/packages/ui/stories/components/trackRow.stories.js @@ -11,7 +11,7 @@ storiesOf('Components/Track row', module)
thumbnailHeader={} isTrackFavorite={ - (track: Track) => track.artist === 'Test Artist 2' + (track: Track) => _.isEqual(track.artists, ['Test Artist 2']) } playlists={playlists} strings={trackTableStrings} @@ -110,7 +111,7 @@ export const DragAndDrop = () => { positionHeader={} thumbnailHeader={} - isTrackFavorite={(track: Track) => track.artist === 'Test Artist 2'} + isTrackFavorite={(track: Track) => _.isEqual(track.artists, ['Test Artist 2'])} playlists={playlists} strings={trackTableStrings} onDragEnd={(result) => { @@ -147,7 +148,7 @@ export const ListeningHistory = () =>
{ }, diff --git a/packages/ui/test/playerBar.test.tsx b/packages/ui/test/playerBar.test.tsx index d576e56ec9..699fa17eff 100644 --- a/packages/ui/test/playerBar.test.tsx +++ b/packages/ui/test/playerBar.test.tsx @@ -11,7 +11,7 @@ makeSnapshotTest( timeToEnd: '2:43', fill: 66, track: 'Test song', - artist: 'Test artist', + artists: ['Test artist'], cover: 'https://i.imgur.com/4euOws2.jpg', volume: 60, queue: { queueItems: [] }, diff --git a/packages/ui/test/trackTable.test.tsx b/packages/ui/test/trackTable.test.tsx index 59060c21ee..17b14e9e99 100644 --- a/packages/ui/test/trackTable.test.tsx +++ b/packages/ui/test/trackTable.test.tsx @@ -1,3 +1,4 @@ +import { isEqual } from 'lodash'; import React from 'react'; import { Icon } from 'semantic-ui-react'; @@ -26,14 +27,14 @@ makeSnapshotTest( { position: 1, thumbnail: 'https://i.imgur.com/4euOws2.jpg', - artist: 'Test Artist', + artists: ['Test Artist'], title: 'Test Title', album: 'Test Album', duration: '1:00' }, { position: 2, thumbnail: 'https://i.imgur.com/4euOws2.jpg', - artist: 'Test Artist 2', + artists: ['Test Artist 2'], name: 'Test Title 2', album: 'Test Album', duration: '1:00' @@ -41,7 +42,7 @@ makeSnapshotTest( { position: 3, thumbnail: 'https://i.imgur.com/4euOws2.jpg', - artist: {name: 'Test Artist 3' }, + artists: ['Test Artist 3'], name: 'Test Title 3', album: 'Test Album', duration: '1:00' @@ -54,7 +55,7 @@ makeSnapshotTest( titleHeader: 'Title', durationHeader: 'Length', isTrackFavorite: - (track: Track) => track.artist === 'Test Artist 2' + (track: Track) => isEqual(track.artists, ['Test Artist 2']) }, '(Snapshot) Track table - example data with all rows' ); From e7b2e901dc109dd32d1ab1b1a64ea3cc7ed7e4e8 Mon Sep 17 00:00:00 2001 From: Lucki Date: Thu, 22 Aug 2024 16:06:54 +0200 Subject: [PATCH 02/22] Read autoradio artist into new artists array --- packages/app/app/containers/SoundContainer/autoradio.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/app/app/containers/SoundContainer/autoradio.js b/packages/app/app/containers/SoundContainer/autoradio.js index 0307f21493..40d19973f4 100644 --- a/packages/app/app/containers/SoundContainer/autoradio.js +++ b/packages/app/app/containers/SoundContainer/autoradio.js @@ -82,7 +82,13 @@ export function addAutoradioTrackToQueue (callProps) { if (selectedTrack === null) { return Promise.reject(new Error('No similar track or artist were found.')); } - return addToQueue(selectedTrack.artist, selectedTrack); + + // The API doesn't returns artist.name we have to convert to the new format + if (!selectedTrack.artists && selectedTrack.artist) { + selectedTrack.artists = _.isString(selectedTrack.artist) ? [selectedTrack.artist] : [selectedTrack.artist.name]; + } + + return addToQueue(selectedTrack.artists, selectedTrack); }) .catch(function (err) { logger.error('error', err); From dfca7221b73b2b98e962f07b5cdc15c9119f3401 Mon Sep 17 00:00:00 2001 From: Lucki Date: Thu, 22 Aug 2024 15:36:34 +0200 Subject: [PATCH 03/22] Copy old format history DB entries to new array --- .../ListeningHistoryView/ListeningHistorySection.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/app/app/components/ListeningHistoryView/ListeningHistorySection.tsx b/packages/app/app/components/ListeningHistoryView/ListeningHistorySection.tsx index 11c47acffe..b3fd2099ab 100644 --- a/packages/app/app/components/ListeningHistoryView/ListeningHistorySection.tsx +++ b/packages/app/app/components/ListeningHistoryView/ListeningHistorySection.tsx @@ -13,6 +13,12 @@ export type ListeningHistorySection = { export const ListeningHistorySection: React.FC = ({tracks}) => { const date = tracks?.[0].createdAt.toLocaleDateString(); + tracks = tracks?.map(track => { + // @ts-expect-error DB is not an array of artists yet + track.artists = [track.artist]; + return track; + }); + return
{date} From bac2ba39bc2a18cdde6a0030678ea1006e06bf36 Mon Sep 17 00:00:00 2001 From: Lucki Date: Tue, 20 Aug 2024 16:58:54 +0200 Subject: [PATCH 04/22] [Spotify] Adapt to new `artists` --- packages/core/src/plugins/meta/spotify.test.ts | 4 ++-- packages/core/src/plugins/meta/spotify.ts | 4 ++-- packages/core/src/rest/Spotify.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/core/src/plugins/meta/spotify.test.ts b/packages/core/src/plugins/meta/spotify.test.ts index 41a96b0ceb..ca046d69da 100644 --- a/packages/core/src/plugins/meta/spotify.test.ts +++ b/packages/core/src/plugins/meta/spotify.test.ts @@ -106,7 +106,7 @@ describe('SpotifyMetaProvider', () => { expect(result).toEqual([{ id: 'track-id', title: 'test track', - artist: 'test artist', + artists: ['test artist'], source: 'Spotify', thumb: 'thumbnail.jpg' }]); @@ -377,7 +377,7 @@ describe('SpotifyMetaProvider', () => { position: 1, name: 'test track', title: 'test track', - artist: 'test artist', + artists: ['test artist'], duration: 1, thumbnail: 'thumbnail.jpg' }] diff --git a/packages/core/src/plugins/meta/spotify.ts b/packages/core/src/plugins/meta/spotify.ts index bf5bb62c71..c7e8f62c2d 100644 --- a/packages/core/src/plugins/meta/spotify.ts +++ b/packages/core/src/plugins/meta/spotify.ts @@ -66,7 +66,7 @@ export class SpotifyMetaProvider extends MetaProvider { return { id: spotifyTrack.id, title: spotifyTrack.name, - artist: spotifyTrack.artists[0].name, + artists: spotifyTrack.artists.map(artist => artist.name), source: SearchResultsSource.Spotify, thumb }; @@ -167,7 +167,7 @@ export class SpotifyMetaProvider extends MetaProvider { year: result.release_date, tracklist: result.tracks.items.map(track => new Track({ title: track.name, - artist: result.artists[0].name, + artists: track.artists.map(artist => artist.name), duration: track.duration_ms/1000, position: track.track_number, thumbnail: thumb diff --git a/packages/core/src/rest/Spotify.ts b/packages/core/src/rest/Spotify.ts index 0d3ffbe3b3..4d62cd9771 100644 --- a/packages/core/src/rest/Spotify.ts +++ b/packages/core/src/rest/Spotify.ts @@ -248,7 +248,7 @@ export const mapSpotifyTrack = (track: SpotifyTrack): PlaylistTrack | null => { try { return { uuid: track.id, - artists: [track.artists[0].name], + artists: track.artists.map(artist => artist.name), name: track.name, album: track.album.name, thumbnail: thumb, From 5075fe70692753cf82dc359055f69f9912839e34 Mon Sep 17 00:00:00 2001 From: Lucki Date: Tue, 20 Aug 2024 17:03:41 +0200 Subject: [PATCH 05/22] [MusicBrainz] Adapt to new `artists` --- packages/core/src/plugins/meta/musicbrainz.ts | 2 +- packages/core/src/rest/Musicbrainz.ts | 2 +- packages/core/src/rest/Musicbrainz.types.ts | 5 +++++ 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/core/src/plugins/meta/musicbrainz.ts b/packages/core/src/plugins/meta/musicbrainz.ts index 086947afc5..1d9fc279bd 100644 --- a/packages/core/src/plugins/meta/musicbrainz.ts +++ b/packages/core/src/plugins/meta/musicbrainz.ts @@ -144,7 +144,7 @@ class MusicbrainzMetaProvider extends MetaProvider { tracklist: _.flatMap(releaseDetails.media, medium => _.map(medium.tracks, track => { const newtrack = new Track(); newtrack.ids[SearchResultsSource.Musicbrainz] = track.id; - newtrack.artist = artistName; + newtrack.artists = track['artist-credit'].map(artist => artist.name); newtrack.title = track.title; newtrack.duration = Math.ceil(track.length/1000); newtrack.position = track.position; diff --git a/packages/core/src/rest/Musicbrainz.ts b/packages/core/src/rest/Musicbrainz.ts index e2ed294cae..c30f017aa5 100644 --- a/packages/core/src/rest/Musicbrainz.ts +++ b/packages/core/src/rest/Musicbrainz.ts @@ -62,7 +62,7 @@ const getReleaseGroupDetails = (releaseGroupId: string): Promise => new Promise((resolve, reject) => { - nb.release(releaseId, { inc: 'artists+recordings+genres' }, (err, response) => { + nb.release(releaseId, { inc: 'artists+artist-credits+recordings+genres' }, (err, response) => { err ? reject(err) : resolve(response); }); }); diff --git a/packages/core/src/rest/Musicbrainz.types.ts b/packages/core/src/rest/Musicbrainz.types.ts index daa2050819..30293a6905 100644 --- a/packages/core/src/rest/Musicbrainz.types.ts +++ b/packages/core/src/rest/Musicbrainz.types.ts @@ -3,6 +3,10 @@ export type MusicbrainzArtist = { name: string; } +export type MusicBrainzArtistCredit = { + name: string; +} + export type MusicbrainzReleaseGroup = { id: string; title: string; @@ -15,6 +19,7 @@ export type MusicbrainzTrack = { number: string; position: number; title: string; + 'artist-credit': MusicBrainzArtistCredit[]; }; export type MusicbrainzMedia = { From ab6e0c8f475eea5b9cc3891418ce2f6205fad223 Mon Sep 17 00:00:00 2001 From: Lucki Date: Tue, 20 Aug 2024 17:19:19 +0200 Subject: [PATCH 06/22] [Discogs] Adapt to new `artists` --- packages/core/src/plugins/meta/discogs.ts | 8 ++++---- packages/core/src/rest/Discogs.types.ts | 4 ++++ 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/core/src/plugins/meta/discogs.ts b/packages/core/src/plugins/meta/discogs.ts index b60ade1f77..252368223c 100644 --- a/packages/core/src/plugins/meta/discogs.ts +++ b/packages/core/src/plugins/meta/discogs.ts @@ -106,13 +106,13 @@ class DiscogsMetaProvider extends MetaProvider { }; } - discogsTrackToGeneric(discogsTrack: DiscogsTrack, artist: string): Track { + discogsTrackToGeneric(discogsTrack: DiscogsTrack, albumArtist: string): Track { const track = new Track(); - track.artist = artist; + track.artists = discogsTrack.artists?.map(artist => artist.name) ?? [albumArtist]; track.title = discogsTrack.title; track.duration = discogsTrack.duration; track.position = discogsTrack.position; - track.extraArtists = _.map(discogsTrack.extraartists, 'name'); + track.artists = track.artists.concat(_.map(discogsTrack.extraartists, 'name')); track.type = discogsTrack.type_; return track; } @@ -153,7 +153,7 @@ class DiscogsMetaProvider extends MetaProvider { .then(response => response.json()) .then(json => { if (json.results) { - const artists = json.results.flatMap(item => + const artists = json.results.flatMap(item => (item.type === 'artist') ? [this.discogsArtistSearchResultToGeneric(item)] : [] ); diff --git a/packages/core/src/rest/Discogs.types.ts b/packages/core/src/rest/Discogs.types.ts index 660e1dfa80..04276e5224 100644 --- a/packages/core/src/rest/Discogs.types.ts +++ b/packages/core/src/rest/Discogs.types.ts @@ -104,6 +104,10 @@ export type DiscogsTrack = { duration: string; position: string; title: string; + artists?: { + name: string; + id: number; + }[]; extraartists?: { name: string; }[]; From ddf6d611bba8742731803d58e3a8bae7f6b59ab0 Mon Sep 17 00:00:00 2001 From: Lucki Date: Wed, 21 Aug 2024 00:22:15 +0200 Subject: [PATCH 07/22] Display artists in TrackRow when deviating from album artist --- .../app/app/components/AlbumView/index.tsx | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/packages/app/app/components/AlbumView/index.tsx b/packages/app/app/components/AlbumView/index.tsx index 0a22cb6319..5f3521018c 100644 --- a/packages/app/app/components/AlbumView/index.tsx +++ b/packages/app/app/components/AlbumView/index.tsx @@ -41,6 +41,28 @@ export const AlbumView: React.FC = ({ const displayPlaylistCreationDialog = () => setIsCreatePlaylistDialogOpen(true); const hidePlaylistCreationDialog = () => setIsCreatePlaylistDialogOpen(false); + const displayArtistColumn = () => { + if (!album.artist || !album.tracklist) { + return false; + } + + for (const track of album.tracklist) { + if (!track.artists) { + return false; + } + + if (track.artists.length > 1) { + return true; + } + + if (album.artist !== track.artists?.[0]) { + return true; + } + } + + return false; + }; + const release_date: Date = new Date(album.year); return
@@ -178,7 +200,7 @@ export const AlbumView: React.FC = ({ tracks={album.tracklist} displayDeleteButton={false} displayThumbnail={false} - displayArtist={false} + displayArtist={displayArtistColumn()} displayAlbum={false} />
From 6479d6f428af548216929d462612dcb774f357f0 Mon Sep 17 00:00:00 2001 From: Lucki Date: Wed, 21 Aug 2024 02:26:08 +0200 Subject: [PATCH 08/22] Adjust reading the config into the new format --- packages/app/app/actions/downloads.ts | 51 +++++++++++++++++----- packages/app/app/actions/favorites.ts | 61 +++++++++++++++++++++++---- packages/app/app/actions/playlists.ts | 35 ++++++++++++++- 3 files changed, 127 insertions(+), 20 deletions(-) diff --git a/packages/app/app/actions/downloads.ts b/packages/app/app/actions/downloads.ts index bbedabd0d8..54f2c8ffe2 100644 --- a/packages/app/app/actions/downloads.ts +++ b/packages/app/app/actions/downloads.ts @@ -25,7 +25,7 @@ const changePropertyForItem = ({downloads, uuid, propertyName='status', value}:C export const readDownloads = createStandardAction(DownloadActionTypes.READ_DOWNLOADS).map( () => { - const downloads: Download[] = store.get('downloads'); + const downloads: Download[] = getDownloadsBackwardsCompatible(); return { payload: downloads }; } ); @@ -33,7 +33,7 @@ export const readDownloads = createStandardAction(DownloadActionTypes.READ_DOWNL export const addToDownloads = createStandardAction(DownloadActionTypes.ADD_TO_DOWNLOADS).map( (_:StreamProvider[], track: Track) => { const clonedTrack: TrackItem = safeAddUuid(getTrackItem(track)); - let downloads: Download[] = store.get('downloads'); + let downloads: Download[] = getDownloadsBackwardsCompatible(); const existingTrack = downloads.find(({track}) => { const {name, artists} = track; @@ -61,7 +61,7 @@ export const addToDownloads = createStandardAction(DownloadActionTypes.ADD_TO_DO export const onDownloadStarted = createStandardAction(DownloadActionTypes.DOWNLOAD_STARTED).map( (uuid: string) => { - const downloads: Download[] = store.get('downloads'); + const downloads: Download[] = getDownloadsBackwardsCompatible(); const payload = changePropertyForItem({ downloads, uuid, @@ -74,7 +74,7 @@ export const onDownloadStarted = createStandardAction(DownloadActionTypes.DOWNLO export const onDownloadPause = createStandardAction(DownloadActionTypes.DOWNLOAD_PAUSED).map( (uuid: string) => { - const downloads: Download[] = store.get('downloads'); + const downloads: Download[] = getDownloadsBackwardsCompatible(); const payload = changePropertyForItem({ downloads, uuid, @@ -87,7 +87,7 @@ export const onDownloadPause = createStandardAction(DownloadActionTypes.DOWNLOA export const onDownloadResume = createStandardAction(DownloadActionTypes.DOWNLOAD_RESUMED).map( (uuid: string) => { - const downloads: Download[] = store.get('downloads'); + const downloads: Download[] = getDownloadsBackwardsCompatible(); const payload = changePropertyForItem({ downloads, uuid, @@ -102,7 +102,7 @@ export const onDownloadResume = createStandardAction(DownloadActionTypes.DOWNLOA export const onDownloadProgress = createStandardAction(DownloadActionTypes.DOWNLOAD_PROGRESS).map( (uuid: string, progress: number) => { - const downloads = store.get('downloads'); + const downloads = getDownloadsBackwardsCompatible(); let payload = changePropertyForItem({ downloads, uuid, @@ -123,7 +123,7 @@ export const onDownloadProgress = createStandardAction(DownloadActionTypes.DOWNL export const onDownloadError = createStandardAction(DownloadActionTypes.DOWNLOAD_ERROR).map( (uuid: string) => { - const downloads: Download[] = store.get('downloads'); + const downloads: Download[] = getDownloadsBackwardsCompatible(); const payload = changePropertyForItem({ downloads, uuid, @@ -138,7 +138,7 @@ export const onDownloadError = createStandardAction(DownloadActionTypes.DOWNLOAD export const onDownloadRemoved = createStandardAction(DownloadActionTypes.DOWNLOAD_REMOVED).map( (uuid: string) => { - const downloads: Download[] = store.get('downloads'); + const downloads: Download[] = getDownloadsBackwardsCompatible(); const filteredTracks = downloads.filter(item => item.track.uuid !== uuid); return { payload: filteredTracks @@ -147,7 +147,7 @@ export const onDownloadRemoved = createStandardAction(DownloadActionTypes.DOWNLO export const onDownloadFinished = createStandardAction(DownloadActionTypes.DOWNLOAD_FINISHED).map( (uuid: string) => { - const downloads: Download[] = store.get('downloads'); + const downloads: Download[] = getDownloadsBackwardsCompatible(); const payload = changePropertyForItem({ downloads, uuid, @@ -161,7 +161,7 @@ export const onDownloadFinished = createStandardAction(DownloadActionTypes.DOWNL export const clearFinishedDownloads = createStandardAction(DownloadActionTypes.CLEAR_FINISHED_DOWNLOADS).map( () => { - const downloads: Download[] = store.get('downloads'); + const downloads: Download[] = getDownloadsBackwardsCompatible(); const filteredTracks = downloads.filter(( item ) => item.status !== DownloadStatus.FINISHED && item.status !== DownloadStatus.ERROR @@ -171,3 +171,34 @@ export const clearFinishedDownloads = createStandardAction(DownloadActionTypes.C payload: filteredTracks }; }); + +/** +* Helper function to read the old track format into the new format. +* +* `Track.artist` and `Track.extraArtists` are written into {@link Track.artists} +*/ +function getDownloadsBackwardsCompatible(): Download[] { + const downloads: Download[] = store.get('downloads'); + + downloads.forEach(download => { + // @ts-expect-error For backwards compatibility we're trying to parse an invalid field + if (download.track.artists || !download.track.artist) { + return; + } + + // @ts-expect-error For backwards compatibility we're trying to parse an invalid field + if (download.track.artist) { + // @ts-expect-error For backwards compatibility we're trying to parse an invalid field + download.track.artists = _.isString(download.track.artist) ? [download.track.artist] : [download.track.artist.name]; + } + + // Assuming we have `extraArtists` on a track, we must had an `artist` which + // was already saved into `artists`, so this `track.artists` shouldn't be undefined + // @ts-expect-error For backwards compatibility we're trying to parse an invalid field + download.track.extraArtists?.forEach(artist => { + download.track.artists.push(artist); + }); + }); + + return downloads; +} diff --git a/packages/app/app/actions/favorites.ts b/packages/app/app/actions/favorites.ts index 1fb8f246ca..7e4f91f314 100644 --- a/packages/app/app/actions/favorites.ts +++ b/packages/app/app/actions/favorites.ts @@ -17,7 +17,7 @@ export const ADD_FAVORITE_ARTIST = 'ADD_FAVORITE_ARTIST'; export const REMOVE_FAVORITE_ARTIST = 'REMOVE_FAVORITE_ARTIST'; export function readFavorites() { - const favorites = store.get('favorites'); + const favorites = getFavoritesBackwardsCompatible(); return { type: READ_FAVORITES, payload: favorites @@ -27,7 +27,7 @@ export function readFavorites() { export function addFavoriteTrack(track) { const clonedTrack = flow(safeAddUuid, getTrackItem)(track); - const favorites = store.get('favorites'); + const favorites = getFavoritesBackwardsCompatible(); const filteredTracks = favorites.tracks.filter(t => !areTracksEqualByName(t, track)); favorites.tracks = [...filteredTracks, omit(clonedTrack, 'streams')]; @@ -42,7 +42,7 @@ export function addFavoriteTrack(track) { const bulkAddFavoriteTracksAction = createStandardAction(BULK_ADD_FAVORITE_TRACKS)(); export const bulkAddFavoriteTracks = (tracks: Track[]) => { - const favorites = store.get('favorites'); + const favorites = getFavoritesBackwardsCompatible(); favorites.tracks = unionWith(favorites.tracks, tracks, areTracksEqualByName); store.set('favorites', favorites); @@ -50,7 +50,7 @@ export const bulkAddFavoriteTracks = (tracks: Track[]) => { }; export function removeFavoriteTrack(track) { - const favorites = store.get('favorites'); + const favorites = getFavoritesBackwardsCompatible(); favorites.tracks = favorites.tracks.filter(t => !areTracksEqualByName(t, track)); store.set('favorites', favorites); @@ -62,7 +62,7 @@ export function removeFavoriteTrack(track) { } export function addFavoriteAlbum(album) { - const favorites = store.get('favorites'); + const favorites = getFavoritesBackwardsCompatible(); favorites.albums = _.concat(favorites.albums, album); store.set('favorites', favorites); @@ -73,7 +73,7 @@ export function addFavoriteAlbum(album) { } export function removeFavoriteAlbum(album) { - const favorites = store.get('favorites'); + const favorites = getFavoritesBackwardsCompatible(); _.remove(favorites.albums, { artist: album.artist, title: album.title @@ -88,7 +88,7 @@ export function removeFavoriteAlbum(album) { export function addFavoriteArtist(artist) { - const favorites = store.get('favorites'); + const favorites = getFavoritesBackwardsCompatible(); const savedArtist = { id: artist.id, name: artist.name, @@ -107,7 +107,7 @@ export function addFavoriteArtist(artist) { } export function removeFavoriteArtist(artist) { - const favorites = store.get('favorites'); + const favorites = getFavoritesBackwardsCompatible(); _.remove(favorites.artists, { id: artist.id, name: artist.name @@ -119,3 +119,48 @@ export function removeFavoriteArtist(artist) { payload: favorites }; } + +/** +* Helper function to read the old track format into the new format. +* +* `Track.artist` and `Track.extraArtists` are written into {@link Track.artists} +*/ +function getFavoritesBackwardsCompatible() { + const favorites = store.get('favorites'); + + favorites.tracks?.forEach(track => { + if (track.artists || !track.artist) { + return; + } + + if (track.artist) { + track.artists = _.isString(track.artist) ? [track.artist] : [track.artist.name]; + } + + // Assuming we have `extraArtists` on a track, we must had an `artist` which + // was already saved into `artists`, so this `track.artists` shouldn't be undefined + track.extraArtists?.forEach(artist => { + track.artists.push(artist); + }); + }); + + favorites.albums?.forEach(album => { + album.tracklist?.forEach(track => { + if (track.artists || !track.artist) { + return; + } + + if (track.artist) { + track.artists = _.isString(track.artist) ? [track.artist] : [track.artist.name]; + } + + // Assuming we have `extraArtists` on a track, we must had an `artist` which + // was already saved into `artists`, so this `track.artists` shouldn't be undefined + track.extraArtists?.forEach(artist => { + track.artists.push(artist); + }); + }); + }); + + return favorites; +} diff --git a/packages/app/app/actions/playlists.ts b/packages/app/app/actions/playlists.ts index 6b0f2ce14c..de806eea0b 100644 --- a/packages/app/app/actions/playlists.ts +++ b/packages/app/app/actions/playlists.ts @@ -55,7 +55,7 @@ export const loadLocalPlaylists = () => dispatch => { dispatch(loadLocalPlaylistsAction.request()); try { - const playlists: Playlist[] = store.get('playlists'); + const playlists: Playlist[] = getPlaylistsBackwardsCompatible(); dispatch(loadLocalPlaylistsAction.success(isEmpty(playlists) ? [] : playlists)); } catch (error) { dispatch(loadLocalPlaylistsAction.failure()); @@ -147,7 +147,7 @@ export function addPlaylistFromFile(filePath, t) { throw new Error('missing tracks or name'); } - let playlists = store.get('playlists') || []; + let playlists = getPlaylistsBackwardsCompatible() || []; const playlist = PlaylistHelper.formatPlaylistForStorage(name, tracks, v4(), source); if (!(tracks?.length > 0)) { @@ -167,3 +167,34 @@ export function addPlaylistFromFile(filePath, t) { }); }; } + +/** +* Helper function to read the old track format into the new format. +* +* `Track.artist` and `Track.extraArtists` are written into {@link Track.artists} +*/ +function getPlaylistsBackwardsCompatible(): Playlist[] { + const playlists: Playlist[] = store.get('playlists'); + + playlists?.forEach(playlist => { + playlist.tracks?.forEach(track => { + // @ts-expect-error For backwards compatibility we're trying to parse an invalid field + if (track.artists || !track.artist) { + // New format already present, do nothing + return; + } + + // @ts-expect-error For backwards compatibility we're trying to parse an invalid field + track.artists = _.isString(track.artist) ? [track.artist] : [track.artist.name]; + + // Assuming we have `extraArtists` on a track, we must had an `artist` which + // was already saved into `artists`, so this `track.artists` shouldn't be undefined + // @ts-expect-error For backwards compatibility we're trying to parse an invalid field + track.extraArtists?.forEach(artist => { + track.artists.push(artist); + }); + }); + }); + + return playlists; +} From ca09d7ca6bb12404cb3a8041fee06ef2e6ea97b4 Mon Sep 17 00:00:00 2001 From: Lucki Date: Tue, 27 Aug 2024 01:24:34 +0200 Subject: [PATCH 09/22] [WIP] fixup! Adjust reading the config into the new format --- packages/app/app/actions/downloads.ts | 26 +++------------- packages/app/app/actions/favorites.ts | 45 ++++++--------------------- packages/app/app/actions/helpers.ts | 18 +++++++++++ packages/app/app/actions/playlists.ts | 27 +++------------- 4 files changed, 36 insertions(+), 80 deletions(-) diff --git a/packages/app/app/actions/downloads.ts b/packages/app/app/actions/downloads.ts index 54f2c8ffe2..b41f339c6d 100644 --- a/packages/app/app/actions/downloads.ts +++ b/packages/app/app/actions/downloads.ts @@ -2,7 +2,7 @@ import _ from 'lodash'; import { isEqual } from 'lodash'; import { store, StreamProvider } from '@nuclear/core'; import { getTrackItem } from '@nuclear/ui'; -import { safeAddUuid } from './helpers'; +import { rewriteTrackArtists, safeAddUuid } from './helpers'; import { Download, DownloadStatus, Track, TrackItem } from '@nuclear/ui/lib/types'; import { createStandardAction } from 'typesafe-actions'; import { Download as DownloadActionTypes } from './actionTypes'; @@ -179,26 +179,8 @@ export const clearFinishedDownloads = createStandardAction(DownloadActionTypes.C */ function getDownloadsBackwardsCompatible(): Download[] { const downloads: Download[] = store.get('downloads'); - - downloads.forEach(download => { - // @ts-expect-error For backwards compatibility we're trying to parse an invalid field - if (download.track.artists || !download.track.artist) { - return; - } - - // @ts-expect-error For backwards compatibility we're trying to parse an invalid field - if (download.track.artist) { - // @ts-expect-error For backwards compatibility we're trying to parse an invalid field - download.track.artists = _.isString(download.track.artist) ? [download.track.artist] : [download.track.artist.name]; - } - - // Assuming we have `extraArtists` on a track, we must had an `artist` which - // was already saved into `artists`, so this `track.artists` shouldn't be undefined - // @ts-expect-error For backwards compatibility we're trying to parse an invalid field - download.track.extraArtists?.forEach(artist => { - download.track.artists.push(artist); - }); + return downloads.map(download => { + download.track = rewriteTrackArtists(download.track); + return download; }); - - return downloads; } diff --git a/packages/app/app/actions/favorites.ts b/packages/app/app/actions/favorites.ts index 7e4f91f314..09ce525359 100644 --- a/packages/app/app/actions/favorites.ts +++ b/packages/app/app/actions/favorites.ts @@ -1,9 +1,10 @@ import _, { flow, omit, unionWith } from 'lodash'; -import { store, Track } from '@nuclear/core'; +import { store, Album, Artist, Track } from '@nuclear/core'; import { areTracksEqualByName, getTrackItem } from '@nuclear/ui'; -import { safeAddUuid } from './helpers'; +import { rewriteTrackArtists, safeAddUuid } from './helpers'; import { createStandardAction } from 'typesafe-actions'; +import { TrackItem } from '@nuclear/ui/lib/types'; export const READ_FAVORITES = 'READ_FAVORITES'; export const ADD_FAVORITE_TRACK = 'ADD_FAVORITE_TRACK'; @@ -126,41 +127,13 @@ export function removeFavoriteArtist(artist) { * `Track.artist` and `Track.extraArtists` are written into {@link Track.artists} */ function getFavoritesBackwardsCompatible() { - const favorites = store.get('favorites'); - - favorites.tracks?.forEach(track => { - if (track.artists || !track.artist) { - return; - } - - if (track.artist) { - track.artists = _.isString(track.artist) ? [track.artist] : [track.artist.name]; - } - - // Assuming we have `extraArtists` on a track, we must had an `artist` which - // was already saved into `artists`, so this `track.artists` shouldn't be undefined - track.extraArtists?.forEach(artist => { - track.artists.push(artist); - }); - }); + const favorites: { albums?: Album[], artists?: Artist[], tracks?: Track[] } = store.get('favorites'); - favorites.albums?.forEach(album => { - album.tracklist?.forEach(track => { - if (track.artists || !track.artist) { - return; - } - - if (track.artist) { - track.artists = _.isString(track.artist) ? [track.artist] : [track.artist.name]; - } - - // Assuming we have `extraArtists` on a track, we must had an `artist` which - // was already saved into `artists`, so this `track.artists` shouldn't be undefined - track.extraArtists?.forEach(artist => { - track.artists.push(artist); - }); - }); + favorites.tracks = favorites.tracks?.map(rewriteTrackArtists); + favorites.albums = favorites.albums?.map(album => { + album.tracklist = album.tracklist?.map(rewriteTrackArtists); + return album; }); - return favorites; + return favorites as any; } diff --git a/packages/app/app/actions/helpers.ts b/packages/app/app/actions/helpers.ts index 5714d9e413..862b6da36f 100644 --- a/packages/app/app/actions/helpers.ts +++ b/packages/app/app/actions/helpers.ts @@ -1,6 +1,8 @@ import { v4 } from 'uuid'; import _ from 'lodash'; import { createAction } from 'redux-actions'; +import { PlaylistTrack, Track } from '@nuclear/core'; +import { TrackItem } from '@nuclear/ui/lib/types'; type ActionsBasicType = { [k: string]: (...payload: any) => any; @@ -22,3 +24,19 @@ export const safeAddUuid = track => { return clonedTrack; }; + +export function rewriteTrackArtists(track: T): T { + const clonedTrack = _.cloneDeep(track); + + // @ts-expect-error For backwards compatibility we're trying to parse an invalid field + if (clonedTrack.artists || !clonedTrack.artist) { + return clonedTrack; + } + + // @ts-expect-error For backwards compatibility we're trying to parse an invalid field + clonedTrack.artists = _.isString(clonedTrack.artist) ? [clonedTrack.artist] : [clonedTrack.artist.name]; + // @ts-expect-error For backwards compatibility we're trying to parse an invalid field + clonedTrack.artists = clonedTrack.artists.concat(clonedTrack.extraArtists?.map(artist)); + + return clonedTrack; +} diff --git a/packages/app/app/actions/playlists.ts b/packages/app/app/actions/playlists.ts index de806eea0b..1f6a0c3de5 100644 --- a/packages/app/app/actions/playlists.ts +++ b/packages/app/app/actions/playlists.ts @@ -3,7 +3,7 @@ import { v4 } from 'uuid'; import { remote } from 'electron'; import { createAsyncAction, createStandardAction } from 'typesafe-actions'; -import { store, PlaylistHelper, Playlist, PlaylistTrack, rest } from '@nuclear/core'; +import { store, PlaylistHelper, Playlist, PlaylistTrack, Track, rest } from '@nuclear/core'; import { GetPlaylistsByUserIdResponseBody } from '@nuclear/core/src/rest/Nuclear/Playlists.types'; import { ErrorBody } from '@nuclear/core/src/rest/Nuclear/types'; @@ -18,6 +18,7 @@ import { success, error } from './toasts'; import { IdentityStore } from '../reducers/nuclear/identity'; import { PlaylistsStore } from '../reducers/playlists'; import { isEmpty } from 'lodash'; +import { rewriteTrackArtists } from './helpers'; export const updatePlaylistsAction = createStandardAction(Playlists.UPDATE_LOCAL_PLAYLISTS)(); @@ -175,26 +176,8 @@ export function addPlaylistFromFile(filePath, t) { */ function getPlaylistsBackwardsCompatible(): Playlist[] { const playlists: Playlist[] = store.get('playlists'); - - playlists?.forEach(playlist => { - playlist.tracks?.forEach(track => { - // @ts-expect-error For backwards compatibility we're trying to parse an invalid field - if (track.artists || !track.artist) { - // New format already present, do nothing - return; - } - - // @ts-expect-error For backwards compatibility we're trying to parse an invalid field - track.artists = _.isString(track.artist) ? [track.artist] : [track.artist.name]; - - // Assuming we have `extraArtists` on a track, we must had an `artist` which - // was already saved into `artists`, so this `track.artists` shouldn't be undefined - // @ts-expect-error For backwards compatibility we're trying to parse an invalid field - track.extraArtists?.forEach(artist => { - track.artists.push(artist); - }); - }); + return playlists.map(playlist => { + playlist.tracks = playlist.tracks?.map(rewriteTrackArtists); + return playlist; }); - - return playlists; } From 129ee02ce85b68d8f15c46e6d3b3c90da4bda1bc Mon Sep 17 00:00:00 2001 From: Lucki Date: Tue, 27 Aug 2024 01:49:27 +0200 Subject: [PATCH 10/22] [WIP] Adjust `NuclearMeta` --- packages/core/src/types/index.ts | 2 +- packages/main/src/controllers/player.ts | 2 +- packages/main/src/services/discord/index.ts | 2 +- .../main/src/services/local-library/index.ts | 4 ++-- packages/main/src/services/trayMenu/index.ts | 4 ++-- packages/scanner/index.d.ts | 2 +- packages/scanner/src/js.rs | 24 ++++++++++++++++++- packages/scanner/src/metadata.rs | 12 +++++----- packages/scanner/src/scanner.rs | 8 +++---- 9 files changed, 41 insertions(+), 19 deletions(-) diff --git a/packages/core/src/types/index.ts b/packages/core/src/types/index.ts index 39af57fb66..271a9d8c8c 100644 --- a/packages/core/src/types/index.ts +++ b/packages/core/src/types/index.ts @@ -14,7 +14,7 @@ export interface NuclearStatus { export interface NuclearMeta { uuid: string; - artist: string; + artists: string[]; streams?: Array<{ duration: number }>; name?: string; position?: number; diff --git a/packages/main/src/controllers/player.ts b/packages/main/src/controllers/player.ts index dbf35ae098..5027721927 100644 --- a/packages/main/src/controllers/player.ts +++ b/packages/main/src/controllers/player.ts @@ -76,7 +76,7 @@ class IpcPlayer { return; } - this.window.setTitle(`${arg.artist} - ${arg.name} - Nuclear Music Player`); + this.window.setTitle(`${arg.artists[0]} - ${arg.name} - Nuclear Music Player`); this.systemApi.sendMetadata && this.systemApi.sendMetadata(arg); this.discord.trackChange(arg); this.trayMenu.update({track: arg}); diff --git a/packages/main/src/services/discord/index.ts b/packages/main/src/services/discord/index.ts index 1eb74784ee..aac2173e0d 100644 --- a/packages/main/src/services/discord/index.ts +++ b/packages/main/src/services/discord/index.ts @@ -82,7 +82,7 @@ class Discord { this.baseStart = Date.now(); this.pausedTotal = 0; this.activity = { - details: `${track.artist} - ${track.name}`, + details: `${track.artists[0]} - ${track.name}`, startTimestamp: this.baseStart, largeImageKey: 'logo' }; diff --git a/packages/main/src/services/local-library/index.ts b/packages/main/src/services/local-library/index.ts index e875e6c05f..26ec74457d 100644 --- a/packages/main/src/services/local-library/index.ts +++ b/packages/main/src/services/local-library/index.ts @@ -74,7 +74,7 @@ class LocalLibrary { }(), position: common.track.no, album: common.album, - artist: common.artist || 'unknown', + artists: [common.artist] || ['unknown'], imageData: common.picture && common.picture[0].data, lastScanned: +Date.now() }; @@ -124,7 +124,7 @@ class LocalLibrary { if (data && data.recordings && data.recordings.length) { meta.name = data.recordings[0].name; - meta.artist = data.recordings[0].artists?.[0].name || 'unknown'; + meta.artists = data.recordings[0].artists?.map(artist => artist.name) || ['unknown']; } if (!meta.name) { diff --git a/packages/main/src/services/trayMenu/index.ts b/packages/main/src/services/trayMenu/index.ts index 2329dac1dd..3cb3a44e4d 100644 --- a/packages/main/src/services/trayMenu/index.ts +++ b/packages/main/src/services/trayMenu/index.ts @@ -64,7 +64,7 @@ class TrayMenu { enabled: false }); template.push({ - label: `by ${this.playerContext.track.artist}`, + label: `by ${this.playerContext.track.artists[0]}`, enabled: false }); } else { @@ -137,7 +137,7 @@ class TrayMenu { getToolTipString() { return this.playerContext.track ? - `${this.playerContext.isPlaying ? 'Playing: ' : ''} ${this.playerContext.track.name} - ${this.playerContext.track.artist}` : + `${this.playerContext.isPlaying ? 'Playing: ' : ''} ${this.playerContext.track.name} - ${this.playerContext.track.artists[0]}` : this.config.title ; } diff --git a/packages/scanner/index.d.ts b/packages/scanner/index.d.ts index 2ddbc0d594..d4b3b8785d 100644 --- a/packages/scanner/index.d.ts +++ b/packages/scanner/index.d.ts @@ -1,6 +1,6 @@ export type LocalTrack = { uuid: string; - artist: string; + artists: string[]; title?: string; album?: string; duration?: number; diff --git a/packages/scanner/src/js.rs b/packages/scanner/src/js.rs index 3ad62539d5..5c972671a1 100644 --- a/packages/scanner/src/js.rs +++ b/packages/scanner/src/js.rs @@ -56,12 +56,34 @@ pub fn set_optional_field_buffer( } } +pub fn set_optional_field_string_array( + cx: &mut FunctionContext, + obj: &mut Handle, + field_name: &str, + value: Option>, +) { + match value { + Some(v) => { + let field_value = cx.empty_array(); + for (i, str) in v.iter().enumerate() { + let js_str = cx.string(&str); + field_value.set(cx, i as u32, js_str).unwrap(); + } + obj.set(cx, field_name, field_value).unwrap(); + } + None => { + let undefined = cx.undefined(); + obj.set(cx, field_name, undefined).unwrap(); + } + } +} + pub fn set_properties_from_metadata( cx: &mut FunctionContext, obj: &mut Handle, metadata: &AudioMetadata, ) { - set_optional_field_str(cx, obj, "artist", metadata.artist.clone()); + set_optional_field_string_array(cx, obj, "artists", metadata.artists.clone()); set_optional_field_str(cx, obj, "title", metadata.title.clone()); set_optional_field_str(cx, obj, "album", metadata.album.clone()); diff --git a/packages/scanner/src/metadata.rs b/packages/scanner/src/metadata.rs index 44528d7f97..8834c72998 100644 --- a/packages/scanner/src/metadata.rs +++ b/packages/scanner/src/metadata.rs @@ -22,7 +22,7 @@ use crate::{ #[derive(Default, Debug, Clone, Builder)] #[builder(setter(strip_option))] pub struct AudioMetadata { - pub artist: Option, + pub artists: Option>, pub title: Option, pub album: Option, pub duration: Option, @@ -35,7 +35,7 @@ pub struct AudioMetadata { impl AudioMetadata { pub fn new() -> Self { Self { - artist: None, + artists: None, title: None, album: None, duration: None, @@ -74,7 +74,7 @@ impl MetadataExtractor for Mp3MetadataExtractor { let tag = tag.unwrap(); let mut metadata = AudioMetadata::new(); - metadata.artist = tag.artist().map(|s| s.to_string()); + metadata.artists = tag.artist().map(|s| s.to_string()).and_then(|s| Some(vec![s])); metadata.title = tag.title().map(|s| s.to_string()); metadata.album = tag.album().map(|s| s.to_string()); let duration = mp3_duration::from_path(&path).map(|duration| duration.as_secs() as u32); @@ -139,7 +139,7 @@ impl MetadataExtractor for FlacMetadataExtractor { let tag = tag.unwrap(); let mut metadata = AudioMetadata::new(); - metadata.artist = Self::extract_string_metadata(&tag, "ARTIST", Some("ALBUMARTIST")); + metadata.artists = Self::extract_string_metadata(&tag, "ARTIST", Some("ALBUMARTIST")).and_then(|s| Some(vec![s])); metadata.title = Self::extract_string_metadata(&tag, "TITLE", None); metadata.album = Self::extract_string_metadata(&tag, "ALBUM", None); let total_samples = tag.get_streaminfo().unwrap().total_samples; @@ -213,7 +213,7 @@ impl MetadataExtractor for OggMetadataExtractor { metadata.title = Some(tag.value.to_string()); } Some(StandardTagKey::Artist) => { - metadata.artist = Some(tag.value.to_string()); + metadata.artists = Some(vec![tag.value.to_string()]); } Some(StandardTagKey::Album) => { metadata.album = Some(tag.value.to_string()); @@ -257,7 +257,7 @@ impl MetadataExtractor for Mp4MetadataExtractor { let tag = tag.unwrap(); let mut metadata = AudioMetadata::new(); - metadata.artist = tag.artist().map(|s| s.to_string()); + metadata.artists = tag.artist().map(|s| s.to_string()).and_then(|s| Some(vec![s])); metadata.title = tag.title().map(|s| s.to_string()); metadata.album = tag.album().map(|s| s.to_string()); metadata.duration = tag.duration().map(|d| d.as_secs() as u32); diff --git a/packages/scanner/src/scanner.rs b/packages/scanner/src/scanner.rs index fa4d9f6fce..998d9360b7 100644 --- a/packages/scanner/src/scanner.rs +++ b/packages/scanner/src/scanner.rs @@ -44,7 +44,7 @@ where if let Ok(mut metadata) = metadata { metadata.title = metadata.title.clone().or(Some(filename.clone())); - metadata.artist = metadata.artist.clone().or(Some("unknown".to_string())); + metadata.artists = metadata.artists.clone().or(Some(vec!["unknown".to_string()])); Ok(LocalTrack { uuid: Uuid::new_v4().to_string(), metadata: metadata, @@ -112,7 +112,7 @@ mod tests { pub fn test_extractor_from_path(_path: &str) -> Option> { Some(Box::new(TestMetadataExtractor::new( AudioMetadataBuilder::default() - .artist("Test Artist".to_string()) + .artists(vec!["Test Artist".to_string()]) .title("Test Title".to_string()) .album("Test Album".to_string()) .duration(10) @@ -131,7 +131,7 @@ mod tests { let thumbnails_dir: String = "tests/thumbnails".to_string(); let local_track = visit_file(path, test_extractor_from_path, &thumbnails_dir).unwrap(); assert_eq!(local_track.filename, "test.mp3"); - assert_eq!(local_track.metadata.artist, Some("Test Artist".to_string())); + assert_eq!(local_track.metadata.artists, Some(vec!["Test Artist".to_string()])); assert_eq!(local_track.metadata.title, Some("Test Title".to_string())); assert_eq!(local_track.metadata.album, Some("Test Album".to_string())); assert_eq!(local_track.metadata.duration, Some(10)); @@ -157,7 +157,7 @@ mod tests { let local_track = visit_file(path, test_extractor_from_path_no_metadata, &thumbnails_dir).unwrap(); assert_eq!(local_track.filename, "test.mp3"); - assert_eq!(local_track.metadata.artist, Some("unknown".to_string())); + assert_eq!(local_track.metadata.artists, Some(vec!["unknown".to_string()])); assert_eq!(local_track.metadata.title, Some("test.mp3".to_string())); assert_eq!(local_track.metadata.album, None); assert_eq!(local_track.metadata.duration, None); From 0ecdce92e3499cd529beac2c8fad4f6d7d0e12b1 Mon Sep 17 00:00:00 2001 From: nukeop <12746779+nukeop@users.noreply.github.com> Date: Wed, 16 Oct 2024 00:47:15 +0200 Subject: [PATCH 11/22] Reset xesam:artist to an array --- packages/main/src/services/@linux/system-api.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/main/src/services/@linux/system-api.ts b/packages/main/src/services/@linux/system-api.ts index 3fce4073ff..5f25860bd5 100644 --- a/packages/main/src/services/@linux/system-api.ts +++ b/packages/main/src/services/@linux/system-api.ts @@ -200,7 +200,7 @@ class LinuxMediaService extends MprisService implements NuclearApi { clearTrackList() { this.tracks = []; this.sendMetadata({ - artist: 'Nuclear', + artists: ['Nuclear'], name: '', uuid: '0', thumbnail: '', From a9906b3320d2e1b89b6472b9b65a690bcd04ead5 Mon Sep 17 00:00:00 2001 From: nukeop <12746779+nukeop@users.noreply.github.com> Date: Wed, 16 Oct 2024 02:15:48 +0200 Subject: [PATCH 12/22] Adapt GridTrackTable to multiple artists per track --- packages/ui/lib/components/GridTrackTable/index.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/ui/lib/components/GridTrackTable/index.tsx b/packages/ui/lib/components/GridTrackTable/index.tsx index eafc22967e..1a748a03a1 100644 --- a/packages/ui/lib/components/GridTrackTable/index.tsx +++ b/packages/ui/lib/components/GridTrackTable/index.tsx @@ -117,9 +117,7 @@ export const GridTrackTable = ({ displayArtist && { id: TrackTableColumn.Artist, Header: ({ column }) => , - accessor: (track: T) => isString(track.artist) - ? track.artist - : track.artist.name, + accessor: (track: T) => track.artists?.[0], Cell: TextCell, enableSorting: true, columnWidth: '6em' From 2a4e2c89a231b99f6a720b9d2e7231b62ac963fc Mon Sep 17 00:00:00 2001 From: nukeop <12746779+nukeop@users.noreply.github.com> Date: Thu, 17 Oct 2024 01:04:27 +0200 Subject: [PATCH 13/22] Avoid mutation when converting a playlist to the new format --- packages/app/app/actions/playlists.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/app/app/actions/playlists.ts b/packages/app/app/actions/playlists.ts index 1f6a0c3de5..e615ea4ede 100644 --- a/packages/app/app/actions/playlists.ts +++ b/packages/app/app/actions/playlists.ts @@ -177,7 +177,9 @@ export function addPlaylistFromFile(filePath, t) { function getPlaylistsBackwardsCompatible(): Playlist[] { const playlists: Playlist[] = store.get('playlists'); return playlists.map(playlist => { - playlist.tracks = playlist.tracks?.map(rewriteTrackArtists); - return playlist; + return { + ...playlist, + tracks: playlist.tracks?.map(rewriteTrackArtists) + }; }); } From 8efdb5290e1589f29056b7d2d4a66121b54db049 Mon Sep 17 00:00:00 2001 From: nukeop <12746779+nukeop@users.noreply.github.com> Date: Thu, 17 Oct 2024 01:09:12 +0200 Subject: [PATCH 14/22] Avoid mutation when remapping downloads and favorites --- packages/app/app/actions/downloads.ts | 8 ++++---- packages/app/app/actions/favorites.ts | 16 ++++++++-------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/app/app/actions/downloads.ts b/packages/app/app/actions/downloads.ts index e132c58d33..b9ed857411 100644 --- a/packages/app/app/actions/downloads.ts +++ b/packages/app/app/actions/downloads.ts @@ -188,10 +188,10 @@ export const clearFinishedDownloads = createStandardAction(DownloadActionTypes.C */ function getDownloadsBackwardsCompatible(): Download[] { const downloads: Download[] = store.get('downloads'); - return downloads.map(download => { - download.track = rewriteTrackArtists(download.track); - return download; - }); + return downloads.map(download => ({ + ...download, + track: rewriteTrackArtists(download.track) + })); } export const resumeDownloads = createStandardAction(DownloadActionTypes.RESUME_DOWNLOADS).map( diff --git a/packages/app/app/actions/favorites.ts b/packages/app/app/actions/favorites.ts index 09ce525359..228eee8ece 100644 --- a/packages/app/app/actions/favorites.ts +++ b/packages/app/app/actions/favorites.ts @@ -4,7 +4,6 @@ import { areTracksEqualByName, getTrackItem } from '@nuclear/ui'; import { rewriteTrackArtists, safeAddUuid } from './helpers'; import { createStandardAction } from 'typesafe-actions'; -import { TrackItem } from '@nuclear/ui/lib/types'; export const READ_FAVORITES = 'READ_FAVORITES'; export const ADD_FAVORITE_TRACK = 'ADD_FAVORITE_TRACK'; @@ -129,11 +128,12 @@ export function removeFavoriteArtist(artist) { function getFavoritesBackwardsCompatible() { const favorites: { albums?: Album[], artists?: Artist[], tracks?: Track[] } = store.get('favorites'); - favorites.tracks = favorites.tracks?.map(rewriteTrackArtists); - favorites.albums = favorites.albums?.map(album => { - album.tracklist = album.tracklist?.map(rewriteTrackArtists); - return album; - }); - - return favorites as any; + return { + ...favorites, + tracks: favorites.tracks?.map(rewriteTrackArtists), + albums: favorites.albums?.map(album => ({ + ...album, + tracklist: album.tracklist?.map(rewriteTrackArtists) + })) + }; } From 6a214c5f0a072ed9408bdd01100b374cf51b117a Mon Sep 17 00:00:00 2001 From: nukeop <12746779+nukeop@users.noreply.github.com> Date: Thu, 17 Oct 2024 01:18:16 +0200 Subject: [PATCH 15/22] Use new artist format in GridTrackTable tests --- packages/ui/test/gridTrackTable.test.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/ui/test/gridTrackTable.test.tsx b/packages/ui/test/gridTrackTable.test.tsx index f546b3263e..affeeb1179 100644 --- a/packages/ui/test/gridTrackTable.test.tsx +++ b/packages/ui/test/gridTrackTable.test.tsx @@ -24,14 +24,14 @@ makeSnapshotTest(GridTrackTable, { { position: 1, thumbnail: 'https://i.imgur.com/4euOws2.jpg', - artist: 'Test Artist', + artists: ['Test Artist'], title: 'Test Title', album: 'Test Album', duration: '1:00' }, { position: 2, thumbnail: 'https://i.imgur.com/4euOws2.jpg', - artist: 'Test Artist 2', + artists: ['Test Artist 2', 'Test Artist 4'], name: 'Test Title 2', album: 'Test Album', duration: '1:00' @@ -39,7 +39,7 @@ makeSnapshotTest(GridTrackTable, { { position: 3, thumbnail: 'https://i.imgur.com/4euOws2.jpg', - artist: {name: 'Test Artist 3' }, + artists: ['Test Artist 3'], name: 'Test Title 3', album: 'Test Album', duration: '1:00' From dd1fd00f560f7aaf28c000f8ff8925aabac6c371 Mon Sep 17 00:00:00 2001 From: nukeop <12746779+nukeop@users.noreply.github.com> Date: Thu, 17 Oct 2024 01:19:53 +0200 Subject: [PATCH 16/22] Use new metadata format in linux system api (mpris) --- packages/main/src/services/@linux/system-api.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/main/src/services/@linux/system-api.ts b/packages/main/src/services/@linux/system-api.ts index 64e570aded..bc5cb7d7c8 100644 --- a/packages/main/src/services/@linux/system-api.ts +++ b/packages/main/src/services/@linux/system-api.ts @@ -144,7 +144,7 @@ class LinuxMediaService extends MprisService implements NuclearApi { this.metadata['mpris:length'] = 0; this.window.send(IpcEvents.STOP); this.sendMetadata({ - artist: 'Nuclear', + artists: ['Nuclear'], name: '', uuid: '0', thumbnail: '', From 37a607a254482973b00db1963df3539c3600d3ca Mon Sep 17 00:00:00 2001 From: nukeop <12746779+nukeop@users.noreply.github.com> Date: Sun, 20 Oct 2024 02:46:34 +0200 Subject: [PATCH 17/22] Favorites multi-artist support, snapshot update --- packages/app/app/actions/favorites.ts | 25 +++++++++++++------ .../AlbumViewContainer.test.tsx.snap | 8 +++--- .../ArtistViewContainer.test.tsx.snap | 8 +++--- .../containers/ArtistViewContainer/hooks.ts | 3 ++- .../DashboardContainer.test.tsx.snap | 6 ++--- .../DeezerPlaylistAdapter.test.tsx.snap | 6 ++--- .../FavoritesContainer.tracks.test.tsx.snap | 12 ++++----- .../LibraryViewContainer.test.tsx.snap | 8 +++--- .../PlaylistViewContainer.test.tsx.snap | 6 ++--- .../SpotifyPlaylistAdapter.test.tsx.snap | 6 ++--- .../lib/components/GridTrackTable/index.tsx | 4 +-- .../lib/components/GridTrackTable/styles.scss | 6 ++--- 12 files changed, 53 insertions(+), 45 deletions(-) diff --git a/packages/app/app/actions/favorites.ts b/packages/app/app/actions/favorites.ts index 228eee8ece..63e3984267 100644 --- a/packages/app/app/actions/favorites.ts +++ b/packages/app/app/actions/favorites.ts @@ -1,10 +1,18 @@ import _, { flow, omit, unionWith } from 'lodash'; -import { store, Album, Artist, Track } from '@nuclear/core'; +import { store, Track } from '@nuclear/core'; import { areTracksEqualByName, getTrackItem } from '@nuclear/ui'; import { rewriteTrackArtists, safeAddUuid } from './helpers'; import { createStandardAction } from 'typesafe-actions'; +export type FavoriteArtist = { + id: string; + name: string; + source: string; + coverImage: string; + thumb: string; +} + export const READ_FAVORITES = 'READ_FAVORITES'; export const ADD_FAVORITE_TRACK = 'ADD_FAVORITE_TRACK'; export const REMOVE_FAVORITE_TRACK = 'REMOVE_FAVORITE_TRACK'; @@ -29,9 +37,11 @@ export function addFavoriteTrack(track) { const favorites = getFavoritesBackwardsCompatible(); const filteredTracks = favorites.tracks.filter(t => !areTracksEqualByName(t, track)); - favorites.tracks = [...filteredTracks, omit(clonedTrack, 'streams')]; - store.set('favorites', favorites); + store.set('favorites', { + ...favorites, + tracks: [...filteredTracks, omit(clonedTrack, 'streams')] + }); return { type: ADD_FAVORITE_TRACK, @@ -39,7 +49,7 @@ export function addFavoriteTrack(track) { }; } -const bulkAddFavoriteTracksAction = createStandardAction(BULK_ADD_FAVORITE_TRACKS)(); +const bulkAddFavoriteTracksAction = createStandardAction(BULK_ADD_FAVORITE_TRACKS)>(); export const bulkAddFavoriteTracks = (tracks: Track[]) => { const favorites = getFavoritesBackwardsCompatible(); @@ -86,8 +96,7 @@ export function removeFavoriteAlbum(album) { }; } -export function addFavoriteArtist(artist) { - +export function addFavoriteArtist(artist: FavoriteArtist) { const favorites = getFavoritesBackwardsCompatible(); const savedArtist = { id: artist.id, @@ -97,7 +106,7 @@ export function addFavoriteArtist(artist) { thumb: artist.thumb }; - favorites.artists = _.concat(favorites.artists || [], savedArtist); + favorites.artists = [...(favorites.artists ?? []), savedArtist]; store.set('favorites', favorites); return { @@ -126,7 +135,7 @@ export function removeFavoriteArtist(artist) { * `Track.artist` and `Track.extraArtists` are written into {@link Track.artists} */ function getFavoritesBackwardsCompatible() { - const favorites: { albums?: Album[], artists?: Artist[], tracks?: Track[] } = store.get('favorites'); + const favorites = store.get('favorites'); return { ...favorites, diff --git a/packages/app/app/containers/AlbumViewContainer/__snapshots__/AlbumViewContainer.test.tsx.snap b/packages/app/app/containers/AlbumViewContainer/__snapshots__/AlbumViewContainer.test.tsx.snap index e3d17fecb1..d9dca45540 100644 --- a/packages/app/app/containers/AlbumViewContainer/__snapshots__/AlbumViewContainer.test.tsx.snap +++ b/packages/app/app/containers/AlbumViewContainer/__snapshots__/AlbumViewContainer.test.tsx.snap @@ -131,7 +131,7 @@ exports[`Album view container should display an album 1`] = `
{ const dispatch = useDispatch(); @@ -42,7 +43,7 @@ export const useArtistViewProps = () => { const isFavorite = getIsFavorite(artist, favoriteArtists); const addFavoriteArtist = useCallback(async () => { - dispatch(FavoritesActions.addFavoriteArtist(artist)); + dispatch(FavoritesActions.addFavoriteArtist(artist as FavoriteArtist)); }, [artist, dispatch]); const removeFavoriteArtist = useCallback(async () => { diff --git a/packages/app/app/containers/DashboardContainer/__snapshots__/DashboardContainer.test.tsx.snap b/packages/app/app/containers/DashboardContainer/__snapshots__/DashboardContainer.test.tsx.snap index 2a74d9665a..7380df863f 100644 --- a/packages/app/app/containers/DashboardContainer/__snapshots__/DashboardContainer.test.tsx.snap +++ b/packages/app/app/containers/DashboardContainer/__snapshots__/DashboardContainer.test.tsx.snap @@ -574,7 +574,7 @@ exports[`Dashboard container should display top tracks after going to top tracks
({ id: TrackTableColumn.Selection, Header: SelectionHeader, Cell: SelectionCell, - columnWidth: '7.5em' + columnWidth: '3em' }, displayPosition && { id: TrackTableColumn.Position, @@ -120,7 +120,7 @@ export const GridTrackTable = ({ accessor: (track: T) => track.artists?.[0], Cell: TextCell, enableSorting: true, - columnWidth: '6em' + columnWidth: 'minmax(8em, 1fr)' }, displayAlbum && { id: TrackTableColumn.Album, diff --git a/packages/ui/lib/components/GridTrackTable/styles.scss b/packages/ui/lib/components/GridTrackTable/styles.scss index 117ad3fe58..1d7a233726 100644 --- a/packages/ui/lib/components/GridTrackTable/styles.scss +++ b/packages/ui/lib/components/GridTrackTable/styles.scss @@ -215,12 +215,10 @@ .play_button { display: none; - // Manual adjustments to center the icon - padding-top: 0.75em !important; - i.play.icon { // Manual adjustments to center the icon - margin-left: 0.06em !important; + margin-left: 0.1em !important; + margin-top: 0.05em !important; } } } From 97d248873ee06504a853f96b48562baa51822e62 Mon Sep 17 00:00:00 2001 From: nukeop <12746779+nukeop@users.noreply.github.com> Date: Mon, 21 Oct 2024 02:25:00 +0200 Subject: [PATCH 18/22] Fix add to favorites action --- packages/app/app/actions/favorites.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/app/app/actions/favorites.ts b/packages/app/app/actions/favorites.ts index 63e3984267..f6f647f38a 100644 --- a/packages/app/app/actions/favorites.ts +++ b/packages/app/app/actions/favorites.ts @@ -38,10 +38,8 @@ export function addFavoriteTrack(track) { const favorites = getFavoritesBackwardsCompatible(); const filteredTracks = favorites.tracks.filter(t => !areTracksEqualByName(t, track)); - store.set('favorites', { - ...favorites, - tracks: [...filteredTracks, omit(clonedTrack, 'streams')] - }); + favorites.tracks = [...filteredTracks, omit(clonedTrack, 'streams')]; + store.set('favorites', favorites); return { type: ADD_FAVORITE_TRACK, From 84b72160ffbfcb16a1de0de645367a37416443f1 Mon Sep 17 00:00:00 2001 From: nukeop <12746779+nukeop@users.noreply.github.com> Date: Mon, 21 Oct 2024 02:26:37 +0200 Subject: [PATCH 19/22] Update grid track table snapshots --- .../ui/test/__snapshots__/gridTrackTable.test.tsx.snap | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/ui/test/__snapshots__/gridTrackTable.test.tsx.snap b/packages/ui/test/__snapshots__/gridTrackTable.test.tsx.snap index d759a8cf26..4af760c761 100644 --- a/packages/ui/test/__snapshots__/gridTrackTable.test.tsx.snap +++ b/packages/ui/test/__snapshots__/gridTrackTable.test.tsx.snap @@ -15,7 +15,7 @@ exports[`(Snapshot) Grid track table - empty should render correctly 1`] = `
Date: Mon, 28 Oct 2024 01:53:11 +0100 Subject: [PATCH 20/22] Rewrite SoundContainer in Typescript --- packages/app/app/App.js | 4 +- packages/app/app/actions/scrobbling.ts | 6 +- .../containers/SoundContainer/autoradio.js | 30 +- .../app/containers/SoundContainer/index.js | 294 ------------------ .../app/containers/SoundContainer/index.tsx | 191 ++++++++++++ packages/app/app/reducers/queue.ts | 1 + packages/app/app/selectors/equalizer.ts | 3 + packages/app/app/selectors/player.ts | 3 + 8 files changed, 218 insertions(+), 314 deletions(-) delete mode 100644 packages/app/app/containers/SoundContainer/index.js create mode 100644 packages/app/app/containers/SoundContainer/index.tsx create mode 100644 packages/app/app/selectors/equalizer.ts diff --git a/packages/app/app/App.js b/packages/app/app/App.js index a46f75d732..da5acc3926 100644 --- a/packages/app/app/App.js +++ b/packages/app/app/App.js @@ -37,7 +37,7 @@ import PlayerBarContainer from './containers/PlayerBarContainer'; import MiniPlayerContainer from './containers/MiniPlayerContainer'; import IpcContainer from './containers/IpcContainer'; -import SoundContainer from './containers/SoundContainer'; +import { SoundContainer } from './containers/SoundContainer'; import ToastContainer from './containers/ToastContainer'; import ShortcutsContainer from './containers/ShortcutsContainer'; import ErrorBoundary from './containers/ErrorBoundary'; @@ -78,7 +78,7 @@ class App extends React.PureComponent { if (e.button === 1) { e.preventDefault(); } - } + } updateConnectivityStatus = (isConnected) => { this.props.actions.changeConnectivity(isConnected); } diff --git a/packages/app/app/actions/scrobbling.ts b/packages/app/app/actions/scrobbling.ts index 8283b727c4..bb0988ad74 100644 --- a/packages/app/app/actions/scrobbling.ts +++ b/packages/app/app/actions/scrobbling.ts @@ -50,7 +50,7 @@ export function lastFmConnectAction() { }; } -export function lastFmLoginAction(authToken) { +export function lastFmLoginAction(authToken: string) { return dispatch => { dispatch({ type: 'FAV_IMPORT_INIT', @@ -103,7 +103,7 @@ export function disableScrobbling() { }; } -export function scrobbleAction(artist, track, session) { +export function scrobbleAction(artist: string, track: string, session: string) { return dispatch => { lastfm.scrobble(artist, track, session) .then(() => { @@ -115,7 +115,7 @@ export function scrobbleAction(artist, track, session) { }; } -export function updateNowPlayingAction(artist, track, session) { +export function updateNowPlayingAction(artist:string, track: string, session: string) { return dispatch => { lastfm.updateNowPlaying(artist, track, session) .then(() => { diff --git a/packages/app/app/containers/SoundContainer/autoradio.js b/packages/app/app/containers/SoundContainer/autoradio.js index 40d19973f4..760491706b 100644 --- a/packages/app/app/containers/SoundContainer/autoradio.js +++ b/packages/app/app/containers/SoundContainer/autoradio.js @@ -51,7 +51,7 @@ let similarTracksResultsLimit = 10; */ let autoradioArtistDeviation = 0.20; -function computeParameters (crazinessScore = 10) { +function computeParameters(crazinessScore = 10) { autoradioArtistDeviation = crazinessScore / 100; similarTracksResultsLimit = crazinessScore; autoradioImpactingTrackNumber = 101 - crazinessScore; @@ -66,7 +66,7 @@ let props; * random track to play. * It will remove all tracks which are already present in the queue. */ -export function addAutoradioTrackToQueue (callProps) { +export function addAutoradioTrackToQueue(callProps) { props = callProps; const currentSong = props.queue.queueItems[props.queue.currentSong]; computeParameters(props.settings.autoradioCraziness); @@ -95,7 +95,7 @@ export function addAutoradioTrackToQueue (callProps) { }); } -function getSimilarTracksToQueue (number) { +function getSimilarTracksToQueue(number) { const similarTracksPromises = []; for (let i = props.queue.currentSong; i >= Math.max(0, props.queue.currentSong - number); i--) { @@ -116,7 +116,7 @@ function getSimilarTracksToQueue (number) { }); } -function getScoredRandomTrack (tracks) { +function getScoredRandomTrack(tracks) { let sum = 0; const cumulativeBias = tracks.map(function (track) { sum += track.match; return sum; @@ -129,16 +129,16 @@ function getScoredRandomTrack (tracks) { return Promise.resolve(tracks[chosenIndex]); } -function getTrackNotInQueue (tracks, deviation) { +function getTrackNotInQueue(tracks, deviation) { const newtracks = tracks.filter((track) => !isTrackInQueue(track)); return getRandomElement(getArraySlice(newtracks, deviation)); } -function getArraySlice (arr, ratio) { +function getArraySlice(arr, ratio) { return arr.slice(0, Math.round((arr.length - 1) * ratio) + 1); } -function getNewTrack (getter, track) { +function getNewTrack(getter, track) { let getTrack; if (getter === 'track') { getTrack = getSimilarTracks(track); @@ -152,7 +152,7 @@ function getNewTrack (getter, track) { } // `track` format here is directly from the lastfm api -function isTrackInQueue (track) { +function isTrackInQueue(track) { const queue = props.queue.queueItems; for (const i in queue) { if (queue[i].artists.includes(track.artist.name) && queue[i].name === track.name) { @@ -162,7 +162,7 @@ function isTrackInQueue (track) { return false; } -function getSimilarTracks (currentSong, limit = 100) { +function getSimilarTracks(currentSong, limit = 100) { return lastfm.getSimilarTracks(currentSong.artists?.[0], currentSong.name, limit) .then(tracks => tracks.json()) .then(trackJson => { @@ -170,7 +170,7 @@ function getSimilarTracks (currentSong, limit = 100) { }); } -function getTracksFromSimilarArtist (artist) { +function getTracksFromSimilarArtist(artist) { return lastfm .getArtistInfo(artist) .then(artist => artist.json()) @@ -183,15 +183,15 @@ function getTracksFromSimilarArtist (artist) { .then(topTracks => _.get(topTracks, 'toptracks.track', [])); } -function getSimilarArtists (artistJson) { +function getSimilarArtists(artistJson) { return Promise.resolve(artistJson.artist.similar.artist); } -function getRandomElement (arr) { +function getRandomElement(arr) { return arr[Math.round(Math.random() * (arr.length - 1))]; } -function getArtistTopTracks (artist) { +function getArtistTopTracks(artist) { return lastfm .getArtistTopTracks(_.get(artist, 'name', artist)) .then(topTracks => { @@ -200,9 +200,9 @@ function getArtistTopTracks (artist) { }); } -function addToQueue (artists, track) { +function addToQueue(artists, track) { return new Promise((resolve) => { - props.actions.addToQueue({ + props.addToQueue({ artists, name: track.name, thumbnail: track.thumbnail ?? track.image[0]['#text'] ?? track.thumb diff --git a/packages/app/app/containers/SoundContainer/index.js b/packages/app/app/containers/SoundContainer/index.js deleted file mode 100644 index 43f39daa4e..0000000000 --- a/packages/app/app/containers/SoundContainer/index.js +++ /dev/null @@ -1,294 +0,0 @@ -import React from 'react'; -import { withRouter } from 'react-router-dom'; -import { connect } from 'react-redux'; -import { bindActionCreators } from 'redux'; -import { compose, withProps } from 'recompose'; -import Sound, { Volume, Equalizer, AnalyserByFrequency } from 'react-hifi'; -import logger from 'electron-timber'; -import { head } from 'lodash'; -import { IpcEvents, rest } from '@nuclear/core'; -import { post as mastodonPost } from '@nuclear/core/src/rest/Mastodon'; - -import * as SearchActions from '../../actions/search'; -import * as PlayerActions from '../../actions/player'; -import * as EqualizerActions from '../../actions/equalizer'; -import * as QueueActions from '../../actions/queue'; -import * as ScrobblingActions from '../../actions/scrobbling'; -import * as LyricsActions from '../../actions/lyrics'; -import * as VisualizerActions from '../../actions/visualizer'; -import { filterFrequencies } from '../../components/Equalizer/chart'; -import * as Autoradio from './autoradio'; -import VisualizerContainer from '../../containers/VisualizerContainer'; -import Normalizer from '../../components/Normalizer'; -import globals from '../../globals'; -import HlsPlayer from '../../components/HLSPlayer'; -import { ipcRenderer } from 'electron'; - -const lastfm = new rest.LastFmApi(globals.lastfmApiKey, globals.lastfmApiSecret); - -class SoundContainer extends React.Component { - constructor(props) { - super(props); - - this.handlePlaying = this.handlePlaying.bind(this); - this.handleFinishedPlaying = this.handleFinishedPlaying.bind(this); - this.handleLoading = this.handleLoading.bind(this); - this.handleLoaded = this.handleLoaded.bind(this); - this.handleError = this.handleError.bind(this); - this.soundRef = React.createRef(); - } - - handlePlaying(update) { - const seek = update.position; - const progress = (update.position / update.duration) * 100; - const rate = (this.props.player.playbackRate + 2) / 4; - this.props.actions.updatePlaybackProgress(progress, seek); - this.props.actions.updateStreamLoading(false); - - if (this.soundRef?.current?.audio){ - this.soundRef.current.audio.setAttribute('playbackRate', ''); - this.soundRef.current.audio.playbackRate = rate; - } - } - - handleLoading() { - this.props.actions.updateStreamLoading(true); - } - - handleLoaded() { - this.handleLoadLyrics(); - this.handleAutoRadio(); - this.props.actions.updateStreamLoading(false); - } - - handleLoadLyrics() { - const currentSong = this.props.queue.queueItems[ - this.props.queue.currentSong - ]; - - if (currentSong && typeof currentSong.lyrics === 'undefined') { - this.props.actions.lyricsSearch(currentSong); - } - } - - handleAutoRadio() { - if ( - this.props.settings.autoradio && - this.props.queue.currentSong === this.props.queue.queueItems.length - 1 - ) { - Autoradio.addAutoradioTrackToQueue(this.props); - } - } - - handleFinishedPlaying() { - if (this.props.settings['visualizer.shuffle']) { - this.props.actions.randomizePreset(); - } - - const currentSong = this.props.queue.queueItems[ - this.props.queue.currentSong - ]; - if ( - this.props.scrobbling.lastFmScrobblingEnabled && - this.props.scrobbling.lastFmSessionKey - ) { - this.props.actions.scrobbleAction( - currentSong.artist, - currentSong.title ?? currentSong.name, - this.props.scrobbling.lastFmSessionKey - ); - } - - if (this.props.settings.listeningHistory) { - ipcRenderer.send(IpcEvents.POST_LISTENING_HISTORY_ENTRY, { - artist: currentSong.artists?.[0], - title: currentSong.title ?? currentSong.name - }); - } - - if ( - this.props.settings.shuffleQueue || - this.props.queue.currentSong < this.props.queue.queueItems.length - 1 || - this.props.settings.loopAfterQueueEnd - ) { - this.props.actions.nextSong(); - } else { - this.props.actions.pausePlayback(false); - } - - if (this.props.settings.mastodonAccessToken && - this.props.settings.mastodonInstance) { - const selectedStreamUrl = this.props.currentStream?.originalUrl || ''; - let content = this.props.settings.mastodonPostFormat + ''; - content = content.replaceAll('{{artist}}', currentSong.artist); - content = content.replaceAll('{{title}}', currentSong.name); - content = content.replaceAll('{{url}}', selectedStreamUrl); - mastodonPost( - this.props.settings.mastodonInstance, - this.props.settings.mastodonAccessToken, - content - ); - } - } - - addAutoradioTrackToQueue() { - const currentSong = this.props.queue.queueItems[this.props.queue.currentSong]; - return lastfm - .getArtistInfo(currentSong.artist) - .then(artist => artist.json()) - .then(artistJson => this.getSimilarArtists(artistJson.artist)) - .then(similarArtists => this.getRandomElement(similarArtists)) - .then(selectedArtist => this.getArtistTopTracks(selectedArtist)) - .then(topTracks => this.getRandomElement(topTracks.toptracks.track)) - .then(track => { - return this.addToQueue(track.artists, track); - }); - } - - getSimilarArtists(artistJson) { - return new Promise((resolve) => { - resolve(artistJson.similar.artist); - }); - } - - getRandomElement(arr) { - const devianceParameter = 0.2; // We will select one of the 20% most similar artists - const randomElement = - arr[Math.round(Math.random() * (devianceParameter * (arr.length - 1)))]; - return new Promise((resolve) => resolve(randomElement)); - } - - getArtistTopTracks(artist) { - return lastfm - .getArtistTopTracks(artist.name) - .then(topTracks => topTracks.json()); - } - - addToQueue(artist, track) { - return new Promise((resolve) => { - this.props.actions.addToQueue({ - artists: [artist.name], - name: track.name, - thumbnail: track.thumbnail ?? track.image[0]['#text'] ?? track.thumb - }); - resolve(true); - }); - } - - handleError(err) { - logger.error(err.message); - const { queue } = this.props; - this.props.actions.removeFromQueue(queue.currentSong); - } - - shouldComponentUpdate(nextProps) { - const currentSong = nextProps.queue.queueItems[nextProps.queue.currentSong]; - - return ( - this.props.equalizer !== nextProps.equalizer || - this.props.queue.currentSong !== nextProps.queue.currentSong || - this.props.player.playbackStatus !== nextProps.player.playbackStatus || - this.props.player.seek !== nextProps.player.seek || - (Boolean(currentSong) && Boolean(currentSong.streams)) - ); - } - - isHlsStream(url) { - return /http.*?\.m3u8/g.test(url); - } - - render() { - const { queue, player, equalizer, actions, enableSpectrum, currentStream, location, defaultEqualizer } = this.props; - const currentTrack = queue.queueItems[queue.currentSong]; - const usedEqualizer = enableSpectrum ? equalizer : defaultEqualizer; - - return Boolean(currentStream) && (this.isHlsStream(currentStream.stream) ? ( - - ) : ( - - - - ({ - ...acc, - [freq]: usedEqualizer.values[idx] || 0 - }), {})} - preAmp={usedEqualizer.preAmp} - /> - - - - )); - } -} - -function mapStateToProps(state) { - return { - queue: state.queue, - plugins: state.plugin, - player: state.player, - scrobbling: state.scrobbling, - settings: state.settings, - equalizer: state.equalizer.presets[state.equalizer.selected], - defaultEqualizer: state.equalizer.presets.default, - enableSpectrum: state.equalizer.enableSpectrum - }; -} - -function mapDispatchToProps(dispatch) { - return { - actions: bindActionCreators( - Object.assign( - {}, - SearchActions, - PlayerActions, - QueueActions, - ScrobblingActions, - LyricsActions, - EqualizerActions, - VisualizerActions - ), - dispatch - ) - }; -} - -export default compose( - withRouter, - connect( - mapStateToProps, - mapDispatchToProps - ), - withProps(({ queue }) => ({ - currentTrack: queue.queueItems[queue.currentSong] - })), - withProps(({ currentTrack }) => ({ - currentStream: head(currentTrack?.streams) - })) -)(SoundContainer); diff --git a/packages/app/app/containers/SoundContainer/index.tsx b/packages/app/app/containers/SoundContainer/index.tsx new file mode 100644 index 0000000000..8c117d1e3e --- /dev/null +++ b/packages/app/app/containers/SoundContainer/index.tsx @@ -0,0 +1,191 @@ +import React, { ComponentProps, createRef, FC } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import Sound, { AnalyserByFrequency, Equalizer, Volume } from 'react-hifi'; +import { ipcRenderer } from 'electron'; +import logger from 'electron-timber'; +import { head } from 'lodash'; +import { IpcEvents, rest } from '@nuclear/core'; +import { post as mastodonPost } from '@nuclear/core/src/rest/Mastodon'; + +import * as Autoradio from './autoradio'; +import VisualizerContainer from '../VisualizerContainer'; +import * as PlayerActions from '../../actions/player'; +import * as EqualizerActions from '../../actions/equalizer'; +import * as QueueActions from '../../actions/queue'; +import * as ScrobblingActions from '../../actions/scrobbling'; +import * as LyricsActions from '../../actions/lyrics'; +import * as VisualizerActions from '../../actions/visualizer'; +import { filterFrequencies } from '../../components/Equalizer/chart'; +import Normalizer from '../../components/Normalizer'; +import HlsPlayer from '../../components/HLSPlayer'; +import globals from '../../globals'; +import { QueueItem } from '../../reducers/queue'; +import { queue as queueSelector } from '../../selectors/queue'; +import { playerStateSelector } from '../../selectors/player'; +import { scrobblingSelector } from '../../selectors/scrobbling'; +import { settingsSelector } from '../../selectors/settings'; +import { equalizerSelector } from '../../selectors/equalizer'; + +export const SoundContainer: FC = () => { + const soundRef = createRef(); + const dispatch = useDispatch(); + const queue = useSelector(queueSelector); + const player = useSelector(playerStateSelector); + const scrobbling = useSelector(scrobblingSelector); + const settings = useSelector(settingsSelector); + const equalizer = useSelector(equalizerSelector); + + const currentTrack = queue.queueItems[queue.currentSong]; + const currentStream = head(currentTrack?.streams); + const usedEqualizer = equalizer.enableSpectrum ? equalizer.presets[equalizer.selected] : equalizer.presets.default; + + const handlePlaying = ({position, duration}: {position: number; duration: number;}) => { + const seek = position; + const progress = (position / duration) * 100; + const rate = (player.playbackRate + 2) / 4; + dispatch(PlayerActions.updatePlaybackProgress(progress, seek)); + dispatch(PlayerActions.updateStreamLoading(false)); + + // Needed because this bug was present in the JS code I'm porting to TS. Can be resolved if we fix it in react-hifi + // @ts-expect-error Property 'audio' is private and only accessible within class 'Sound'. + const audio = soundRef?.current?.audio; + + if (audio) { + audio.setAttribute('playbackRate', ''); + audio.playbackRate = rate; + } + }; + + const handleFinishedPlaying = () => { + if (settings['visualizer.shuffle']) { + dispatch(VisualizerActions.randomizePreset()); + } + + // Needed for legacy reasons. Should be removed when the queue item types are straightened out + // @ts-expect-error Property 'title' does not exist on type 'QueueItem'.ts(2339) + const currentTrackTitle = currentTrack.title ?? currentTrack.name; + + if ( + scrobbling.lastFmScrobblingEnabled && + scrobbling.lastFmSessionKey + ) { + dispatch(ScrobblingActions.scrobbleAction( + currentTrack.artists[0], + currentTrackTitle, + scrobbling.lastFmSessionKey + )); + } + + if (settings.listeningHistory) { + ipcRenderer.send(IpcEvents.POST_LISTENING_HISTORY_ENTRY, { + artist: currentTrack.artists?.[0], + title: currentTrackTitle + }); + } + + if ( + settings.shuffleQueue || + queue.currentSong < queue.queueItems.length - 1 || + settings.loopAfterQueueEnd + ) { + dispatch(QueueActions.nextSong()); + } else { + dispatch(PlayerActions.pausePlayback(false)); + } + + if (settings.mastodonAccessToken && + settings.mastodonInstance) { + const selectedStreamUrl = currentStream?.originalUrl || ''; + let content = settings.mastodonPostFormat + ''; + content = content.replaceAll('{{artist}}', currentTrack.artists[0]); + content = content.replaceAll('{{title}}', currentTrack.name); + content = content.replaceAll('{{url}}', selectedStreamUrl); + mastodonPost( + settings.mastodonInstance, + settings.mastodonAccessToken, + content + ); + } + }; + + const handleLoading = () => { + dispatch(PlayerActions.updateStreamLoading(true)); + }; + + const handleLoaded = () => { + handleLoadLyrics(); + handleAutoRadio(); + dispatch(PlayerActions.updateStreamLoading(false)); + }; + + const handleLoadLyrics = () => { + dispatch(LyricsActions.lyricsSearch(currentTrack)); + }; + + const handleAutoRadio = () => { + if ( + settings.autoradio && + queue.currentSong === queue.queueItems.length - 1 + ) { + Autoradio.addAutoradioTrackToQueue({ queue, settings, addToQueue: (item: QueueItem) => dispatch(QueueActions.addToQueue(item)) }); + } + }; + + const handleError = (err: Error) => { + logger.error(err.message); + dispatch(QueueActions.removeFromQueue(queue.currentSong)); + }; + const isHlsStream = (url: string) => { + return /http.*?\.m3u8/g.test(url); + }; + + return ( + Boolean(currentStream) && (isHlsStream(currentStream.stream) ? ( + ['playStatus']} + onFinishedPlaying={handleFinishedPlaying} + muted={player.muted} + volume={player.volume} + /> + ) : ( + ['playStatus']} + onPlaying={handlePlaying} + onFinishedPlaying={handleFinishedPlaying} + onLoading={handleLoading} + onLoad={handleLoaded} + position={player.seek} + onError={handleError} + ref={soundRef} + > + + + < Equalizer + data={ + filterFrequencies.reduce((acc, freq, idx) => ({ + ...acc, + [freq]: usedEqualizer.values[idx] || 0 + }), {}) + } + preAmp={usedEqualizer.preAmp} + /> + EqualizerActions.setSpectrum(data)) + } + /> + + + )) + ); +}; + + diff --git a/packages/app/app/reducers/queue.ts b/packages/app/app/reducers/queue.ts index ce71857c2f..a0eb72295b 100644 --- a/packages/app/app/reducers/queue.ts +++ b/packages/app/app/reducers/queue.ts @@ -12,6 +12,7 @@ export type TrackStream = { title?: string; thumbnail?: string; stream?: string; + originalUrl?: string; skipSegments?: Segment[]; }; diff --git a/packages/app/app/selectors/equalizer.ts b/packages/app/app/selectors/equalizer.ts new file mode 100644 index 0000000000..49444541a2 --- /dev/null +++ b/packages/app/app/selectors/equalizer.ts @@ -0,0 +1,3 @@ +import { RootState } from '../reducers'; + +export const equalizerSelector = (s: RootState) => s.equalizer; diff --git a/packages/app/app/selectors/player.ts b/packages/app/app/selectors/player.ts index b9f3fa4b0f..845a98d0de 100644 --- a/packages/app/app/selectors/player.ts +++ b/packages/app/app/selectors/player.ts @@ -1,3 +1,4 @@ +import { RootState } from '../reducers'; import { createStateSelectors } from './helpers'; export const playerSelectors = createStateSelectors( @@ -12,3 +13,5 @@ export const playerSelectors = createStateSelectors( 'playbackRate' ] ); + +export const playerStateSelector = (s:RootState) => s.player; From 4dcbd5808064603306e0c2f8d9d1c97ca397f724 Mon Sep 17 00:00:00 2001 From: nukeop <12746779+nukeop@users.noreply.github.com> Date: Tue, 29 Oct 2024 23:35:29 +0100 Subject: [PATCH 21/22] Update last.fm action types --- .../Settings/Integrations/LastFmSocialIntegration.tsx | 7 ++++--- packages/app/app/components/Settings/index.tsx | 7 ++++--- .../app/containers/SoundContainer/SoundContainer.test.tsx | 2 +- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/app/app/components/Settings/Integrations/LastFmSocialIntegration.tsx b/packages/app/app/components/Settings/Integrations/LastFmSocialIntegration.tsx index f20b753ac2..07398fc0ae 100644 --- a/packages/app/app/components/Settings/Integrations/LastFmSocialIntegration.tsx +++ b/packages/app/app/components/Settings/Integrations/LastFmSocialIntegration.tsx @@ -9,15 +9,16 @@ import Spacer from '../../Spacer'; import SocialIntegration from '../SocialIntegration'; import styles from '../styles.scss'; +import { lastFmConnectAction, lastFmLoginAction, lastFmLogOut } from '../../../actions/scrobbling'; type LastFmSocialIntegrationProps = { actions: { fetchAllFmFavorites: React.MouseEventHandler; enableScrobbling: Function; disableScrobbling: Function; - lastFmConnectAction: React.MouseEventHandler; - lastFmLoginAction: React.MouseEventHandler; - lastFmLogOut: React.MouseEventHandler; + lastFmConnectAction: typeof lastFmConnectAction; + lastFmLoginAction: typeof lastFmLoginAction; + lastFmLogOut: typeof lastFmLogOut; }; scrobbling: ReturnType; importfavs: ReturnType; diff --git a/packages/app/app/components/Settings/index.tsx b/packages/app/app/components/Settings/index.tsx index 34c7ef8ad0..1cf19dc21c 100644 --- a/packages/app/app/components/Settings/index.tsx +++ b/packages/app/app/components/Settings/index.tsx @@ -15,6 +15,7 @@ import styles from './styles.scss'; import { LastFmSocialIntegration } from './Integrations/LastFmSocialIntegration'; import { MastodonSocialIntegration } from './Integrations/MastodonSocialIntegration'; import { RootState } from '../../reducers'; +import { lastFmConnectAction, lastFmLoginAction, lastFmLogOut } from '../../actions/scrobbling'; const volumeSliderColors = { fillColor: { r: 248, g: 248, b: 242, a: 1 }, @@ -30,9 +31,9 @@ export type SettingsProps = { fetchAllFmFavorites: React.MouseEventHandler; enableScrobbling: Function; disableScrobbling: Function; - lastFmConnectAction: React.MouseEventHandler; - lastFmLoginAction: React.MouseEventHandler; - lastFmLogOut: React.MouseEventHandler; + lastFmConnectAction: typeof lastFmConnectAction; + lastFmLoginAction: typeof lastFmLoginAction; + lastFmLogOut: typeof lastFmLogOut; }; mastodonActions: { registerNuclear: (instanceUrl: string) => void; diff --git a/packages/app/app/containers/SoundContainer/SoundContainer.test.tsx b/packages/app/app/containers/SoundContainer/SoundContainer.test.tsx index 9664707d2c..00d8ce1481 100644 --- a/packages/app/app/containers/SoundContainer/SoundContainer.test.tsx +++ b/packages/app/app/containers/SoundContainer/SoundContainer.test.tsx @@ -3,7 +3,7 @@ import { waitFor } from '@testing-library/react'; import { AnyProps, mountComponent } from '../../../test/testUtils'; import { buildStoreState } from '../../../test/storeBuilders'; -import SoundContainer from '.'; +import { SoundContainer } from '.'; jest.mock('react-hls-player', () => { return { From e2805bb63b57ff76f91c7e69ff924d71a8421a47 Mon Sep 17 00:00:00 2001 From: nukeop <12746779+nukeop@users.noreply.github.com> Date: Tue, 29 Oct 2024 23:41:06 +0100 Subject: [PATCH 22/22] Replace failing playlist --- packages/core/src/rest/youtube.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/rest/youtube.test.ts b/packages/core/src/rest/youtube.test.ts index 3f72ef840e..17e7a1161b 100644 --- a/packages/core/src/rest/youtube.test.ts +++ b/packages/core/src/rest/youtube.test.ts @@ -3,7 +3,7 @@ const youtubeService = rest.Youtube; const playlistLessThan100 = 'https://www.youtube.com/watch?v=TKYsuU86-DQ&list=PL0eyrZgxdwhwNC5ppZo_dYGVjerQY3xYU'; -const playlistGreaterThan100 = 'https://www.youtube.com/playlist?list=PLuUrokoVSxlcgocBXbDF76yWd3YKWpOH9'; +const playlistGreaterThan100 = 'https://www.youtube.com/playlist?list=PL8F6B0753B2CCA128'; describe('Youtube tests', () => { it('should able to get playlist less than 100 tracks', async () => {