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/downloads.ts b/packages/app/app/actions/downloads.ts index 56b3cd30c0..377ea7fc8b 100644 --- a/packages/app/app/actions/downloads.ts +++ b/packages/app/app/actions/downloads.ts @@ -1,7 +1,8 @@ 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'; @@ -24,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 }; } ); @@ -32,11 +33,11 @@ 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, artist} = track; - return artist === clonedTrack.artist && name === clonedTrack.name; + const {name, artists} = track; + return isEqual(artists, clonedTrack.artists) && name === clonedTrack.name; }); if (!existingTrack ){ @@ -61,7 +62,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 +75,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 +88,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, @@ -132,7 +133,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, @@ -147,7 +148,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 @@ -156,7 +157,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, @@ -170,7 +171,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 @@ -181,6 +182,19 @@ export const clearFinishedDownloads = createStandardAction(DownloadActionTypes.C }; }); +/** +* 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'); + return downloads.map(download => ({ + ...download, + track: rewriteTrackArtists(download.track) + })); +} + export const resumeDownloads = createStandardAction(DownloadActionTypes.RESUME_DOWNLOADS).map( () => { const downloads: Download[] = store.get('downloads'); diff --git a/packages/app/app/actions/favorites.ts b/packages/app/app/actions/favorites.ts index 08afcdc943..b899c878e4 100644 --- a/packages/app/app/actions/favorites.ts +++ b/packages/app/app/actions/favorites.ts @@ -2,11 +2,19 @@ import _, { flow, omit, unionWith } from 'lodash'; import { store, 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 { addToDownloads } from './downloads'; import StreamProviderPlugin from '@nuclear/core/src/plugins/streamProvider'; +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'; @@ -19,7 +27,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 @@ -29,10 +37,10 @@ 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')]; + favorites.tracks = [...filteredTracks, omit(clonedTrack, 'streams')]; store.set('favorites', favorites); const settings = store.get('settings'); @@ -50,10 +58,10 @@ 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 = store.get('favorites'); + const favorites = getFavoritesBackwardsCompatible(); favorites.tracks = unionWith(favorites.tracks, tracks, areTracksEqualByName); store.set('favorites', favorites); @@ -61,7 +69,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); @@ -73,7 +81,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); @@ -84,7 +92,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 @@ -97,9 +105,8 @@ export function removeFavoriteAlbum(album) { }; } -export function addFavoriteArtist(artist) { - - const favorites = store.get('favorites'); +export function addFavoriteArtist(artist: FavoriteArtist) { + const favorites = getFavoritesBackwardsCompatible(); const savedArtist = { id: artist.id, name: artist.name, @@ -108,7 +115,7 @@ export function addFavoriteArtist(artist) { thumb: artist.thumb }; - favorites.artists = _.concat(favorites.artists || [], savedArtist); + favorites.artists = [...(favorites.artists ?? []), savedArtist]; store.set('favorites', favorites); return { @@ -118,7 +125,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 @@ -130,3 +137,21 @@ 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'); + + return { + ...favorites, + tracks: favorites.tracks?.map(rewriteTrackArtists), + albums: favorites.albums?.map(album => ({ + ...album, + tracklist: album.tracklist?.map(rewriteTrackArtists) + })) + }; +} 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/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/playlists.ts b/packages/app/app/actions/playlists.ts index 6b0f2ce14c..e615ea4ede 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)(); @@ -55,7 +56,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 +148,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 +168,18 @@ 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'); + return playlists.map(playlist => { + return { + ...playlist, + tracks: playlist.tracks?.map(rewriteTrackArtists) + }; + }); +} 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/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/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/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} />
diff --git a/packages/app/app/components/ArtistView/PopularTracks/index.tsx b/packages/app/app/components/ArtistView/PopularTracks/index.tsx index f7fda12676..e93f8de7b1 100644 --- a/packages/app/app/components/ArtistView/PopularTracks/index.tsx +++ b/packages/app/app/components/ArtistView/PopularTracks/index.tsx @@ -62,7 +62,7 @@ const PopularTracks: React.FC = ({ .slice(0, Math.min(tracks.length, MAX_POPULAR_TRACKS_DISPLAYED)) .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 1eaacd5dd6..87be719f85 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/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} 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 e39df7c9fb..b313e3e0c8 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 ( ; 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/components/TagView/TagTopTracks/index.tsx b/packages/app/app/components/TagView/TagTopTracks/index.tsx index f325437633..637a8ec1a3 100644 --- a/packages/app/app/components/TagView/TagTopTracks/index.tsx +++ b/packages/app/app/components/TagView/TagTopTracks/index.tsx @@ -9,16 +9,14 @@ import TrackRow from '../../TrackRow'; import TrackTableContainer from '../../../containers/TrackTableContainer'; type TagTopTrack = { - artist: { - name: string; - }; + artists: string[]; name: string; image: { '#text': string }[]; }; type TagTopTracksProps = { tracks: TagTopTrack[]; - addToQueue: (track: { artist: string; name: string; thumbnail: string }) => 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/__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`] = `
{ 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 693eb45cdf..90c35b5006 100644 --- a/packages/app/app/containers/ArtistViewContainer/ArtistViewContainer.test.tsx +++ b/packages/app/app/containers/ArtistViewContainer/ArtistViewContainer.test.tsx @@ -79,7 +79,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' }) ]); @@ -93,9 +93,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); @@ -106,7 +104,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' }) ]); @@ -123,7 +121,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' }) ]); @@ -143,7 +141,7 @@ describe('Artist view container', () => { completion: 0, status: 'Waiting', track: expect.objectContaining({ - artist: 'test artist', + artists: ['test artist'], name: 'test artist top track 1' }) } @@ -162,7 +160,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' }) ]); @@ -180,7 +178,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' }) ]); @@ -193,15 +191,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/ArtistViewContainer/__snapshots__/ArtistViewContainer.test.tsx.snap b/packages/app/app/containers/ArtistViewContainer/__snapshots__/ArtistViewContainer.test.tsx.snap index 44fb3e8f53..e1e6c6fc03 100644 --- a/packages/app/app/containers/ArtistViewContainer/__snapshots__/ArtistViewContainer.test.tsx.snap +++ b/packages/app/app/containers/ArtistViewContainer/__snapshots__/ArtistViewContainer.test.tsx.snap @@ -126,7 +126,7 @@ exports[`Artist view container should display an artist 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/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/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
{ 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(); @@ -106,10 +106,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(); @@ -127,10 +127,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(); @@ -193,7 +193,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 }]); @@ -203,7 +203,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/FavoritesContainer/__snapshots__/FavoritesContainer.tracks.test.tsx.snap b/packages/app/app/containers/FavoritesContainer/__snapshots__/FavoritesContainer.tracks.test.tsx.snap index 94f4eefaee..2b912a94db 100644 --- a/packages/app/app/containers/FavoritesContainer/__snapshots__/FavoritesContainer.tracks.test.tsx.snap +++ b/packages/app/app/containers/FavoritesContainer/__snapshots__/FavoritesContainer.tracks.test.tsx.snap @@ -229,7 +229,7 @@ exports[`Favorite tracks view container should display favorite tracks 1`] = `
{ 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/LibraryViewContainer/__snapshots__/LibraryViewContainer.test.tsx.snap b/packages/app/app/containers/LibraryViewContainer/__snapshots__/LibraryViewContainer.test.tsx.snap index 183bfd3c3b..2f1553bd6c 100644 --- a/packages/app/app/containers/LibraryViewContainer/__snapshots__/LibraryViewContainer.test.tsx.snap +++ b/packages/app/app/containers/LibraryViewContainer/__snapshots__/LibraryViewContainer.test.tsx.snap @@ -837,7 +837,7 @@ exports[`Library view container should display local library in simple list mode
{ 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 306a779798..8aed939802 100644 --- a/packages/app/app/containers/PlayQueueContainer/PlayQueueContainer.test.tsx +++ b/packages/app/app/containers/PlayQueueContainer/PlayQueueContainer.test.tsx @@ -104,7 +104,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' }) @@ -129,7 +129,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' } @@ -152,7 +152,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' }) ]); @@ -175,15 +175,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' }) ]); @@ -203,15 +203,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' }) ]); @@ -239,11 +239,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 47b12774ce..300742a26e 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/PlaylistViewContainer/__snapshots__/PlaylistViewContainer.test.tsx.snap b/packages/app/app/containers/PlaylistViewContainer/__snapshots__/PlaylistViewContainer.test.tsx.snap index f18beda0d3..5d7fc5f4df 100644 --- a/packages/app/app/containers/PlaylistViewContainer/__snapshots__/PlaylistViewContainer.test.tsx.snap +++ b/packages/app/app/containers/PlaylistViewContainer/__snapshots__/PlaylistViewContainer.test.tsx.snap @@ -120,7 +120,7 @@ exports[`Playlist view container should display a playlist 1`] = `
, 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/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 { diff --git a/packages/app/app/containers/SoundContainer/autoradio.js b/packages/app/app/containers/SoundContainer/autoradio.js index 99e5f1176e..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); @@ -82,14 +82,20 @@ 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); }); } -function getSimilarTracksToQueue (number) { +function getSimilarTracksToQueue(number) { const similarTracksPromises = []; for (let i = props.queue.currentSong; i >= Math.max(0, props.queue.currentSong - number); i--) { @@ -110,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; @@ -123,21 +129,21 @@ 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); } else { - getTrack = getTracksFromSimilarArtist(track.artist); + getTrack = getTracksFromSimilarArtist(track.artists?.[0]); } return getTrack .then(similarTracks => { @@ -145,25 +151,26 @@ function getNewTrack (getter, track) { }); } -function isTrackInQueue (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; } } return false; } -function getSimilarTracks (currentSong, limit = 100) { - return lastfm.getSimilarTracks(currentSong.artist, currentSong.name, limit) +function getSimilarTracks(currentSong, limit = 100) { + return lastfm.getSimilarTracks(currentSong.artists?.[0], currentSong.name, limit) .then(tracks => tracks.json()) .then(trackJson => { return _.get(trackJson, 'similartracks.track', []); }); } -function getTracksFromSimilarArtist (artist) { +function getTracksFromSimilarArtist(artist) { return lastfm .getArtistInfo(artist) .then(artist => artist.json()) @@ -176,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 => { @@ -193,10 +200,10 @@ function getArtistTopTracks (artist) { }); } -function addToQueue (artist, track) { +function addToQueue(artists, track) { return new Promise((resolve) => { - props.actions.addToQueue({ - artist: artist.name, + 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 29e849886a..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.artist, - 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.artist, 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({ - artist: 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/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/SpotifyPlaylistAdapter/__snapshots__/SpotifyPlaylistAdapter.test.tsx.snap b/packages/app/app/containers/SpotifyPlaylistAdapter/__snapshots__/SpotifyPlaylistAdapter.test.tsx.snap index 35343f310d..dbf9340538 100644 --- a/packages/app/app/containers/SpotifyPlaylistAdapter/__snapshots__/SpotifyPlaylistAdapter.test.tsx.snap +++ b/packages/app/app/containers/SpotifyPlaylistAdapter/__snapshots__/SpotifyPlaylistAdapter.test.tsx.snap @@ -117,7 +117,7 @@ exports[`Spotfiy playlist adapter should display a Spotify playlist 1`] = `
{ 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 cdfacc2b07..6c02c845e3 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..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[]; }; @@ -25,7 +26,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/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/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/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; 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 25f5b54284..81b629a460 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: '', @@ -390,7 +384,7 @@ export const buildStoreState = () => { tracks: [ { uuid: 'test-track-1', - artist: 'test artist 1', + artists: ['test artist 1'], name: 'test track', thumbnail: 'test thumbnail', stream: { @@ -402,7 +396,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: { @@ -420,7 +414,7 @@ export const buildStoreState = () => { tracks: [ { uuid: 'test-track-1', - artist: 'test artist 1', + artists: ['test artist 1'], name: 'test track', thumbnail: 'test thumbnail', stream: @@ -433,7 +427,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: { @@ -458,14 +452,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, @@ -520,7 +514,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: { @@ -535,7 +529,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: { @@ -616,12 +610,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 }] } @@ -652,7 +646,7 @@ export const buildStoreState = () => { completion: 1, track: { uuid: '1', - artist: 'test artist 1', + artists: ['test artist 1'], name: 'finished track' } }, @@ -661,7 +655,7 @@ export const buildStoreState = () => { completion: 0.1, track: { uuid: '2', - artist: 'test artist 2', + artists: ['test artist 2'], name: 'track with errorx' } }, @@ -670,7 +664,7 @@ export const buildStoreState = () => { completion: 0.3, track: { uuid: '3', - artist: 'test artist 3', + artists: ['test artist 3'], name: 'paused track' } }, @@ -679,7 +673,7 @@ export const buildStoreState = () => { completion: 0.5, track: { uuid: '4', - artist: 'test artist 4', + artists: ['test artist 4'], name: 'started track' } }, @@ -688,7 +682,7 @@ export const buildStoreState = () => { completion: 0, track: { uuid: '5', - artist: 'test artist 5', + artists: ['test artist 5'], name: 'waiting track' } } @@ -715,7 +709,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: [{ @@ -753,7 +747,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: [{ @@ -771,7 +765,7 @@ export const buildStoreState = () => { error: false }, { - artist: 'test artist 3', + artists: ['test artist 3'], name: 'test track 3', thumbnail: undefined, streams: [{ @@ -854,7 +848,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', @@ -867,7 +861,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', @@ -879,7 +873,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 dccf5c7379..33d8445f8b 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..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)] : [] ); @@ -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..1d9fc279bd 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 @@ -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/plugins/meta/spotify.test.ts b/packages/core/src/plugins/meta/spotify.test.ts index 21ebe84e04..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' }]); @@ -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 @@ -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 478485f5db..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 }; @@ -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, @@ -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/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/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; }[]; 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 = { diff --git a/packages/core/src/rest/Spotify.ts b/packages/core/src/rest/Spotify.ts index 6d3c1a448f..0aac3e4e92 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.map(artist => artist.name), name: track.name, album: track.album.name, thumbnail: thumb, 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 () => { 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..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; @@ -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 218c044b42..95fc2c34c9 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'; @@ -64,7 +64,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/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/@linux/system-api.ts b/packages/main/src/services/@linux/system-api.ts index 2b635a5021..bc5cb7d7c8 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' }; @@ -143,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: '', @@ -207,7 +208,7 @@ class LinuxMediaService extends MprisService implements NuclearApi { clearTrackList() { this.tracks = []; this.sendMetadata({ - artist: 'Nuclear', + artists: ['Nuclear'], name: '', uuid: '0', thumbnail: '', 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/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/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); diff --git a/packages/ui/lib/components/GridTrackTable/index.tsx b/packages/ui/lib/components/GridTrackTable/index.tsx index eafc22967e..2884a5331d 100644 --- a/packages/ui/lib/components/GridTrackTable/index.tsx +++ b/packages/ui/lib/components/GridTrackTable/index.tsx @@ -78,7 +78,7 @@ export const GridTrackTable = ({ id: TrackTableColumn.Selection, Header: SelectionHeader, Cell: SelectionCell, - columnWidth: '7.5em' + columnWidth: '3em' }, displayPosition && { id: TrackTableColumn.Position, @@ -117,12 +117,10 @@ 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' + 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; } } } diff --git a/packages/ui/lib/components/MiniPlayer/MiniTrackInfo/index.tsx b/packages/ui/lib/components/MiniPlayer/MiniTrackInfo/index.tsx index 788a622da7..6fee2f182b 100644 --- a/packages/ui/lib/components/MiniPlayer/MiniTrackInfo/index.tsx +++ b/packages/ui/lib/components/MiniPlayer/MiniTrackInfo/index.tsx @@ -12,7 +12,7 @@ export type MiniTrackInfoProps = Omit = ({ cover = artPlaceholder as unknown as string, track, - artist, + artists, addToFavorites, removeFromFavorites, isFavorite = false, @@ -30,7 +30,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' );