Browse Source

Add types, normalize API returns

master
jeffvli 3 years ago
committed by Jeff
parent
commit
4af81d4404
  1. 14
      src/__tests__/App.test.tsx
  2. 746
      src/api/api.ts
  3. 108
      src/api/types.ts
  4. 10
      src/components/card/Card.tsx
  5. 56
      src/components/dashboard/Dashboard.tsx
  6. 16
      src/components/library/AlbumList.tsx
  7. 4
      src/components/library/AlbumView.tsx
  8. 4
      src/components/library/ArtistList.tsx
  9. 22
      src/components/library/ArtistView.tsx
  10. 10
      src/components/library/FolderList.tsx
  11. 6
      src/components/library/GenreList.tsx
  12. 29
      src/components/player/NowPlayingMiniView.tsx
  13. 28
      src/components/player/NowPlayingView.tsx
  14. 9
      src/components/playlist/PlaylistList.tsx
  15. 4
      src/components/playlist/PlaylistView.tsx
  16. 4
      src/components/search/SearchView.tsx
  17. 46
      src/components/settings/ListViewColumns.ts
  18. 26
      src/components/shared/ContextMenu.tsx
  19. 10
      src/components/starred/StarredView.tsx

14
src/__tests__/App.test.tsx

@ -220,7 +220,7 @@ const configState: ConfigPage = {
},
{
id: 'Title',
dataKey: 'name',
dataKey: 'title',
alignment: 'left',
flexGrow: 5,
label: 'Title',
@ -276,11 +276,11 @@ const configState: ConfigPage = {
label: 'CoverArt',
},
{
id: 'Name',
dataKey: 'name',
id: 'Title',
dataKey: 'title',
alignment: 'left',
flexGrow: 5,
label: 'Name',
label: 'Title',
},
{
id: 'Albums',
@ -311,11 +311,11 @@ const configState: ConfigPage = {
label: '#',
},
{
id: 'Name',
dataKey: 'name',
id: 'Title',
dataKey: 'title',
alignment: 'left',
flexGrow: 5,
label: 'Name',
label: 'Tame',
},
{
id: 'Albums',

746
src/api/api.ts

@ -5,6 +5,7 @@ import settings from 'electron-settings';
import { nanoid } from 'nanoid/non-secure';
import axiosRetry from 'axios-retry';
import { mockSettings } from '../shared/mockSettings';
import { Item } from './types';
const legacyAuth =
process.env.NODE_ENV === 'test'
@ -31,6 +32,23 @@ const getAuth = (useLegacyAuth: boolean) => {
const auth = getAuth(legacyAuth);
const API_BASE_URL = `${auth.server}/rest`;
const authParams = legacyAuth
? {
u: auth.username,
p: auth.password,
v: '1.15.0',
c: 'sonixd',
f: 'json',
}
: {
u: auth.username,
s: auth.salt,
t: auth.hash,
v: '1.15.0',
c: 'sonixd',
f: 'json',
};
export const api = axios.create({
baseURL: API_BASE_URL,
});
@ -65,54 +83,6 @@ axiosRetry(api, {
},
});
export const autoFailApi = axios.create({
baseURL: API_BASE_URL,
validateStatus: () => {
return false;
},
});
autoFailApi.interceptors.request.use((config) => {
config.params = config.params || {};
config.params.u = auth.username;
config.params.s = auth.salt;
config.params.t = auth.hash;
config.params.v = '1.15.0';
config.params.c = 'sonixd';
config.params.f = 'json';
return config;
});
autoFailApi.interceptors.response.use(
(res) => {
// Return the subsonic response directly
res.data = res.data['subsonic-response'];
return res;
},
(err) => {
return Promise.reject(err);
}
);
axiosRetry(autoFailApi, {
retries: 5,
retryCondition: (e: any) => {
return e.response.data['subsonic-response'].status !== 'ok';
},
retryDelay: (retryCount) => {
return retryCount * 1000;
},
});
const authParams = {
u: auth.username,
s: auth.salt,
t: auth.hash,
v: '1.15.0',
c: 'sonixd',
f: 'json',
};
const getCoverArtUrl = (item: any, useLegacyAuth: boolean, size?: number) => {
if (!item.coverArt && !item.artistImageUrl) {
return 'img/placeholder.jpg';
@ -127,38 +97,14 @@ const getCoverArtUrl = (item: any, useLegacyAuth: boolean, size?: number) => {
}
if (useLegacyAuth) {
if (!size) {
return (
`${API_BASE_URL}/getCoverArt` +
`?id=${item.coverArt}` +
`&u=${auth.username}` +
`&s=${auth.salt}` +
`&t=${auth.hash}` +
`&v=1.15.0` +
`&c=sonixd`
);
}
return (
`${API_BASE_URL}/getCoverArt` +
`?id=${item.coverArt}` +
`&u=${auth.username}` +
`&s=${auth.salt}` +
`&t=${auth.hash}` +
`&p=${auth.password}` +
`&v=1.15.0` +
`&c=sonixd` +
`&size=${size}`
);
}
if (!size) {
return (
`${API_BASE_URL}/getCoverArt` +
`?id=${item.coverArt}` +
`&u=${auth.username}` +
`&s=${auth.salt}` +
`&t=${auth.hash}` +
`&v=1.15.0` +
`&c=sonixd`
`${size ? `&size=${size}` : ''}`
);
}
@ -170,7 +116,7 @@ const getCoverArtUrl = (item: any, useLegacyAuth: boolean, size?: number) => {
`&t=${auth.hash}` +
`&v=1.15.0` +
`&c=sonixd` +
`&size=${size}`
`${size ? `&size=${size}` : ''}`
);
};
@ -220,205 +166,147 @@ const getStreamUrl = (id: string, useLegacyAuth: boolean) => {
);
};
export const getPlaylists = async (sortBy: string) => {
const { data } = await api.get('/getPlaylists');
const newData =
sortBy === 'dateCreated'
? data.playlists?.playlist.sort((a: any, b: any) => {
return a.created > b.created ? -1 : a.created < b.created ? 1 : 0;
})
: sortBy === 'dateModified'
? data.playlists?.playlist.sort((a: any, b: any) => {
return a.changed > b.changed ? -1 : a.changed < b.changed ? 1 : 0;
})
: sortBy === 'name'
? _.orderBy(data.playlists.playlist || [], [(entry) => entry.name.toLowerCase()], 'asc')
: data.playlists?.playlist;
return (newData || []).map((playlist: any) => ({
...playlist,
name: playlist.name,
image:
playlist.songCount > 0 ? getCoverArtUrl(playlist, legacyAuth, 350) : 'img/placeholder.jpg',
type: 'playlist',
const normalizeSong = (item: any) => {
return {
id: item.id,
parent: item.parent,
isDir: item.isDir,
title: item.title,
album: item.album,
albumId: item.albumId,
artist: item.artist,
artistId: item.artistId,
track: item.track,
year: item.year,
genre: item.genre,
size: item.size,
contentType: item.contentType,
suffix: item.suffix,
duration: item.duration,
bitRate: item.bitRate,
path: item.path,
playCount: item.playCount,
discNumber: item.discNumber,
created: item.created,
streamUrl: getStreamUrl(item.id, legacyAuth),
image: getCoverArtUrl(item, legacyAuth, 150),
starred: item.starred,
type: Item.Music,
uniqueId: nanoid(),
}));
};
};
export const getPlaylist = async (id: string) => {
const { data } = await api.get(`/getPlaylist?id=${id}`);
const normalizeAlbum = (item: any) => {
return {
...data.playlist,
entry: null, // Normalize to 'song' instead of 'entry'
song: (data.playlist.entry || []).map((entry: any, index: any) => ({
...entry,
streamUrl: getStreamUrl(entry.id, legacyAuth),
image: getCoverArtUrl(entry, legacyAuth, 150),
type: 'music',
index,
id: item.id,
title: item.name,
albumId: item.id,
artist: item.artist,
artistId: item.artistId,
songCount: item.songCount,
duration: item.duration,
created: item.created,
year: item.year,
genre: item.genre,
image: getCoverArtUrl(item, legacyAuth, 350),
isDir: false,
starred: item.starred,
type: Item.Album,
uniqueId: nanoid(),
})),
image:
data.playlist.songCount > 0
? getCoverArtUrl(data.playlist, legacyAuth, 350)
: 'img/placeholder.jpg',
song: (item.song || []).map((entry: any) => normalizeSong(entry)),
};
};
export const getPing = async () => {
const { data } = await api.get(`/ping`);
return data;
};
export const getStream = async (id: string) => {
const { data } = await api.get(`/stream?id=${id}`);
return data;
const normalizeArtist = (item: any) => {
return {
id: item.id,
title: item.name,
albumCount: item.albumCount,
image: getCoverArtUrl(item, legacyAuth, 350),
starred: item.starred,
type: Item.Artist,
uniqueId: nanoid(),
album: (item.album || []).map((entry: any) => normalizeAlbum(entry)),
};
};
export const getDownload = async (options: { id: string }) => {
const { data } = await api.get(`/download?id=${options.id}`, {
responseType: 'blob',
onDownloadProgress: (progressEvent) => {
const percentCompleted = Math.floor((progressEvent.loaded / progressEvent.total) * 100);
console.log(`percentCompleted`, percentCompleted);
},
});
return data;
const normalizeArtistInfo = (item: any) => {
return {
biography: item.biography,
lastFmUrl: item.lastFmUrl,
imageUrl: item.largeImageUrl || item.mediumImageUrl || item.smallImageUrl,
similarArtist: (item.similarArtist || []).map((entry: any) => normalizeArtist(entry)),
};
};
export const getPlayQueue = async () => {
const { data } = await api.get(`/getPlayQueue`);
const normalizePlaylist = (item: any) => {
return {
...data.playQueue,
entry: (data.playQueue.entry || []).map((entry: any, index: any) => ({
...entry,
streamUrl: getStreamUrl(entry.id, legacyAuth),
index,
})),
id: item.id,
title: item.name,
comment: item.comment,
owner: item.owner,
public: item.public,
songCount: item.songCount,
duration: item.duration,
created: item.created,
changed: item.changed,
image: item.songCount > 0 ? getCoverArtUrl(item, legacyAuth, 350) : 'img/placeholder.jpg',
type: Item.Playlist,
uniqueId: nanoid(),
song: (item.entry || []).map((entry: any) => normalizeSong(entry)),
};
};
export const getStarred = async (options: { musicFolderId?: string | number }) => {
const { data } = await api.get(`/getStarred2`, { params: options });
const normalizeGenre = (item: any) => {
return {
...data.starred2,
album: (data.starred2.album || []).map((entry: any, index: any) => ({
...entry,
title: entry.name,
albumId: entry.id,
image: getCoverArtUrl(entry, legacyAuth, 350),
starred: entry.starred || undefined,
type: 'album',
isDir: false,
index,
uniqueId: nanoid(),
})),
song: (data.starred2.song || []).map((entry: any, index: any) => ({
...entry,
streamUrl: getStreamUrl(entry.id, legacyAuth),
image: getCoverArtUrl(entry, legacyAuth, 150),
starred: entry.starred || undefined,
type: 'music',
index,
id: item.id,
title: item.value,
songCount: item.songCount,
albumCount: item.albumCount,
type: Item.Genre,
uniqueId: nanoid(),
})),
artist: (data.starred2.artist || []).map((entry: any, index: any) => ({
...entry,
albumCount: entry.albumCount || undefined,
coverArt: getCoverArtUrl(entry, legacyAuth),
image: getCoverArtUrl(entry, legacyAuth, 350),
starred: entry.starred || Date.now(), // Airsonic does not return the starred date
type: 'artist',
index,
uniqueId: nanoid(),
})),
};
};
export const getAlbums = async (options: {
type:
| 'random'
| 'newest'
| 'highest'
| 'frequent'
| 'recent'
| 'alphabeticalByName'
| 'alphabeticalByArtist'
| 'starred'
| 'byYear'
| 'byGenre';
size?: number;
offset?: number;
fromYear?: number;
toYear?: number;
genre?: string;
musicFolderId?: string | number;
}) => {
const { data } = await api.get(`/getAlbumList2`, {
params: options,
});
const normalizeFolder = (item: any) => {
return {
...data.albumList2,
album: (data.albumList2.album || []).map((entry: any, index: any) => ({
...entry,
title: entry.name,
albumId: entry.id,
image: getCoverArtUrl(entry, legacyAuth, 350),
starred: entry.starred || undefined,
type: 'album',
isDir: false,
index,
id: item.id,
title: item.name || item.title,
isDir: true,
image: getCoverArtUrl(item, legacyAuth, 150),
type: Item.Folder,
uniqueId: nanoid(),
})),
};
};
export const getAlbumsDirect = async (options: {
type:
| 'random'
| 'newest'
| 'highest'
| 'frequent'
| 'recent'
| 'alphabeticalByName'
| 'alphabeticalByArtist'
| 'starred'
| 'byYear'
| 'byGenre';
size?: number;
offset?: number;
fromYear?: number;
toYear?: number;
genre?: string;
musicFolderId?: string | number;
}) => {
const { data } = await api.get(`/getAlbumList2`, {
params: options,
});
export const getPlaylist = async (id: string) => {
const { data } = await api.get(`/getPlaylist?id=${id}`);
return normalizePlaylist(data.playlist);
};
const albums = (data.albumList2.album || []).map((entry: any, index: any) => ({
...entry,
title: entry.name,
albumId: entry.id,
image: getCoverArtUrl(entry, legacyAuth, 350),
starred: entry.starred || undefined,
type: 'album',
isDir: false,
index,
uniqueId: nanoid(),
}));
export const getPlaylists = async () => {
const { data } = await api.get('/getPlaylists');
return (data.playlists?.playlist || []).map((playlist: any) => normalizePlaylist(playlist));
};
return albums;
export const getStarred = async (options: { musicFolderId?: string | number }) => {
const { data } = await api.get(`/getStarred2`, { params: options });
return {
album: (data.starred2.album || []).map((entry: any) => normalizeAlbum(entry)),
song: (data.starred2.song || []).map((entry: any) => normalizeSong(entry)),
artist: (data.starred2.artist || []).map((entry: any) => normalizeArtist(entry)),
};
};
export const getAlbum = async (options: { id: string }) => {
const { data } = await api.get(`/getAlbum`, { params: options });
return normalizeAlbum(data.album);
};
export const getAllAlbums = (
export const getAlbums = async (
options: {
type:
| string // Handle generic genres
| 'random'
| 'newest'
| 'highest'
@ -435,9 +323,11 @@ export const getAllAlbums = (
toYear?: number;
genre?: string;
musicFolderId?: string | number;
recursive?: boolean;
},
data: any[] = []
recursiveData: any[] = []
) => {
if (options.recursive) {
const albums: any = api
.get(`/getAlbumList2`, {
params: {
@ -446,7 +336,9 @@ export const getAllAlbums = (
: 'byGenre',
size: 500,
offset: options.offset,
genre: options.type.match('alphabeticalByName|alphabeticalByArtist|frequent|newest|recent')
genre: options.type.match(
'alphabeticalByName|alphabeticalByArtist|frequent|newest|recent'
)
? undefined
: options.type,
musicFolderId: options.musicFolderId,
@ -454,60 +346,31 @@ export const getAllAlbums = (
})
.then((res) => {
if (!res.data.albumList2.album || res.data.albumList2.album.length === 0) {
// Flatten the array and return once there are no more albums left
const flattened = _.flatten(data);
return flattened.map((entry: any, index: any) => ({
...entry,
title: entry.name,
albumId: entry.id,
image: getCoverArtUrl(entry, legacyAuth, 350),
starred: entry.starred || undefined,
type: 'album',
isDir: false,
index,
uniqueId: nanoid(),
}));
// Flatten and return once there are no more albums left
const flattenedAlbums = _.flatten(recursiveData);
return (flattenedAlbums || []).map((entry: any) => normalizeAlbum(entry));
}
// On every iteration, push the existing combined album array and increase the offset
data.push(res.data.albumList2.album);
return getAllAlbums(
recursiveData.push(res.data.albumList2.album);
return getAlbums(
{
type: options.type,
size: options.size,
offset: options.offset + options.size,
musicFolderId: options.musicFolderId,
recursive: true,
},
data
recursiveData
);
})
.catch((err) => console.log(err));
return albums;
};
export const getAlbum = async (id: string) => {
const { data } = await api.get(`/getAlbum`, {
params: {
id,
},
});
}
return {
...data.album,
image: getCoverArtUrl(data.album, legacyAuth, 350),
type: 'album',
isDir: false,
song: (data.album.song || []).map((entry: any, index: any) => ({
...entry,
streamUrl: getStreamUrl(entry.id, legacyAuth),
image: getCoverArtUrl(entry, legacyAuth, 150),
type: 'music',
starred: entry.starred || undefined,
index,
uniqueId: nanoid(),
})),
};
const { data } = await api.get(`/getAlbumList2`, { params: options });
return (data.albumList2.album || []).map((entry: any) => normalizeAlbum(entry));
};
export const getRandomSongs = async (options: {
@ -517,103 +380,47 @@ export const getRandomSongs = async (options: {
toYear?: number;
musicFolderId?: number;
}) => {
const { data } = await api.get(`/getRandomSongs`, {
params: options,
});
const { data } = await api.get(`/getRandomSongs`, { params: options });
return (data.randomSongs.song || []).map((entry: any) => normalizeSong(entry));
};
return {
...data.randomSongs,
song: (data.randomSongs.song || []).map((entry: any, index: any) => ({
...entry,
streamUrl: getStreamUrl(entry.id, legacyAuth),
image: getCoverArtUrl(entry, legacyAuth, 150),
starred: entry.starred || undefined,
index,
uniqueId: nanoid(),
})),
};
export const getArtist = async (options: { id: string }) => {
const { data } = await api.get(`/getArtist`, { params: options });
return normalizeArtist(data.artist);
};
export const getArtists = async (options: { musicFolderId?: string | number }) => {
const { data } = await api.get(`/getArtists`, {
params: options,
});
const artistList: any[] = [];
const { data } = await api.get(`/getArtists`, { params: options });
const artists = (data.artists?.index || []).flatMap((index: any) => index.artist);
artists.map((artist: any) =>
artistList.push({
...artist,
image: getCoverArtUrl(artist, legacyAuth, 350),
type: 'artist',
uniqueId: nanoid(),
})
);
return artistList;
return (artists || []).map((entry: any) => normalizeArtist(entry));
};
export const getArtist = async (id: string) => {
const { data } = await api.get(`/getArtist`, {
params: {
id,
},
});
return {
...data.artist,
image: getCoverArtUrl(data.artist, legacyAuth, 350),
type: 'artist',
album: (data.artist.album || []).map((entry: any, index: any) => ({
...entry,
albumId: entry.id,
type: 'album',
isDir: false,
image: getCoverArtUrl(entry, legacyAuth, 350),
starred: entry.starred || undefined,
index,
uniqueId: nanoid(),
})),
};
export const getArtistInfo = async (options: { id: string; count: number }) => {
const { data } = await api.get(`/getArtistInfo2`, { params: options });
return normalizeArtistInfo(data.artistInfo2);
};
export const getArtistInfo = async (id: string, count = 10) => {
const { data } = await api.get(`/getArtistInfo2`, {
params: {
id,
count,
},
});
return {
...data.artistInfo2,
};
};
export const getAllArtistSongs = async (id: string) => {
export const getArtistSongs = async (options: { id: string }) => {
const promises = [];
const artist = await getArtist(id);
const artist = await getArtist({ id: options.id });
for (let i = 0; i < artist.album.length; i += 1) {
promises.push(getAlbum(artist.album[i].id));
promises.push(getAlbum({ id: artist.album[i].id }));
}
const res = await Promise.all(promises);
return _.flatten(_.map(res, 'song'));
return (_.flatten(_.map(res, 'song')) || []).map((entry: any) => normalizeSong(entry));
};
export const startScan = async () => {
const { data } = await api.get(`/startScan`);
const scanStatus = data?.scanStatus;
return scanStatus;
};
export const getScanStatus = async () => {
const { data } = await api.get(`/getScanStatus`);
const scanStatus = data?.scanStatus;
return scanStatus;
};
@ -668,13 +475,7 @@ export const batchStar = async (ids: string[], type: string) => {
params.append(key, value);
});
res.push(
(
await api.get(`/star`, {
params,
})
).data
);
res.push((await api.get(`/star`, { params })).data);
}
return res;
@ -707,42 +508,20 @@ export const batchUnstar = async (ids: string[], type: string) => {
params.append(key, value);
});
res.push(
(
await api.get(`/unstar`, {
params,
})
).data
);
res.push((await api.get(`/unstar`, { params })).data);
}
return res;
};
export const setRating = async (id: string, rating: number) => {
const { data } = await api.get(`/setRating`, {
params: {
id,
rating,
},
});
const { data } = await api.get(`/setRating`, { params: { id, rating } });
return data;
};
export const getSimilarSongs = async (id: string, count: number) => {
const { data } = await api.get(`/getSimilarSongs2`, {
params: { id, count },
});
return {
song: (data.similarSongs2.song || []).map((entry: any, index: any) => ({
...entry,
image: getCoverArtUrl(entry, legacyAuth, 150),
index,
uniqueId: nanoid(),
})),
};
const { data } = await api.get(`/getSimilarSongs2`, { params: { id, count } });
return (data.similarSongs2.song || []).map((entry: any) => normalizeSong(entry));
};
export const updatePlaylistSongs = async (id: string, entry: any[]) => {
@ -793,22 +572,12 @@ export const updatePlaylistSongsLg = async (playlistId: string, entry: any[]) =>
};
export const deletePlaylist = async (id: string) => {
const { data } = await api.get(`/deletePlaylist`, {
params: {
id,
},
});
const { data } = await api.get(`/deletePlaylist`, { params: { id } });
return data;
};
export const createPlaylist = async (name: string) => {
const { data } = await api.get(`/createPlaylist`, {
params: {
name,
},
});
const { data } = await api.get(`/createPlaylist`, { params: { name } });
return data;
};
@ -832,71 +601,13 @@ export const updatePlaylist = async (
export const clearPlaylist = async (playlistId: string) => {
// Specifying the playlistId without any songs will empty the existing playlist
const { data } = await api.get(`/createPlaylist`, {
params: {
playlistId,
songId: '',
},
});
const { data } = await api.get(`/createPlaylist`, { params: { playlistId, songId: '' } });
return data;
};
export const getGenres = async () => {
const { data } = await api.get(`/getGenres`);
return (data.genres.genre || []).map((entry: any, index: any) => ({
id: entry.value, // List view uses id to match the playing song so we need an arbitrary id here
...entry,
name: entry.value,
index,
uniqueId: nanoid(),
}));
};
export const search2 = async (options: {
query: string;
artistCount?: number;
artistOffset?: 0;
albumCount?: number;
albumOffset?: 0;
songCount?: number;
songOffset?: 0;
musicFolderId?: string | number;
}) => {
const { data } = await api.get(`/search2`, { params: options });
const results = data.searchResult3;
return {
artist: (results.artist || []).map((entry: any, index: any) => ({
...entry,
image: getCoverArtUrl(entry, legacyAuth, 350),
starred: entry.starred || undefined,
type: 'artist',
index,
uniqueId: nanoid(),
})),
album: (results.album || []).map((entry: any, index: any) => ({
...entry,
albumId: entry.id,
image: getCoverArtUrl(entry, legacyAuth, 350),
starred: entry.starred || undefined,
type: 'album',
isDir: false,
index,
uniqueId: nanoid(),
})),
song: (results.song || []).map((entry: any, index: any) => ({
...entry,
streamUrl: getStreamUrl(entry.id, legacyAuth),
image: getCoverArtUrl(entry, legacyAuth, 150),
type: 'music',
starred: entry.starred || undefined,
index,
uniqueId: nanoid(),
})),
};
return (data.genres.genre || []).map((entry: any) => normalizeGenre(entry));
};
export const search3 = async (options: {
@ -911,44 +622,15 @@ export const search3 = async (options: {
}) => {
const { data } = await api.get(`/search3`, { params: options });
const results = data.searchResult3;
return {
artist: (results.artist || []).map((entry: any, index: any) => ({
...entry,
image: getCoverArtUrl(entry, legacyAuth, 350),
starred: entry.starred || undefined,
type: 'artist',
index,
uniqueId: nanoid(),
})),
album: (results.album || []).map((entry: any, index: any) => ({
...entry,
albumId: entry.id,
image: getCoverArtUrl(entry, legacyAuth, 350),
starred: entry.starred || undefined,
type: 'album',
isDir: false,
index,
uniqueId: nanoid(),
})),
song: (results.song || []).map((entry: any, index: any) => ({
...entry,
streamUrl: getStreamUrl(entry.id, legacyAuth),
image: getCoverArtUrl(entry, legacyAuth, 150),
type: 'music',
starred: entry.starred || undefined,
index,
uniqueId: nanoid(),
})),
artist: (data.searchResult3.artist || []).map((entry: any) => normalizeArtist(entry)),
album: (data.searchResult3.album || []).map((entry: any) => normalizeAlbum(entry)),
song: (data.searchResult3.song || []).map((entry: any) => normalizeSong(entry)),
};
};
export const scrobble = async (options: { id: string; time?: number; submission?: boolean }) => {
const { data } = await api.get(`/scrobble`, {
params: options,
});
const { data } = await api.get(`/scrobble`, { params: options });
return data;
};
@ -956,97 +638,49 @@ export const getIndexes = async (options: {
musicFolderId?: string | number;
ifModifiedSince?: any;
}) => {
const { data } = await api.get(`/getIndexes`, {
params: options,
});
const { data } = await api.get(`/getIndexes`, { params: options });
let folders: any[] = [];
const folders: any[] = [];
data.indexes.index.forEach((entry: any) => {
entry.artist.forEach((folder: any) => {
folders.push({
...folder,
title: folder.name,
isDir: true,
image: getCoverArtUrl(folder, legacyAuth, 150),
uniqueId: nanoid(),
type: 'folder',
});
folders.push(normalizeFolder(folder));
});
});
folders = _.flatten(folders);
const child: any[] = [];
(data.indexes?.child || []).forEach((song: any, index: any) =>
child.push({
...song,
index,
type: 'music',
streamUrl: getStreamUrl(song.id, legacyAuth),
image: getCoverArtUrl(song, legacyAuth, 150),
uniqueId: nanoid(),
})
);
return _.concat(folders, child);
const child = (data.indexes?.child || []).map((entry: any) => normalizeSong(entry));
return _.concat(_.flatten(folders), child);
};
export const getMusicFolders = async () => {
const { data } = await api.get(`/getMusicFolders`);
return data?.musicFolders?.musicFolder;
return (data?.musicFolders?.musicFolder || []).map((entry: any) => normalizeFolder(entry));
};
export const getMusicDirectory = async (options: { id: string }) => {
const { data } = await api.get(`/getMusicDirectory`, {
params: options,
});
const { data } = await api.get(`/getMusicDirectory`, { params: options });
const child: any[] = [];
const folders = data.directory?.child?.filter((entry: any) => entry.isDir);
const songs = data.directory?.child?.filter((entry: any) => entry.isDir === false);
(folders || []).forEach((folder: any) =>
child.push({
...folder,
image: getCoverArtUrl(folder, legacyAuth, 150),
uniqueId: nanoid(),
type: 'folder',
})
);
(songs || []).forEach((song: any, index: any) =>
child.push({
...song,
index,
type: 'music',
streamUrl: getStreamUrl(song.id, legacyAuth),
image: getCoverArtUrl(song, legacyAuth, 150),
uniqueId: nanoid(),
})
);
(folders || []).forEach((folder: any) => child.push(normalizeFolder(folder)));
(songs || []).forEach((entry: any) => child.push(normalizeSong(entry)));
return {
...data.directory,
title: data.directory?.name,
child,
};
};
export const getAllDirectorySongs = async (options: { id: string }, data: any[] = []) => {
export const getDirectorySongs = async (options: { id: string }, data: any[] = []) => {
if (options.id === 'stop') {
const songs: any[] = [];
(data || []).forEach((song: any, index: any) => {
(data || []).forEach((song: any) => {
(song?.child || []).forEach((entry: any) => {
if (entry.isDir === false) {
songs.push({
...entry,
index,
type: 'music',
streamUrl: getStreamUrl(entry.id, legacyAuth),
image: getCoverArtUrl(entry, legacyAuth, 150),
uniqueId: nanoid(),
});
songs.push(normalizeSong(entry));
}
});
});
@ -1059,17 +693,17 @@ export const getAllDirectorySongs = async (options: { id: string }, data: any[]
if (res.child.filter((entry: any) => entry.isDir === true).length === 0) {
// Add the last directory if there are no other directories
data.push(res);
return getAllDirectorySongs({ id: 'stop' }, data);
return getDirectorySongs({ id: 'stop' }, data);
}
data.push(res);
const nestedFolders = res.child.filter((entry: any) => entry.isDir === true);
for (let i = 0; i < nestedFolders.length; i += 1) {
await getAllDirectorySongs({ id: nestedFolders[i].id }, data);
await getDirectorySongs({ id: nestedFolders[i].id }, data);
}
return getAllDirectorySongs({ id: 'stop' }, data);
return getDirectorySongs({ id: 'stop' }, data);
})
.catch((err) => console.log(err));

108
src/api/types.ts

@ -3,6 +3,112 @@ export enum Server {
Jellyfin = 'jellyfin',
}
export type ServerType = Server.Subsonic | Server.Jellyfin;
export enum Item {
Album = 'album',
Artist = 'artist',
Folder = 'folder',
Genre = 'genre',
Music = 'music',
Playlist = 'playlist',
}
export type ServerType = Server.Subsonic | Server.Jellyfin;
export type APIEndpoints = 'getPlaylist' | 'getPlaylists';
export interface Album {
id: string;
title: string;
isDir?: boolean;
albumId: string;
artist?: string;
artistId?: string;
songCount: number;
duration: number;
created: string;
year?: number;
genre?: string;
image: string;
starred?: string;
type: Item.Album;
uniqueId: string;
song?: Song[];
}
export interface Artist {
id: string;
title: string;
albumCount: number;
image: string;
starred?: string;
type: Item.Artist;
uniqueId: string;
album: Album[];
}
export interface ArtistInfo {
biography?: string;
lastFmUrl?: string;
imageUrl?: string;
similarArtist?: Artist[];
}
export interface Folder {
id: string;
title: string;
isDir?: boolean;
image: string;
type: Item.Folder;
uniqueId: string;
}
export interface Genre {
id: string;
title: string;
songCount?: number;
albumCount?: number;
type: Item.Genre;
uniqueId: string;
}
export interface Playlist {
id: string;
title: string;
comment?: string;
owner: string;
public?: boolean;
songCount: number;
duration: number;
created: string;
changed: string;
image: string;
type: Item.Playlist;
uniqueId: string;
song?: Song[];
}
export interface Song {
id: string;
parent?: string;
title: string;
isDir?: boolean;
album: string;
albumId?: string;
artist: string;
artistId?: string;
track?: number;
year?: number;
genre?: string;
size: number;
contentType?: string;
suffix?: string;
duration?: number;
bitRate?: number;
path?: string;
playCount?: number;
discNumber?: number;
created: string;
streamUrl: string;
image: string;
starred?: string;
type: Item.Music;
uniqueId: string;
}

10
src/components/card/Card.tsx

@ -2,7 +2,7 @@ import React from 'react';
import { Icon } from 'rsuite';
import { useHistory } from 'react-router-dom';
import cacheImage from '../shared/cacheImage';
import { getAlbum, getPlaylist, getAllArtistSongs } from '../../api/api';
import { getAlbum, getPlaylist, getArtistSongs } from '../../api/api';
import { useAppDispatch, useAppSelector } from '../../redux/hooks';
import {
appendPlayQueue,
@ -76,7 +76,7 @@ const Card = ({
}
if (playClick.type === 'album') {
const res = await getAlbum(playClick.id);
const res = await getAlbum({ id: playClick.id });
const songs = filterPlayQueue(config.playback.filters, res.song);
if (songs.entries.length > 0) {
@ -92,7 +92,7 @@ const Card = ({
}
if (playClick.type === 'artist') {
const res = await getAllArtistSongs(playClick.id);
const res = await getArtistSongs({ id: playClick.id });
const songs = filterPlayQueue(config.playback.filters, res);
if (songs.entries.length > 0) {
@ -122,7 +122,7 @@ const Card = ({
}
if (playClick.type === 'album') {
const res = await getAlbum(playClick.id);
const res = await getAlbum({ id: playClick.id });
const songs = filterPlayQueue(config.playback.filters, res.song);
if (songs.entries.length > 0) {
@ -134,7 +134,7 @@ const Card = ({
}
if (playClick.type === 'artist') {
const res = await getAllArtistSongs(playClick.id);
const res = await getArtistSongs({ id: playClick.id });
const songs = filterPlayQueue(config.playback.filters, res);
if (songs.entries.length > 0) {

56
src/components/dashboard/Dashboard.tsx

@ -28,22 +28,22 @@ const Dashboard = () => {
const { isLoading: isLoadingRecent, data: recentAlbums }: any = useQuery(
['recentAlbums', musicFolder],
() => getAlbums({ type: 'recent', size: 20, musicFolderId: musicFolder })
() => getAlbums({ type: 'recent', size: 20, offset: 0, musicFolderId: musicFolder })
);
const { isLoading: isLoadingNewest, data: newestAlbums }: any = useQuery(
['newestAlbums', musicFolder],
() => getAlbums({ type: 'newest', size: 20, musicFolderId: musicFolder })
() => getAlbums({ type: 'newest', size: 20, offset: 0, musicFolderId: musicFolder })
);
const { isLoading: isLoadingRandom, data: randomAlbums }: any = useQuery(
['randomAlbums', musicFolder],
() => getAlbums({ type: 'random', size: 20, musicFolderId: musicFolder })
() => getAlbums({ type: 'random', size: 20, offset: 0, musicFolderId: musicFolder })
);
const { isLoading: isLoadingFrequent, data: frequentAlbums }: any = useQuery(
['frequentAlbums', musicFolder],
() => getAlbums({ type: 'frequent', size: 20, musicFolderId: musicFolder })
() => getAlbums({ type: 'frequent', size: 20, offset: 0, musicFolderId: musicFolder })
);
const handleFavorite = async (rowData: any) => {
@ -51,33 +51,33 @@ const Dashboard = () => {
await star(rowData.id, 'album');
dispatch(setStar({ id: [rowData.id], type: 'star' }));
queryClient.setQueryData(['recentAlbums', musicFolder], (oldData: any) => {
const starredIndices = _.keys(_.pickBy(oldData?.album, { id: rowData.id }));
const starredIndices = _.keys(_.pickBy(oldData, { id: rowData.id }));
starredIndices.forEach((index) => {
oldData.album[index].starred = Date.now();
oldData[index].starred = Date.now();
});
return oldData;
});
queryClient.setQueryData(['newestAlbums', musicFolder], (oldData: any) => {
const starredIndices = _.keys(_.pickBy(oldData?.album, { id: rowData.id }));
const starredIndices = _.keys(_.pickBy(oldData, { id: rowData.id }));
starredIndices.forEach((index) => {
oldData.album[index].starred = Date.now();
oldData[index].starred = Date.now();
});
return oldData;
});
queryClient.setQueryData(['randomAlbums', musicFolder], (oldData: any) => {
const starredIndices = _.keys(_.pickBy(oldData?.album, { id: rowData.id }));
const starredIndices = _.keys(_.pickBy(oldData, { id: rowData.id }));
starredIndices.forEach((index) => {
oldData.album[index].starred = Date.now();
oldData[index].starred = Date.now();
});
return oldData;
});
queryClient.setQueryData(['frequentAlbums', musicFolder], (oldData: any) => {
const starredIndices = _.keys(_.pickBy(oldData?.album, { id: rowData.id }));
const starredIndices = _.keys(_.pickBy(oldData, { id: rowData.id }));
starredIndices.forEach((index) => {
oldData.album[index].starred = Date.now();
oldData[index].starred = Date.now();
});
return oldData;
@ -86,33 +86,33 @@ const Dashboard = () => {
await unstar(rowData.id, 'album');
dispatch(setStar({ id: [rowData.id], type: 'unstar' }));
queryClient.setQueryData(['recentAlbums', musicFolder], (oldData: any) => {
const starredIndices = _.keys(_.pickBy(oldData?.album, { id: rowData.id }));
const starredIndices = _.keys(_.pickBy(oldData, { id: rowData.id }));
starredIndices.forEach((index) => {
oldData.album[index].starred = undefined;
oldData[index].starred = undefined;
});
return oldData;
});
queryClient.setQueryData(['newestAlbums', musicFolder], (oldData: any) => {
const starredIndices = _.keys(_.pickBy(oldData?.album, { id: rowData.id }));
const starredIndices = _.keys(_.pickBy(oldData, { id: rowData.id }));
starredIndices.forEach((index) => {
oldData.album[index].starred = undefined;
oldData[index].starred = undefined;
});
return oldData;
});
queryClient.setQueryData(['randomAlbums', musicFolder], (oldData: any) => {
const starredIndices = _.keys(_.pickBy(oldData?.album, { id: rowData.id }));
const starredIndices = _.keys(_.pickBy(oldData, { id: rowData.id }));
starredIndices.forEach((index) => {
oldData.album[index].starred = undefined;
oldData[index].starred = undefined;
});
return oldData;
});
queryClient.setQueryData(['frequentAlbums', musicFolder], (oldData: any) => {
const starredIndices = _.keys(_.pickBy(oldData?.album, { id: rowData.id }));
const starredIndices = _.keys(_.pickBy(oldData, { id: rowData.id }));
starredIndices.forEach((index) => {
oldData.album[index].starred = undefined;
oldData[index].starred = undefined;
});
return oldData;
@ -134,10 +134,10 @@ const Dashboard = () => {
<>
<ScrollingMenu
title="Recently Played"
data={recentAlbums.album}
data={recentAlbums}
cardTitle={{
prefix: '/library/album',
property: 'name',
property: 'title',
urlProperty: 'albumId',
}}
cardSubtitle={{
@ -158,10 +158,10 @@ const Dashboard = () => {
<ScrollingMenu
title="Recently Added"
data={newestAlbums.album}
data={newestAlbums}
cardTitle={{
prefix: '/library/album',
property: 'name',
property: 'title',
urlProperty: 'albumId',
}}
cardSubtitle={{
@ -182,10 +182,10 @@ const Dashboard = () => {
<ScrollingMenu
title="Random"
data={randomAlbums.album}
data={randomAlbums}
cardTitle={{
prefix: '/library/album',
property: 'name',
property: 'title',
urlProperty: 'albumId',
}}
cardSubtitle={{
@ -206,10 +206,10 @@ const Dashboard = () => {
<ScrollingMenu
title="Most Played"
data={frequentAlbums.album}
data={frequentAlbums}
cardTitle={{
prefix: '/library/album',
property: 'name',
property: 'title',
urlProperty: 'albumId',
}}
cardSubtitle={{

16
src/components/library/AlbumList.tsx

@ -9,7 +9,7 @@ import ListViewType from '../viewtypes/ListViewType';
import useSearchQuery from '../../hooks/useSearchQuery';
import GenericPageHeader from '../layout/GenericPageHeader';
import GenericPage from '../layout/GenericPage';
import { getAlbumsDirect, getAllAlbums, getGenres, star, unstar } from '../../api/api';
import { getAlbums, getGenres, star, unstar } from '../../api/api';
import PageLoader from '../loader/PageLoader';
import { useAppDispatch, useAppSelector } from '../../redux/hooks';
import {
@ -56,16 +56,18 @@ const AlbumList = () => {
['albumList', album.active.filter, musicFolder],
() =>
album.active.filter === 'random'
? getAlbumsDirect({
? getAlbums({
type: 'random',
size: config.lookAndFeel.gridView.cardSize,
offset: 0,
musicFolderId: musicFolder,
})
: getAllAlbums({
: getAlbums({
type: album.active.filter,
size: 500,
offset: 0,
musicFolderId: musicFolder,
recursive: true,
}),
{
cacheTime: 3600000, // Stay in cache for 1 hour
@ -77,8 +79,8 @@ const AlbumList = () => {
return res.map((genre: any) => {
if (genre.albumCount !== 0) {
return {
label: `${genre.value} (${genre.albumCount})`,
value: genre.value,
label: `${genre.title} (${genre.albumCount})`,
value: genre.title,
role: 'Genre',
};
}
@ -86,7 +88,7 @@ const AlbumList = () => {
});
});
const filteredData = useSearchQuery(misc.searchQuery, albums, [
'name',
'title',
'artist',
'genre',
'year',
@ -228,7 +230,7 @@ const AlbumList = () => {
data={misc.searchQuery !== '' ? filteredData : albums}
cardTitle={{
prefix: '/library/album',
property: 'name',
property: 'title',
urlProperty: 'albumId',
}}
cardSubtitle={{

4
src/components/library/AlbumView.tsx

@ -68,7 +68,7 @@ const AlbumView = ({ ...rest }: any) => {
const albumId = rest.id ? rest.id : id;
const { isLoading, isError, data, error }: any = useQuery(['album', albumId], () =>
getAlbum(albumId)
getAlbum({ id: albumId })
);
const filteredData = useSearchQuery(misc.searchQuery, data?.song, [
'title',
@ -240,7 +240,7 @@ const AlbumView = ({ ...rest }: any) => {
id: data.albumId,
}}
imageHeight={200}
title={data.name}
title={data.title}
showTitleTooltip
subtitle={
<div>

4
src/components/library/ArtistList.tsx

@ -45,7 +45,7 @@ const ArtistList = () => {
staleTime: Infinity, // Only allow manual refresh
}
);
const filteredData = useSearchQuery(misc.searchQuery, artists, ['name']);
const filteredData = useSearchQuery(misc.searchQuery, artists, ['title']);
let timeout: any = null;
const handleRowClick = (e: any, rowData: any, tableData: any) => {
@ -151,7 +151,7 @@ const ArtistList = () => {
data={misc.searchQuery !== '' ? filteredData : artists}
cardTitle={{
prefix: '/library/artist',
property: 'name',
property: 'title',
urlProperty: 'id',
}}
cardSubtitle={{

22
src/components/library/ArtistView.tsx

@ -16,7 +16,7 @@ import {
} from '../shared/ToolbarButtons';
import {
getAlbum,
getAllArtistSongs,
getArtistSongs,
getArtist,
getArtistInfo,
getDownloadUrl,
@ -75,17 +75,17 @@ const ArtistView = ({ ...rest }: any) => {
const { id } = useParams<ArtistParams>();
const artistId = rest.id ? rest.id : id;
const { isLoading, isError, data, error }: any = useQuery(['artist', artistId], () =>
getArtist(artistId)
getArtist({ id: artistId })
);
const {
isLoading: isLoadingAI,
isError: isErrorAI,
data: artistInfo,
error: errorAI,
}: any = useQuery(['artistInfo', artistId], () => getArtistInfo(artistId, 8));
}: any = useQuery(['artistInfo', artistId], () => getArtistInfo({ id: artistId, count: 8 }));
const filteredData = useSearchQuery(misc.searchQuery, data?.album, [
'name',
'title',
'artist',
'genre',
'year',
@ -125,7 +125,7 @@ const ArtistView = ({ ...rest }: any) => {
};
const handlePlay = async () => {
const res = await getAllArtistSongs(data.id);
const res = await getArtistSongs({ id: data.id });
const songs = filterPlayQueue(config.playback.filters, res);
if (songs.entries.length > 0) {
@ -141,7 +141,7 @@ const ArtistView = ({ ...rest }: any) => {
};
const handlePlayAppend = async (type: 'next' | 'later') => {
const res = await getAllArtistSongs(data.id);
const res = await getArtistSongs({ id: data.id });
const songs = filterPlayQueue(config.playback.filters, res);
if (songs.entries.length > 0) {
@ -189,11 +189,11 @@ const ArtistView = ({ ...rest }: any) => {
for (let i = 0; i < data.album.length; i += 1) {
// eslint-disable-next-line no-await-in-loop
const albumRes = await getAlbum(data.album[i].id);
const albumRes = await getAlbum({ id: data.album[i].id });
if (albumRes.song[0]?.parent) {
downloadUrls.push(getDownloadUrl(albumRes.song[0].parent));
} else {
notifyToast('warning', `[${albumRes.name}] No parent album found`);
notifyToast('warning', `[${albumRes.title}] No parent album found`);
}
}
@ -295,7 +295,7 @@ const ArtistView = ({ ...rest }: any) => {
id: data.id,
}}
imageHeight={185}
title={data.name}
title={data.title}
showTitleTooltip
subtitle={
<>
@ -393,7 +393,7 @@ const ArtistView = ({ ...rest }: any) => {
}
}}
>
{artist.name}
{artist.title}
</StyledTag>
))}
</TagGroup>
@ -455,7 +455,7 @@ const ArtistView = ({ ...rest }: any) => {
data={misc.searchQuery !== '' ? filteredData : data.album}
cardTitle={{
prefix: '/library/album',
property: 'name',
property: 'title',
urlProperty: 'albumId',
}}
cardSubtitle={{

10
src/components/library/FolderList.tsx

@ -61,7 +61,7 @@ const FolderList = () => {
const filteredData = useSearchQuery(
misc.searchQuery,
folderData?.id ? folderData?.child : indexData,
['name', 'title', 'artist', 'album', 'year', 'genre', 'path']
['title', 'artist', 'album', 'year', 'genre', 'path']
);
useEffect(() => {
@ -98,7 +98,7 @@ const FolderList = () => {
const selected = folderData?.id ? folderData?.child : indexData?.child;
dispatch(
setPlayQueueByRowClick({
entries: selected.filter((entry: any) => entry.isDir === false),
entries: selected.filter((entry: any) => entry?.isDir === false),
currentIndex: rowData.index,
currentSongId: rowData.id,
uniqueSongId: rowData.uniqueId,
@ -149,8 +149,8 @@ const FolderList = () => {
header={
<GenericPageHeader
title={`${
folderData?.name
? folderData.name
folderData?.title
? folderData.title
: isLoadingFolderData
? 'Loading...'
: 'Select a folder'
@ -167,7 +167,7 @@ const FolderList = () => {
data={musicFolders}
defaultValue={musicFolder}
valueKey="id"
labelKey="name"
labelKey="title"
onChange={(e: any) => {
setMusicFolder(e);
}}

6
src/components/library/GenreList.tsx

@ -27,7 +27,7 @@ const GenreList = () => {
const res = await getGenres();
return _.orderBy(res, 'songCount', 'desc');
});
const filteredData = useSearchQuery(misc.searchQuery, genres, ['value']);
const filteredData = useSearchQuery(misc.searchQuery, genres, ['title']);
let timeout: any = null;
const handleRowClick = (e: any, rowData: any, tableData: any) => {
@ -48,12 +48,12 @@ const GenreList = () => {
const handleRowDoubleClick = (rowData: any) => {
window.clearTimeout(timeout);
timeout = null;
dispatch(setActive({ ...album.active, filter: rowData.value }));
dispatch(setActive({ ...album.active, filter: rowData.title }));
dispatch(clearSelected());
// Needs a small delay or the filter won't set properly when navigating to the album list
setTimeout(() => {
history.push(`/library/album?sortType=${rowData.value}`);
history.push(`/library/album?sortType=${rowData.title}`);
}, 50);
};

29
src/components/player/NowPlayingMiniView.tsx

@ -82,8 +82,8 @@ const NowPlayingMiniView = () => {
const genresOrderedBySongCount = _.orderBy(res, 'songCount', 'desc');
return genresOrderedBySongCount.map((genre: any) => {
return {
label: `${genre.value} (${genre.songCount})`,
value: genre.value,
label: `${genre.title} (${genre.songCount})`,
title: genre.title,
role: 'Genre',
};
});
@ -190,7 +190,7 @@ const NowPlayingMiniView = () => {
const cleanedSongs = filterPlayQueue(
config.playback.filters,
res.song.filter((song: any) => {
res.filter((song: any) => {
// Remove invalid songs that may break the player
return song.bitRate && song.duration;
})
@ -210,7 +210,7 @@ const NowPlayingMiniView = () => {
notifyToast(
'info',
getPlayedSongsNotification({
original: res.song.length,
original: res.length,
filtered: cleanedSongs.count.filtered,
type: 'play',
})
@ -224,7 +224,7 @@ const NowPlayingMiniView = () => {
notifyToast(
'info',
getPlayedSongsNotification({
original: res.song.length,
original: res.length,
filtered: cleanedSongs.count.filtered,
type: 'add',
})
@ -238,7 +238,7 @@ const NowPlayingMiniView = () => {
notifyToast(
'info',
getPlayedSongsNotification({
original: res.song.length,
original: res.length,
filtered: cleanedSongs.count.filtered,
type: 'add',
})
@ -300,7 +300,8 @@ const NowPlayingMiniView = () => {
<Whisper
ref={autoPlaylistTriggerRef}
placement="autoVertical"
trigger="none"
trigger="click"
enterable
speaker={
<StyledPopover>
<ControlLabel>How many tracks? (1-500)*</ControlLabel>
@ -356,6 +357,8 @@ const NowPlayingMiniView = () => {
container={() => genrePickerContainerRef.current}
data={!isLoadingGenres ? genres : []}
value={randomPlaylistGenre}
valueKey="title"
labelKey="label"
virtualized
onChange={(e: string) => setRandomPlaylistGenre(e)}
/>
@ -370,7 +373,7 @@ const NowPlayingMiniView = () => {
data={!isLoadingMusicFolders ? musicFolders : []}
defaultValue={musicFolder}
valueKey="id"
labelKey="name"
labelKey="title"
onChange={(e: any) => {
setMusicFolder(e);
}}
@ -411,15 +414,7 @@ const NowPlayingMiniView = () => {
</StyledPopover>
}
>
<AutoPlaylistButton
size="xs"
noText
onClick={() =>
autoPlaylistTriggerRef.current.state.isOverlayShown
? autoPlaylistTriggerRef.current.close()
: autoPlaylistTriggerRef.current.open()
}
/>
<AutoPlaylistButton size="xs" noText />
</Whisper>
<MoveTopButton
size="xs"

28
src/components/player/NowPlayingView.tsx

@ -97,8 +97,8 @@ const NowPlayingView = () => {
const genresOrderedBySongCount = _.orderBy(res, 'songCount', 'desc');
return genresOrderedBySongCount.map((genre: any) => {
return {
label: `${genre.value} (${genre.songCount})`,
value: genre.value,
label: `${genre.title} (${genre.songCount})`,
title: genre.title,
role: 'Genre',
};
});
@ -201,7 +201,7 @@ const NowPlayingView = () => {
const cleanedSongs = filterPlayQueue(
config.playback.filters,
res.song.filter((song: any) => {
res.filter((song: any) => {
// Remove invalid songs that may break the player
return song.bitRate && song.duration;
})
@ -221,7 +221,7 @@ const NowPlayingView = () => {
notifyToast(
'info',
getPlayedSongsNotification({
original: res.song.length,
original: res.length,
filtered: cleanedSongs.count.filtered,
type: 'play',
})
@ -235,7 +235,7 @@ const NowPlayingView = () => {
notifyToast(
'info',
getPlayedSongsNotification({
original: res.song.length,
original: res.length,
filtered: cleanedSongs.count.filtered,
type: 'add',
})
@ -249,7 +249,7 @@ const NowPlayingView = () => {
notifyToast(
'info',
getPlayedSongsNotification({
original: res.song.length,
original: res.length,
filtered: cleanedSongs.count.filtered,
type: 'add',
})
@ -310,7 +310,8 @@ const NowPlayingView = () => {
<Whisper
ref={autoPlaylistTriggerRef}
placement="autoVertical"
trigger="none"
trigger="click"
enterable
speaker={
<StyledPopover>
<ControlLabel>How many tracks? (1-500)*</ControlLabel>
@ -367,6 +368,8 @@ const NowPlayingView = () => {
container={() => genrePickerContainerRef.current}
data={genres}
value={randomPlaylistGenre}
valueKey="title"
labelKey="label"
virtualized
onChange={(e: string) => setRandomPlaylistGenre(e)}
/>
@ -381,7 +384,7 @@ const NowPlayingView = () => {
data={musicFolders}
defaultValue={musicFolder}
valueKey="id"
labelKey="name"
labelKey="title"
onChange={(e: any) => {
setMusicFolder(e);
}}
@ -421,14 +424,7 @@ const NowPlayingView = () => {
</StyledPopover>
}
>
<AutoPlaylistButton
size="sm"
onClick={() =>
autoPlaylistTriggerRef.current.state.isOverlayShown
? autoPlaylistTriggerRef.current.close()
: autoPlaylistTriggerRef.current.open()
}
/>
<AutoPlaylistButton size="sm" />
</Whisper>
<ButtonGroup>
<MoveTopButton

9
src/components/playlist/PlaylistList.tsx

@ -29,13 +29,12 @@ const PlaylistList = () => {
const config = useAppSelector((state) => state.config);
const misc = useAppSelector((state) => state.misc);
const playlistTriggerRef = useRef<any>();
const [sortBy] = useState('name');
const [newPlaylistName, setNewPlaylistName] = useState('');
const [viewType, setViewType] = useState(settings.getSync('playlistViewType') || 'list');
const { isLoading, isError, data: playlists, error }: any = useQuery(['playlists', sortBy], () =>
getPlaylists(sortBy)
const { isLoading, isError, data: playlists, error }: any = useQuery(['playlists'], () =>
getPlaylists()
);
const filteredData = useSearchQuery(misc.searchQuery, playlists, ['name', 'comment', 'owner']);
const filteredData = useSearchQuery(misc.searchQuery, playlists, ['title', 'comment', 'owner']);
const handleCreatePlaylist = async (name: string) => {
try {
@ -174,7 +173,7 @@ const PlaylistList = () => {
data={misc.searchQuery === '' ? playlists : filteredData}
cardTitle={{
prefix: 'playlist',
property: 'name',
property: 'title',
urlProperty: 'id',
}}
cardSubtitle={{

4
src/components/playlist/PlaylistView.tsx

@ -135,7 +135,7 @@ const PlaylistView = ({ ...rest }) => {
useEffect(() => {
// Set the local playlist data on any changes
dispatch(setPlaylistData(data?.song));
setEditName(data?.name);
setEditName(data?.title);
setEditDescription(data?.comment);
setEditPublic(data?.public);
}, [data, dispatch]);
@ -402,7 +402,7 @@ const PlaylistView = ({ ...rest }) => {
id: data.id,
}}
imageHeight={184}
title={data.name}
title={data.title}
subtitle={
<div>
<PageHeaderSubtitleDataLine $top>

4
src/components/search/SearchView.tsx

@ -164,7 +164,7 @@ const SearchView = () => {
data={data.artist}
cardTitle={{
prefix: '/library/artist',
property: 'name',
property: 'title',
urlProperty: 'id',
}}
cardSubtitle={{
@ -181,7 +181,7 @@ const SearchView = () => {
data={data.album}
cardTitle={{
prefix: '/library/album',
property: 'name',
property: 'title',
urlProperty: 'albumId',
}}
cardSubtitle={{

46
src/components/settings/ListViewColumns.ts

@ -476,7 +476,7 @@ export const albumColumnList = [
label: 'Title',
value: {
id: 'Title',
dataKey: 'name',
dataKey: 'title',
alignment: 'left',
resizable: true,
width: 300,
@ -594,7 +594,7 @@ export const albumColumnListAuto = [
label: 'Title',
value: {
id: 'Title',
dataKey: 'name',
dataKey: 'title',
alignment: 'left',
flexGrow: 5,
label: 'Title',
@ -752,7 +752,7 @@ export const playlistColumnList = [
label: 'Title',
value: {
id: 'Title',
dataKey: 'name',
dataKey: 'title',
alignment: 'left',
resizable: true,
width: 300,
@ -879,7 +879,7 @@ export const playlistColumnListAuto = [
label: 'Title',
value: {
id: 'Title',
dataKey: 'name',
dataKey: 'title',
alignment: 'left',
flexGrow: 5,
label: 'Title',
@ -956,14 +956,14 @@ export const artistColumnList = [
},
},
{
label: 'Name',
label: 'Title',
value: {
id: 'Name',
dataKey: 'name',
id: 'Title',
dataKey: 'title',
alignment: 'left',
resizable: true,
width: 300,
label: 'Name',
label: 'Title',
},
},
];
@ -1012,13 +1012,13 @@ export const artistColumnListAuto = [
},
},
{
label: 'Name',
label: 'Title',
value: {
id: 'Name',
dataKey: 'name',
id: 'Title',
dataKey: 'title',
alignment: 'left',
flexGrow: 5,
label: 'Name',
label: 'Title',
},
},
];
@ -1028,13 +1028,13 @@ export const artistColumnPicker = [
{ label: 'Album Count' },
{ label: 'CoverArt' },
{ label: 'Favorite' },
{ label: 'Name' },
{ label: 'Title' },
];
export const genreColumnPicker = [
{ label: '#' },
{ label: 'Album Count' },
{ label: 'Name' },
{ label: 'Title' },
{ label: 'Track Count' },
];
@ -1062,14 +1062,14 @@ export const genreColumnList = [
},
},
{
label: 'Name',
label: 'Title',
value: {
id: 'Name',
dataKey: 'name',
id: 'Title',
dataKey: 'title',
alignment: 'left',
resizable: true,
width: 300,
label: 'Name',
label: 'Title',
},
},
{
@ -1108,17 +1108,17 @@ export const genreColumnListAuto = [
},
},
{
label: 'Name',
label: 'Title',
value: {
id: 'Name',
dataKey: 'name',
id: 'Title',
dataKey: 'title',
alignment: 'left',
flexGrow: 5,
label: 'Name',
label: 'Title',
},
},
{
label: 'Tracks',
label: 'Track Count',
value: {
id: 'Tracks',
dataKey: 'songCount',

26
src/components/shared/ContextMenu.tsx

@ -14,8 +14,8 @@ import {
getAlbum,
getPlaylist,
deletePlaylist,
getAllArtistSongs,
getAllDirectorySongs,
getArtistSongs,
getDirectorySongs,
} from '../../api/api';
import { useAppDispatch, useAppSelector } from '../../redux/hooks';
import {
@ -120,7 +120,7 @@ export const GlobalContextMenu = () => {
const [indexToMoveTo, setIndexToMoveTo] = useState(0);
const playlistPickerContainerRef = useRef(null);
const { data: playlists }: any = useQuery(['playlists', 'name'], () => getPlaylists('name'));
const { data: playlists }: any = useQuery(['playlists'], () => getPlaylists());
const handlePlay = async () => {
dispatch(setContextMenu({ show: false }));
@ -135,7 +135,7 @@ export const GlobalContextMenu = () => {
});
for (let i = 0; i < folders.length; i += 1) {
promises.push(getAllDirectorySongs({ id: folders[i].id }));
promises.push(getDirectorySongs({ id: folders[i].id }));
}
const res = await Promise.all(promises);
@ -172,7 +172,7 @@ export const GlobalContextMenu = () => {
notifyToast('info', getPlayedSongsNotification({ ...songs.count, type: 'play' }));
} else if (misc.contextMenu.type === 'album') {
for (let i = 0; i < multiSelect.selected.length; i += 1) {
promises.push(getAlbum(multiSelect.selected[i].id));
promises.push(getAlbum({ id: multiSelect.selected[i].id }));
}
const res = await Promise.all(promises);
@ -191,7 +191,7 @@ export const GlobalContextMenu = () => {
notifyToast('info', getPlayedSongsNotification({ ...songs.count, type: 'play' }));
} else if (misc.contextMenu.type === 'artist') {
for (let i = 0; i < multiSelect.selected.length; i += 1) {
promises.push(getAllArtistSongs(multiSelect.selected[i].id));
promises.push(getArtistSongs({ id: multiSelect.selected[i].id }));
}
const res = await Promise.all(promises);
@ -223,7 +223,7 @@ export const GlobalContextMenu = () => {
});
for (let i = 0; i < folders.length; i += 1) {
promises.push(getAllDirectorySongs({ id: multiSelect.selected[i].id }));
promises.push(getDirectorySongs({ id: multiSelect.selected[i].id }));
}
const res = await Promise.all(promises);
@ -252,7 +252,7 @@ export const GlobalContextMenu = () => {
notifyToast('info', getPlayedSongsNotification({ ...songs.count, type: 'add' }));
} else if (misc.contextMenu.type === 'album') {
for (let i = 0; i < multiSelect.selected.length; i += 1) {
promises.push(getAlbum(multiSelect.selected[i].id));
promises.push(getAlbum({ id: multiSelect.selected[i].id }));
}
const res = await Promise.all(promises);
@ -266,7 +266,7 @@ export const GlobalContextMenu = () => {
notifyToast('info', getPlayedSongsNotification({ ...songs.count, type: 'add' }));
} else if (misc.contextMenu.type === 'artist') {
for (let i = 0; i < multiSelect.selected.length; i += 1) {
promises.push(getAllArtistSongs(multiSelect.selected[i].id));
promises.push(getArtistSongs({ id: multiSelect.selected[i].id }));
}
const res = await Promise.all(promises);
@ -298,7 +298,7 @@ export const GlobalContextMenu = () => {
notifyToast(
'success',
`Added ${songCount} song(s) to playlist ${
playlists.find((pl: any) => pl.id === playlistId)?.name
playlists.find((pl: any) => pl.id === playlistId)?.title
}`,
<>
<StyledButton
@ -332,7 +332,7 @@ export const GlobalContextMenu = () => {
});
for (let i = 0; i < folders.length; i += 1) {
promises.push(getAllDirectorySongs({ id: multiSelect.selected[i].id }));
promises.push(getDirectorySongs({ id: multiSelect.selected[i].id }));
}
const folderSongs = await Promise.all(promises);
@ -363,7 +363,7 @@ export const GlobalContextMenu = () => {
}
} else if (misc.contextMenu.type === 'album') {
for (let i = 0; i < multiSelect.selected.length; i += 1) {
promises.push(getAlbum(multiSelect.selected[i].id));
promises.push(getAlbum({ id: multiSelect.selected[i].id }));
}
res = await Promise.all(promises);
@ -700,7 +700,7 @@ export const GlobalContextMenu = () => {
data={playlists}
placement="autoVerticalStart"
virtualized
labelKey="name"
labelKey="title"
valueKey="id"
width={200}
onChange={(e: any) => setSelectedPlaylistId(e)}

10
src/components/starred/StarredView.tsx

@ -50,10 +50,10 @@ const StarredView = () => {
? data?.album
: data?.artist,
favorite.active.tab === 'tracks'
? ['title', 'artist', 'album', 'name', 'genre']
? ['title', 'artist', 'album', 'genre']
: favorite.active.tab === 'albums'
? ['name', 'artist', 'genre', 'year']
: ['name']
? ['title', 'artist', 'genre', 'year']
: ['title']
);
let timeout: any = null;
@ -230,7 +230,7 @@ const StarredView = () => {
data={misc.searchQuery !== '' ? filteredData : data.album}
cardTitle={{
prefix: '/library/album',
property: 'name',
property: 'title',
urlProperty: 'albumId',
}}
cardSubtitle={{
@ -279,7 +279,7 @@ const StarredView = () => {
data={misc.searchQuery !== '' ? filteredData : data.artist}
cardTitle={{
prefix: '/library/artist',
property: 'name',
property: 'title',
urlProperty: 'id',
}}
cardSubtitle={{

Loading…
Cancel
Save