Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
e4bb5e2
Use pathfinder API for search
nukeop May 13, 2025
25e45a1
Cleanup: remove old remote playlist files
nukeop May 15, 2025
ff8e821
Rename
nukeop May 15, 2025
402ebf0
Artist overview API
nukeop May 20, 2025
ca2cdbf
Refactor types
nukeop May 20, 2025
32459e8
Remove redundant type
nukeop May 20, 2025
2128a54
Update mappings for top tracks and similar artists
nukeop May 21, 2025
22d2c34
Convert AllResults to typescript
nukeop May 22, 2025
0c03e65
Deprecate last.fm-specific track search
nukeop May 23, 2025
7046b18
Search results cleanup
nukeop May 28, 2025
ee2b402
Refactor SearchResults
nukeop Jun 4, 2025
714edfb
Convert SearchResults to TS
nukeop Jun 7, 2025
28d8532
Simplify search result rendering, deprecate podcast search
nukeop Jun 7, 2025
a654f33
Deprecate podcast stuff
nukeop Jun 10, 2025
86f09c9
Update api endpoints
nukeop Jun 11, 2025
4e44942
Refactor metadata fetching
nukeop Jun 15, 2025
5219efc
Refactor search results component
nukeop Jun 15, 2025
64b14d4
Refactor search results
nukeop Jun 15, 2025
8f6139f
Create a cache for rest services
nukeop Jun 16, 2025
57fb1fb
Complete artist overview
nukeop Jun 17, 2025
afb11b4
Implement album view
nukeop Jun 19, 2025
ad7aff4
Fix header image on artist page
nukeop Jun 19, 2025
ebaeca6
Thumbnail and cover image mappers
nukeop Jun 21, 2025
a545bf6
Album details by name
nukeop Jun 21, 2025
7b35d3f
Fix spotify test mocks, deprecate spotify meta plugin
nukeop Jun 22, 2025
a82e871
Fix conditions preventing some parts of artist view from rendering
nukeop Jun 22, 2025
28e080c
Fix snapshot tests
nukeop Jun 22, 2025
d578a13
Fix search tests
nukeop Jun 23, 2025
196517c
Fix import path
nukeop Jun 23, 2025
0aa20f4
Fix import path
nukeop Jun 23, 2025
d183008
Fix imports
nukeop Jun 23, 2025
6660b14
Fix import path
nukeop Jun 24, 2025
806ca9e
Fix imports
nukeop Jun 24, 2025
9263794
Fix mock import path
nukeop Jun 24, 2025
91e42b9
Log on info level in prod
nukeop Jun 24, 2025
5f41c09
Fix import path
nukeop Jun 24, 2025
d54283c
Fix import
nukeop Jun 24, 2025
eebb969
Fix imports
nukeop Jun 24, 2025
091d2d3
Remove NuclearStreamMappingsService from mocks
nukeop Jun 24, 2025
85e85d7
Fix imports
nukeop Jun 24, 2025
23e3de0
Fix imports
nukeop Jun 24, 2025
40dd7cf
Fix imports
nukeop Jun 24, 2025
221abc9
Fix imports
nukeop Jun 24, 2025
533048e
FIx imports
nukeop Jun 24, 2025
4abc271
Fix paths
nukeop Jun 25, 2025
f7aef13
Remove deprecated method
nukeop Jul 2, 2025
b80b5fa
Remove deprecated method
nukeop Jul 2, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions packages/app/__mocks__/@nuclear/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,6 @@ module.exports = {
urlSearch: jest.fn().mockResolvedValue([]),
liveStreamSearch: jest.fn().mockResolvedValue([])
},
NuclearPlaylistsService: jest.requireActual('@nuclear/core/src/rest/Nuclear/Playlists').NuclearPlaylistsService,
NuclearStreamMappingsService: jest.requireActual('@nuclear/core/src/rest/Nuclear/StreamMappings').NuclearStreamMappingsService,
Deezer: {
...jest.requireActual('@nuclear/core/src/rest/Deezer'),
getEditorialCharts: jest.fn().mockResolvedValue({
Expand Down
9 changes: 2 additions & 7 deletions packages/app/app/actions/actionTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,14 @@ export enum Search {
ALBUM_INFO_SEARCH_START = 'ALBUM_INFO_SEARCH_START',
ALBUM_INFO_SEARCH_SUCCESS = 'ALBUM_INFO_SEARCH_SUCCESS',
ALBUM_INFO_SEARCH_ERROR = 'ALBUM_INFO_SEARCH_ERROR',
PODCAST_SEARCH_SUCCESS = 'PODCAST_SEARCH_SUCCESS',
ARTIST_INFO_SEARCH_START = 'ARTIST_INFO_SEARCH_START',
ARTIST_INFO_SEARCH_SUCCESS = 'ARTIST_INFO_SEARCH_SUCCESS',
ARTIST_INFO_SEARCH_ERROR = 'ARTIST_INFO_SEARCH_ERROR',
ARTIST_RELEASES_SEARCH_START = 'ARTIST_RELEASES_SEARCH_START',
ARTIST_RELEASES_SEARCH_SUCCESS = 'ARTIST_RELEASES_SEARCH_SUCCESS',
ARTIST_RELEASES_SEARCH_ERROR = 'ARTIST_RELEASES_SEARCH_ERROR',
LASTFM_TRACK_SEARCH_START = 'LASTFM_TRACK_SEARCH_START',
LASTFM_TRACK_SEARCH_SUCCESS = 'LASTFM_TRACK_SEARCH_SUCCESS',
TRACK_SEARCH_START = 'TRACK_SEARCH_START',
TRACK_SEARCH_ERROR = 'TRACK_SEARCH_ERROR',
TRACK_SEARCH_SUCCESS = 'TRACK_SEARCH_SUCCESS',
YOUTUBE_PLAYLIST_SEARCH_START = 'YOUTUBE_PLAYLIST_SEARCH_START',
YOUTUBE_PLAYLIST_SEARCH_SUCCESS = 'YOUTUBE_PLAYLIST_SEARCH_SUCCESS',
Expand Down Expand Up @@ -62,10 +61,6 @@ export enum Playlists {
LOAD_LOCAL_PLAYLISTS_SUCCESS = 'LOAD_LOCAL_PLAYLISTS_SUCCESS',
LOAD_LOCAL_PLAYLISTS_ERROR = 'LOAD_LOCAL_PLAYLISTS_ERROR',
UPDATE_LOCAL_PLAYLISTS = 'UPDATE_LOCAL_PLAYLISTS',

LOAD_REMOTE_PLAYLISTS_START = 'LOAD_REMOTE_PLAYLISTS_START',
LOAD_REMOTE_PLAYLISTS_SUCCESS = 'LOAD_REMOTE_PLAYLISTS_SUCCESS',
LOAD_REMOTE_PLAYLISTS_ERROR = 'LOAD_REMOTE_PLAYLISTS_ERROR',
}

export enum ImportFavs {
Expand Down
23 changes: 0 additions & 23 deletions packages/app/app/actions/nuclear/identity.ts

This file was deleted.

35 changes: 1 addition & 34 deletions packages/app/app/actions/playlists.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@ import { v4 } from 'uuid';
import { createAsyncAction, createStandardAction } from 'typesafe-actions';
import { ipcRenderer } from 'electron';

import { store, PlaylistHelper, Playlist, PlaylistTrack, rest, IpcEvents } from '@nuclear/core';
import { GetPlaylistsByUserIdResponseBody } from '@nuclear/core/src/rest/Nuclear/Playlists.types';
import { ErrorBody } from '@nuclear/core/src/rest/Nuclear/types';
import { store, PlaylistHelper, Playlist, PlaylistTrack, IpcEvents } from '@nuclear/core';

import { Playlists } from './actionTypes';

Expand All @@ -15,7 +13,6 @@ import {
updatePlaylistsOrderEffect
} from './playlists.effects';
import { success, error } from './toasts';
import { IdentityStore } from '../reducers/nuclear/identity';
import { PlaylistsStore } from '../reducers/playlists';
import { isEmpty } from 'lodash';

Expand All @@ -27,12 +24,6 @@ export const loadLocalPlaylistsAction = createAsyncAction(
Playlists.LOAD_LOCAL_PLAYLISTS_ERROR
)<void, Array<Playlist>, void>();

export const loadRemotePlaylistsAction = createAsyncAction(
Playlists.LOAD_REMOTE_PLAYLISTS_START,
Playlists.LOAD_REMOTE_PLAYLISTS_SUCCESS,
Playlists.LOAD_REMOTE_PLAYLISTS_ERROR
)<void, GetPlaylistsByUserIdResponseBody, ErrorBody>();

export const addPlaylist = (tracks: Array<PlaylistTrack>, name: string) => dispatch => {
if (name?.length === 0) {
return;
Expand Down Expand Up @@ -62,30 +53,6 @@ export const loadLocalPlaylists = () => dispatch => {
}
};


export const loadRemotePlaylists = ({ token, signedInUser }: IdentityStore) => async (dispatch, getState) => {
dispatch(loadRemotePlaylistsAction.request());
const { settings } = getState();
const service = new rest.NuclearPlaylistsService(
settings.nuclearPlaylistsServiceUrl
);

try {
if (token) {
const playlists = await service.getPlaylistsByUserId(token, signedInUser.id);
if (playlists.ok) {
dispatch(loadRemotePlaylistsAction.success(playlists.body as GetPlaylistsByUserIdResponseBody));
} else {
throw playlists.body;
}
} else {
throw new Error('No token');
}
} catch (e) {
dispatch(loadRemotePlaylistsAction.failure(e.message));
}
};

export const updatePlaylist = (playlist: Playlist) => dispatch => {
const playlists = updatePlaylistEffect(store)(playlist);
dispatch(updatePlaylistsAction(playlists));
Expand Down
10 changes: 5 additions & 5 deletions packages/app/app/actions/queue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
import { createStandardAction } from 'typesafe-actions';
import { v4 } from 'uuid';

import { rest, StreamProvider } from '@nuclear/core';
import { StreamProvider } from '@nuclear/core';
import StreamProviderPlugin from '@nuclear/core/src/plugins/streamProvider';
import { Nuclear } from '@nuclear/core/src/rest';
import { getTrackArtist, getTrackTitle } from '@nuclear/ui';
import { Track } from '@nuclear/ui/lib/types';

Expand All @@ -13,8 +15,6 @@
import { RootState } from '../reducers';
import { LocalLibraryState } from './local';
import { Queue } from './actionTypes';
import StreamProviderPlugin from '@nuclear/core/src/plugins/streamProvider';
import { isSuccessCacheEntry } from '@nuclear/core/src/rest/Nuclear/StreamMappings';
import { queue as queueSelector } from '../selectors/queue';
import { error } from './toasts';
import { random } from 'lodash';
Expand Down Expand Up @@ -200,15 +200,15 @@
}

try {
const StreamMappingsService = rest.NuclearStreamMappingsService.get(process.env.NUCLEAR_VERIFICATION_SERVICE_URL);
const StreamMappingsService = Nuclear.NuclearStreamMappingsService.get(process.env.NUCLEAR_VERIFICATION_SERVICE_URL);

Check warning on line 203 in packages/app/app/actions/queue.ts

View check run for this annotation

Codecov / codecov/patch

packages/app/app/actions/queue.ts#L203

Added line #L203 was not covered by tests
const topStream = await StreamMappingsService.getTopStream(
getTrackArtist(track),
getTrackTitle(track),
selectedStreamProvider.sourceName,
settings?.userId
);

if (!isSuccessCacheEntry(topStream)) {
if (!Nuclear.isSuccessCacheEntry(topStream)) {
return streamData;
}

Expand Down
126 changes: 42 additions & 84 deletions packages/app/app/actions/search.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,14 @@
import { logger } from '@nuclear/core';
import { rest } from '@nuclear/core';
import _, { isString } from 'lodash';
import artPlaceholder from '../../resources/media/art_placeholder.png';
import globals from '../globals';
import _ from 'lodash';
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 { createStandardAction } from 'typesafe-actions';
import { LastfmTrackMatch, LastfmTrackMatchInternal } from '@nuclear/core/src/rest/Lastfm.types';
import { AlbumDetails, ArtistDetails, SearchResultsAlbum, SearchResultsArtist, SearchResultsSource, SearchResultsTrack } from '@nuclear/core/src/plugins/plugins.types';
import { createAsyncAction, createStandardAction } from 'typesafe-actions';
import { YoutubeResult } from '@nuclear/core/src/rest/Youtube';

const lastfm = new rest.LastFmApi(globals.lastfmApiKey, globals.lastfmApiSecret);
import { getTrackArtist } from '@nuclear/ui';

export const SearchActions = {
unifiedSearchStart: createStandardAction(Search.UNIFIED_SEARCH_START)<string>(),
Expand All @@ -28,7 +24,7 @@
};
}),
youtubeLiveStreamSearchStart: createStandardAction(Search.YOUTUBE_LIVESTREAM_SEARCH_START)<string>(),
youtubeLiveStreamSearchSuccess: createStandardAction(Search.YOUTUBE_LIVESTREAM_SEARCH_SUCCESS).map((id: string, info: YoutubeResult[]) => {
youtubeLiveStreamSearchSuccess: createStandardAction(Search.YOUTUBE_LIVESTREAM_SEARCH_SUCCESS).map((id: string, info: SearchResultsTrack[]) => {
return {
payload: {
id,
Expand Down Expand Up @@ -68,19 +64,8 @@
}
};
}),
podcastSearchSuccess: createStandardAction(Search.PODCAST_SEARCH_SUCCESS)<SearchResultsPodcast[]>(),
setSearchDropdownVisibility: createStandardAction(Search.SEARCH_DROPDOWN_DISPLAY_CHANGE)<boolean>(),
updateSearchHistory: createStandardAction(Search.UPDATE_SEARCH_HISTORY)<string[]>(),
lastFmTrackSearchStart: createStandardAction(Search.LASTFM_TRACK_SEARCH_START)<string>(),
lastFmTrackSearchSuccess: createStandardAction(Search.LASTFM_TRACK_SEARCH_SUCCESS).map((terms: string, searchResults: LastfmTrackMatchInternal[]) => {
return {
payload: {
id: terms,
info: searchResults
}
};
}),
trackSearchSuccess: createStandardAction(Search.TRACK_SEARCH_SUCCESS)<SearchResultsAlbum[]>(),
artistSearchSuccess: createStandardAction(Search.ARTIST_SEARCH_SUCCESS)<SearchResultsArtist[]>(),
artistInfoStart: createStandardAction(Search.ARTIST_INFO_SEARCH_START)<string>(),
artistInfoSuccess: createStandardAction(Search.ARTIST_INFO_SEARCH_SUCCESS).map((artistId: string, info: ArtistDetails) => {
Expand Down Expand Up @@ -121,7 +106,12 @@
error
}
};
})
}),
trackSearchAction: createAsyncAction(
Search.TRACK_SEARCH_START,
Search.TRACK_SEARCH_SUCCESS,
Search.TRACK_SEARCH_ERROR
)<undefined, SearchResultsTrack[], undefined>()
};


Expand All @@ -131,9 +121,12 @@
plugins: { metaProviders }, selected }
} = getState();

return wantedProvider ?
const selectedProvider = wantedProvider ?
_.find(metaProviders, { searchName: wantedProvider }) :
_.find(metaProviders, { sourceName: selected.metaProviders });


return selectedProvider || _.find(metaProviders, { isDefault: true }) || null;
};

