Browse Source

Add jf artist endpoints

- Combined artist and artistInfo types
master
jeffvli 3 years ago
committed by Jeff
parent
commit
cadd1d9f56
  1. 50
      src/api/api.ts
  2. 11
      src/api/controller.ts
  3. 66
      src/api/jellyfinApi.ts
  4. 116
      src/components/library/ArtistView.tsx
  5. 5
      src/components/player/PlayerBar.tsx
  6. 9
      src/types.ts

50
src/api/api.ts

@ -176,10 +176,10 @@ const normalizeSong = (item: any) => {
albumId: item.albumId, albumId: item.albumId,
albumArtist: item.artist, albumArtist: item.artist,
albumArtistId: item.artistId, albumArtistId: item.artistId,
artist: [{ id: item.artistId, title: item.artist }], artist: item.artist && [{ id: item.artistId, title: item.artist }],
track: item.track, track: item.track,
year: item.year, year: item.year,
genre: [{ id: item.genre, title: item.genre }], genre: item.genre && [{ id: item.genre, title: item.genre }],
albumGenre: item.genre, albumGenre: item.genre,
size: item.size, size: item.size,
contentType: item.contentType, contentType: item.contentType,
@ -228,21 +228,19 @@ const normalizeArtist = (item: any) => {
albumCount: item.albumCount, albumCount: item.albumCount,
image: getCoverArtUrl(item, legacyAuth, 350), image: getCoverArtUrl(item, legacyAuth, 350),
starred: item.starred, starred: item.starred,
info: {
biography: item.biography,
externalUrl: item.lastFmUrl && [{ id: item.lastFmUrl, title: 'Last.FM' }],
imageUrl:
!item.externalImageUrl?.match('2a96cbd8b46e442fc41c2b86b821562f') && item.externalImageUrl,
similarArtist: (item.similarArtist || []).map((entry: any) => normalizeArtist(entry)),
},
type: Item.Artist, type: Item.Artist,
uniqueId: nanoid(), uniqueId: nanoid(),
album: (item.album || []).map((entry: any) => normalizeAlbum(entry)), album: (item.album || []).map((entry: any) => normalizeAlbum(entry)),
}; };
}; };
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)),
};
};
const normalizePlaylist = (item: any) => { const normalizePlaylist = (item: any) => {
return { return {
id: item.id, id: item.id,
@ -390,7 +388,17 @@ export const getRandomSongs = async (options: {
export const getArtist = async (options: { id: string }) => { export const getArtist = async (options: { id: string }) => {
const { data } = await api.get(`/getArtist`, { params: options }); const { data } = await api.get(`/getArtist`, { params: options });
return normalizeArtist(data.artist); const { data: infoData } = await api.get(`/getArtistInfo2`, {
params: { id: options.id, count: 8 },
});
return normalizeArtist({
...data.artist,
biography: infoData.artistInfo2.biography,
lastFmUrl: infoData.artistInfo2.lastFmUrl,
externalImageUrl: infoData.artistInfo2.largeImageUrl,
similarArtist: infoData.artistInfo2.similarArtist,
});
}; };
export const getArtists = async (options: { musicFolderId?: string | number }) => { export const getArtists = async (options: { musicFolderId?: string | number }) => {
@ -399,21 +407,21 @@ export const getArtists = async (options: { musicFolderId?: string | number }) =
return (artists || []).map((entry: any) => normalizeArtist(entry)); return (artists || []).map((entry: any) => normalizeArtist(entry));
}; };
export const getArtistInfo = async (options: { id: string; count: number }) => {
const { data } = await api.get(`/getArtistInfo2`, { params: options });
return normalizeArtistInfo(data.artistInfo2);
};
export const getArtistSongs = async (options: { id: string }) => { export const getArtistSongs = async (options: { id: string }) => {
const promises = []; const promises = [];
const artist = await getArtist({ id: options.id }); const { data } = await api.get(`/getArtist`, { params: options });
console.log(`artist`, data.artist);
for (let i = 0; i < artist.album.length; i += 1) { for (let i = 0; i < data.artist.album.length; i += 1) {
promises.push(getAlbum({ id: artist.album[i].id })); promises.push(api.get(`/getAlbum`, { params: { id: data.artist.album[i].id } }));
} }
const res = await Promise.all(promises); const res = await Promise.all(promises);
return (_.flatten(_.map(res, 'song')) || []).map((entry: any) => normalizeSong(entry));
return _.flatten(res.map((entry: any) => entry.data.album.song) || []).map((entry: any) =>
normalizeSong(entry)
);
}; };
export const startScan = async () => { export const startScan = async () => {

11
src/api/controller.ts

@ -8,7 +8,6 @@ import {
getAlbum, getAlbum,
getAlbums, getAlbums,
getArtist, getArtist,
getArtistInfo,
getArtists, getArtists,
getArtistSongs, getArtistSongs,
getDownloadUrl, getDownloadUrl,
@ -34,6 +33,9 @@ import {
updatePlaylistSongsLg, updatePlaylistSongsLg,
} from './api'; } from './api';
import { import {
getArtist as jfGetArtist,
getArtists as jfGetArtists,
getArtistSongs as jfGetArtistSongs,
getAlbum as jfGetAlbum, getAlbum as jfGetAlbum,
getAlbums as jfGetAlbums, getAlbums as jfGetAlbums,
getPlaylist as jfGetPlaylist, getPlaylist as jfGetPlaylist,
@ -49,10 +51,9 @@ const endpoints = [
{ id: 'getAlbum', endpoint: { subsonic: getAlbum, jellyfin: jfGetAlbum } }, { id: 'getAlbum', endpoint: { subsonic: getAlbum, jellyfin: jfGetAlbum } },
{ id: 'getAlbums', endpoint: { subsonic: getAlbums, jellyfin: jfGetAlbums } }, { id: 'getAlbums', endpoint: { subsonic: getAlbums, jellyfin: jfGetAlbums } },
{ id: 'getRandomSongs', endpoint: { subsonic: getRandomSongs, jellyfin: undefined } }, { id: 'getRandomSongs', endpoint: { subsonic: getRandomSongs, jellyfin: undefined } },
{ id: 'getArtist', endpoint: { subsonic: getArtist, jellyfin: undefined } }, { id: 'getArtist', endpoint: { subsonic: getArtist, jellyfin: jfGetArtist } },
{ id: 'getArtists', endpoint: { subsonic: getArtists, jellyfin: undefined } }, { id: 'getArtists', endpoint: { subsonic: getArtists, jellyfin: jfGetArtists } },
{ id: 'getArtistInfo', endpoint: { subsonic: getArtistInfo, jellyfin: undefined } }, { id: 'getArtistSongs', endpoint: { subsonic: getArtistSongs, jellyfin: jfGetArtistSongs } },
{ id: 'getArtistSongs', endpoint: { subsonic: getArtistSongs, jellyfin: undefined } },
{ id: 'startScan', endpoint: { subsonic: startScan, jellyfin: undefined } }, { id: 'startScan', endpoint: { subsonic: startScan, jellyfin: undefined } },
{ id: 'getScanStatus', endpoint: { subsonic: getScanStatus, jellyfin: undefined } }, { id: 'getScanStatus', endpoint: { subsonic: getScanStatus, jellyfin: undefined } },
{ id: 'star', endpoint: { subsonic: star, jellyfin: undefined } }, { id: 'star', endpoint: { subsonic: star, jellyfin: undefined } },

66
src/api/jellyfinApi.ts

@ -75,7 +75,7 @@ const getCoverArtUrl = (item: any, size?: number) => {
const normalizeItem = (item: any) => { const normalizeItem = (item: any) => {
return { return {
id: item.Id, id: item.Id || item.Url,
title: item.Name, title: item.Name,
}; };
}; };
@ -134,6 +134,25 @@ const normalizeAlbum = (item: any) => {
}; };
}; };
const normalizeArtist = (item: any) => {
return {
id: item.Id,
title: item.Name,
albumCount: item.AlbumCount,
image: getCoverArtUrl(item, 350),
starred: item.UserData && item.UserData?.IsFavorite ? 'true' : undefined,
info: {
biography: item.Overview,
externalUrl: (item.ExternalUrls || []).map((entry: any) => normalizeItem(entry)),
imageUrl: undefined,
similarArtist: (item.similarArtist || []).map((entry: any) => normalizeArtist(entry)),
},
type: Item.Artist,
uniqueId: nanoid(),
album: (item.album || []).map((entry: any) => normalizeAlbum(entry)),
};
};
const normalizePlaylist = (item: any) => { const normalizePlaylist = (item: any) => {
return { return {
id: item.Id, id: item.Id,
@ -248,4 +267,49 @@ export const getAlbums = async (options: {
return (data.Items || []).map((entry: any) => normalizeAlbum(entry)); return (data.Items || []).map((entry: any) => normalizeAlbum(entry));
}; };
export const getArtist = async (options: { id: string }) => {
const { data } = await jellyfinApi.get(`/users/${auth.username}/items/${options.id}`);
const { data: albumData } = await jellyfinApi.get(`/users/${auth.username}/items`, {
params: {
artistIds: options.id,
sortBy: 'SortName',
includeItemTypes: 'MusicAlbum',
recursive: true,
fields: 'AudioInfo, ParentId, Genres, DateCreated, ChildCount',
},
});
return normalizeArtist({
...data,
album: albumData.Items,
});
};
export const getArtists = async () => {
const { data } = await jellyfinApi.get(`/artists/albumartists`, {
params: {
sortBy: 'SortName',
sortOrder: 'Ascending',
recursive: true,
imageTypeLimit: 1,
},
});
return (data.Items || []).map((entry: any) => normalizeArtist(entry));
};
export const getArtistSongs = async (options: { id: string }) => {
const { data } = await jellyfinApi.get(`/users/${auth.username}/items`, {
params: {
artistIds: options.id,
sortBy: 'Album',
includeItemTypes: 'Audio',
recursive: true,
fields: 'Genres, DateCreated, MediaSources, UserData',
},
});
return (data.Items || []).map((entry: any) => normalizeSong(entry));
};
// http://192.168.14.11:8096/Users/0e5716f27d7f4b48aadb4a3bd55a38e9/Items/70384e0059a925138783c7275f717859 // http://192.168.14.11:8096/Users/0e5716f27d7f4b48aadb4a3bd55a38e9/Items/70384e0059a925138783c7275f717859

116
src/components/library/ArtistView.tsx

@ -46,7 +46,7 @@ import { StyledButton, StyledPopover, StyledTag } from '../shared/styled';
import { setStatus } from '../../redux/playerSlice'; import { setStatus } from '../../redux/playerSlice';
import { GradientBackground, PageHeaderSubtitleDataLine } from '../layout/styled'; import { GradientBackground, PageHeaderSubtitleDataLine } from '../layout/styled';
import { apiController } from '../../api/controller'; import { apiController } from '../../api/controller';
import { Server } from '../../types'; import { Artist, GenericItem, Server } from '../../types';
const fac = new FastAverageColor(); const fac = new FastAverageColor();
@ -70,18 +70,6 @@ const ArtistView = ({ ...rest }: any) => {
const { isLoading, isError, data, error }: any = useQuery(['artist', artistId], () => const { isLoading, isError, data, error }: any = useQuery(['artist', artistId], () =>
apiController({ serverType: config.serverType, endpoint: 'getArtist', args: { id: artistId } }) apiController({ serverType: config.serverType, endpoint: 'getArtist', args: { id: artistId } })
); );
const {
isLoading: isLoadingAI,
isError: isErrorAI,
data: artistInfo,
error: errorAI,
}: any = useQuery(['artistInfo', artistId], () =>
apiController({
serverType: config.serverType,
endpoint: 'getArtistInfo',
args: config.serverType === Server.Subsonic ? { id: artistId, count: 8 } : null,
})
);
const filteredData = useSearchQuery(misc.searchQuery, data?.album, [ const filteredData = useSearchQuery(misc.searchQuery, data?.album, [
'title', 'title',
@ -261,18 +249,17 @@ const ArtistView = ({ ...rest }: any) => {
if (!isLoading) { if (!isLoading) {
const img = isCached(`${misc.imageCachePath}artist_${data?.id}.jpg`) const img = isCached(`${misc.imageCachePath}artist_${data?.id}.jpg`)
? `${misc.imageCachePath}artist_${data?.id}.jpg` ? `${misc.imageCachePath}artist_${data?.id}.jpg`
: data?.image.includes('placeholder') : data?.image?.includes('placeholder')
? artistInfo?.largeImageUrl && ? data?.info?.imageUrl
!artistInfo?.largeImageUrl?.match('2a96cbd8b46e442fc41c2b86b821562f') ? data?.info?.imageUrl
? artistInfo?.largeImageUrl
: data?.image : data?.image
: data?.image; : data?.image;
const setAvgColor = (imgUrl: string) => { const setAvgColor = (imgUrl: string) => {
if ( if (
data?.image.match('placeholder') || data?.image?.match('placeholder') ||
(data?.image.match('placeholder') && (data?.image?.match('placeholder') &&
artistInfo?.largeImageUrl?.match('2a96cbd8b46e442fc41c2b86b821562f')) data?.info?.imageUrl?.match('2a96cbd8b46e442fc41c2b86b821562f'))
) { ) {
setImageAverageColor({ color: 'rgba(50, 50, 50, .4)', loaded: true }); setImageAverageColor({ color: 'rgba(50, 50, 50, .4)', loaded: true });
} else { } else {
@ -296,7 +283,7 @@ const ArtistView = ({ ...rest }: any) => {
}; };
setAvgColor(img); setAvgColor(img);
} }
}, [artistInfo?.largeImageUrl, data?.id, data?.image, isLoading, misc.imageCachePath]); }, [data?.id, data?.image, data?.info, isLoading, misc.imageCachePath]);
useEffect(() => { useEffect(() => {
const allAlbumDurations = _.sum(_.map(data?.album, 'duration')); const allAlbumDurations = _.sum(_.map(data?.album, 'duration'));
@ -306,16 +293,12 @@ const ArtistView = ({ ...rest }: any) => {
setArtistSongTotal(allSongCount); setArtistSongTotal(allSongCount);
}, [data?.album]); }, [data?.album]);
if (isLoading || isLoadingAI || imageAverageColor.loaded === false) { if (isLoading || imageAverageColor.loaded === false) {
return <PageLoader />; return <PageLoader />;
} }
if (isError || isErrorAI) { if (isError) {
return ( return <span>Error: {error?.message}</span>;
<span>
Error: {error?.message} {errorAI?.message}
</span>
);
} }
return ( return (
@ -329,14 +312,13 @@ const ArtistView = ({ ...rest }: any) => {
header={ header={
<GenericPageHeader <GenericPageHeader
image={ image={
isCached(`${misc.imageCachePath}artist_${data.id}.jpg`) isCached(`${misc.imageCachePath}artist_${data?.id}.jpg`)
? `${misc.imageCachePath}artist_${data.id}.jpg` ? `${misc.imageCachePath}artist_${data?.id}.jpg`
: data.image.includes('placeholder') : data?.image?.includes('placeholder')
? artistInfo?.largeImageUrl && ? data?.info?.imageUrl
!artistInfo?.largeImageUrl?.match('2a96cbd8b46e442fc41c2b86b821562f') ? data?.info?.imageUrl
? artistInfo.largeImageUrl : data?.image
: data.image : data?.image
: data.image
} }
cacheImages={{ cacheImages={{
enabled: settings.getSync('cacheImages'), enabled: settings.getSync('cacheImages'),
@ -353,7 +335,7 @@ const ArtistView = ({ ...rest }: any) => {
{artistDurationTotal} {artistDurationTotal}
</PageHeaderSubtitleDataLine> </PageHeaderSubtitleDataLine>
<CustomTooltip <CustomTooltip
text={artistInfo?.biography text={data?.info.biography
?.replace(/<[^>]*>/, '') ?.replace(/<[^>]*>/, '')
.replace('Read more on Last.fm</a>', '')} .replace('Read more on Last.fm</a>', '')}
placement="bottomStart" placement="bottomStart"
@ -368,11 +350,11 @@ const ArtistView = ({ ...rest }: any) => {
}} }}
> >
<span> <span>
{artistInfo?.biography {data?.info.biography
?.replace(/<[^>]*>/, '') ?.replace(/<[^>]*>/, '')
.replace('Read more on Last.fm</a>', '') .replace('Read more on Last.fm</a>', '')
?.trim() ?.trim()
? `${artistInfo?.biography ? `${data?.info.biography
?.replace(/<[^>]*>/, '') ?.replace(/<[^>]*>/, '')
.replace('Read more on Last.fm</a>', '')}` .replace('Read more on Last.fm</a>', '')}`
: 'No artist biography found'} : 'No artist biography found'}
@ -426,35 +408,39 @@ const ArtistView = ({ ...rest }: any) => {
<div> <div>
<h6>Related artists</h6> <h6>Related artists</h6>
<TagGroup> <TagGroup>
{artistInfo.similarArtist?.map((artist: any) => ( {data.info.similarArtist &&
<StyledTag data?.info.similarArtist.map((artist: Artist) => (
key={artist.id} <StyledTag
onClick={() => { key={artist.id}
if (!rest.isModal) { onClick={() => {
history.push(`/library/artist/${artist.id}`); if (!rest.isModal) {
} else { history.push(`/library/artist/${artist.id}`);
dispatch( } else {
addModalPage({ dispatch(
pageType: 'artist', addModalPage({
id: artist.id, pageType: 'artist',
}) id: artist.id,
); })
} );
}} }
> }}
{artist.title} >
</StyledTag> {artist.title}
))} </StyledTag>
))}
</TagGroup> </TagGroup>
</div> </div>
<br /> <br />
<StyledButton {data.info.externalUrl &&
appearance="primary" data.info.externalUrl.map((ext: GenericItem) => (
disabled={!artistInfo?.lastFmUrl} <StyledButton
onClick={() => shell.openExternal(artistInfo?.lastFmUrl)} key={ext.id}
> appearance="primary"
View on Last.FM onClick={() => shell.openExternal(ext.id)}
</StyledButton> >
{ext.title}
</StyledButton>
))}
</StyledPopover> </StyledPopover>
} }
> >

5
src/components/player/PlayerBar.tsx

@ -439,8 +439,9 @@ const PlayerBar = () => {
enterable enterable
placement="topStart" placement="topStart"
text={ text={
playQueue[currentEntryList][playQueue.currentIndex]?.artist[0]?.title || playQueue[currentEntryList][playQueue.currentIndex]?.artist
'Unknown artist' ? playQueue[currentEntryList][playQueue.currentIndex]?.artist[0]?.title
: 'Unknown artist'
} }
> >
<span <span

9
src/types.ts

@ -52,6 +52,11 @@ export type APIEndpoints =
| 'getMusicDirectorySongs' | 'getMusicDirectorySongs'
| 'getDownloadUrl'; | 'getDownloadUrl';
export interface GenericItem {
id: string;
title: string;
}
export interface Album { export interface Album {
id: string; id: string;
title: string; title: string;
@ -79,6 +84,7 @@ export interface Artist {
albumCount?: number; albumCount?: number;
image?: string; image?: string;
starred?: string; starred?: string;
info?: ArtistInfo;
type?: Item.Artist; type?: Item.Artist;
uniqueId?: string; uniqueId?: string;
album?: Album[]; album?: Album[];
@ -86,10 +92,11 @@ export interface Artist {
export interface ArtistInfo { export interface ArtistInfo {
biography?: string; biography?: string;
lastFmUrl?: string; externalUrl: GenericItem[];
imageUrl?: string; imageUrl?: string;
similarArtist?: Artist[]; similarArtist?: Artist[];
} }
export interface Folder { export interface Folder {
id: string; id: string;
title: string; title: string;

Loading…
Cancel
Save