export const artistSearch = (terms: string) => async (dispatch, getState: () => RootState) => {
Expand All @@ -149,57 +142,17 @@
};

export const trackSearch = (terms: string) => async (dispatch, getState: () => RootState) => {
const selectedProvider = getSelectedMetaProvider(getState);
const results = await selectedProvider.searchForTracks(terms);
dispatch(SearchActions.trackSearchSuccess(results));
};

export const podcastSearch = (terms: string) => async (dispatch, getState: () => RootState) => {
const selectedProvider = getSelectedMetaProvider(getState);
const results = await selectedProvider.searchForPodcast(terms);
dispatch(SearchActions.podcastSearchSuccess(results));
};


const isAcceptableLastFMThumbnail = (thumbnail: string) =>
!(/https?:\/\/lastfm-img\d.akamaized.net\/i\/u\/\d+s\/2a96cbd8b46e442fc41c2b86b821562f\.png/.test(thumbnail));

const getTrackThumbnail = (track: LastfmTrackMatch) => {
const image =
_.get(
track,
['image', 1, '#text'],
_.get(
track,
['image', 0, '#text'],
artPlaceholder
)
);

return !isString(image) ? artPlaceholder : isAcceptableLastFMThumbnail(image) ? image : artPlaceholder;
dispatch(SearchActions.trackSearchAction.request());
try {
const selectedProvider = getSelectedMetaProvider(getState);
const results = await selectedProvider.searchForTracks(terms);
dispatch(SearchActions.trackSearchAction.success(results));
} catch (e) {
logger.error(e);
dispatch(SearchActions.trackSearchAction.failure());

Check warning on line 152 in packages/app/app/actions/search.ts

View check run for this annotation

Codecov / codecov/patch

packages/app/app/actions/search.ts#L151-L152

Added lines #L151 - L152 were not covered by tests
}
};

export const mapLastFMTrackToInternal = (track: LastfmTrackMatch) => ({
...track,
thumbnail: getTrackThumbnail(track)
});

export function lastFmTrackSearch(terms: string) {
return dispatch => {
dispatch(SearchActions.lastFmTrackSearchStart(terms));
Promise.all([lastfm.searchTracks(terms)])
.then(results => Promise.all(results.map(info => info.json())))
.then(results => {
dispatch(
SearchActions.lastFmTrackSearchSuccess(terms, _.get(results[0], 'results.trackmatches.track', []).map(mapLastFMTrackToInternal))
);
})
.catch(error => {
logger.error(error);
});
};
}

export function youtubePlaylistSearch(terms: string) {
return dispatch => {
dispatch(SearchActions.youtubePlaylistSearchStart(terms));
Expand All @@ -218,7 +171,13 @@
export const youtubeLiveStreamSearch = (terms: string) => async (dispatch) => {
dispatch(SearchActions.youtubeLiveStreamSearchStart(terms));
try {
const results = await rest.Youtube.liveStreamSearch(terms);
const results = (await rest.Youtube.liveStreamSearch(terms)).map(el => ({
id: el.streams[0].id,
title: el.name,
artist: getTrackArtist(el),
thumb: el.thumbnail,
source: SearchResultsSource.Youtube
}) satisfies SearchResultsTrack);
dispatch(SearchActions.youtubeLiveStreamSearchSuccess(terms, results));
} catch (e) {
logger.error(e);
Expand All @@ -233,8 +192,7 @@
Promise.all([
dispatch(albumSearch(terms)),
dispatch(artistSearch(terms)),
dispatch(podcastSearch(terms)),
dispatch(lastFmTrackSearch(terms)),
dispatch(trackSearch(terms)),
dispatch(youtubePlaylistSearch(terms)),
dispatch(youtubeLiveStreamSearch(terms))
])
Expand All @@ -251,28 +209,28 @@
};
}

export const albumInfoSearch = (albumId: string, releaseType: 'master' | 'release' = 'master', release: SearchResultsAlbum) => async (dispatch, getState:() => RootState) => {
dispatch(SearchActions.albumInfoStart(albumId));
export const albumInfoSearch = (release: SearchResultsAlbum) => async (dispatch, getState:() => RootState) => {
dispatch(SearchActions.albumInfoStart(release.id));
try {
const selectedProvider = getSelectedMetaProvider(getState);
const albumDetails = await selectedProvider.fetchAlbumDetails(albumId, releaseType, release?.resourceUrl);
dispatch(SearchActions.albumInfoSuccess(albumId, albumDetails));
const albumDetails = await selectedProvider.fetchAlbumDetails(release.id, release.type, release?.resourceUrl);
dispatch(SearchActions.albumInfoSuccess(release.id, albumDetails));

Check warning on line 217 in packages/app/app/actions/search.ts

View check run for this annotation

Codecov / codecov/patch

packages/app/app/actions/search.ts#L217

Added line #L217 was not covered by tests
} catch (e) {
logger.error(e);
dispatch(SearchActions.albumInfoError(albumId, e));
dispatch(SearchActions.albumInfoError(release.id, e));
}
};


export const artistInfoSearch = (artistId: string, artist: SearchResultsArtist) => async (dispatch, getState: () => RootState) => {
dispatch(SearchActions.artistInfoStart(artistId));
export const artistInfoSearch = (artist: SearchResultsArtist) => async (dispatch, getState: () => RootState) => {
dispatch(SearchActions.artistInfoStart(artist.id));

Check warning on line 226 in packages/app/app/actions/search.ts

View check run for this annotation

Codecov / codecov/patch

packages/app/app/actions/search.ts#L226

Added line #L226 was not covered by tests
try {
const selectedProvider = getSelectedMetaProvider(getState, artist?.source);
const artistDetails = await selectedProvider.fetchArtistDetails(artistId);
dispatch(SearchActions.artistInfoSuccess(artistId, artistDetails));
const artistDetails = await selectedProvider.fetchArtistDetails(artist.id);
dispatch(SearchActions.artistInfoSuccess(artist.id, artistDetails));

Check warning on line 230 in packages/app/app/actions/search.ts

View check run for this annotation

Codecov / codecov/patch

packages/app/app/actions/search.ts#L229-L230

Added lines #L229 - L230 were not covered by tests
} catch (e) {
logger.error(e);
dispatch(SearchActions.artistInfoError(artistId, e));
dispatch(SearchActions.artistInfoError(artist.id, e));

Check warning on line 233 in packages/app/app/actions/search.ts

View check run for this annotation

Codecov / codecov/patch

packages/app/app/actions/search.ts#L233

Added line #L233 was not covered by tests
}
};

Expand Down
6 changes: 4 additions & 2 deletions packages/app/app/components/ArtistView/ArtistHeader/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,17 @@ export const ArtistHeader: React.FC<ArtistHeaderProps> = ({
addFavoriteArtist
}) => {
const { t }= useTranslation('artist');

const avatar = artist.thumb ?? artist.images?.[1] ?? artPlaceholder;

return <div className={styles.artist_header_overlay}>
<div className={styles.artist_header_container}>
{
artist.images &&
<div
className={styles.artist_avatar}
style={{
background: `url('${get(artist, 'images[1]', artPlaceholder)
}')`,
background: `url('${avatar}')`,
backgroundRepeat: 'noRepeat',
backgroundPosition: 'center',
backgroundSize: 'cover'
Expand Down
Loading
Loading