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,
albumArtist: item.artist,
albumArtistId: item.artistId,
artist: [{ id: item.artistId, title: item.artist }],
artist: item.artist && [{ id: item.artistId, title: item.artist }],
track: item.track,
year: item.year,
genre: [{ id: item.genre, title: item.genre }],
genre: item.genre && [{ id: item.genre, title: item.genre }],
albumGenre: item.genre,
size: item.size,
contentType: item.contentType,
@ -228,21 +228,19 @@ const normalizeArtist = (item: any) => {
albumCount: item.albumCount,
image: getCoverArtUrl(item, legacyAuth, 350),
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,
uniqueId: nanoid(),
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) => {
return {
id: item.id,
@ -390,7 +388,17 @@ export const getRandomSongs = async (options: {
export const getArtist = async (options: { id: string }) => {
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 }) => {
@ -399,21 +407,21 @@ export const getArtists = async (options: { musicFolderId?: string | number }) =
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 }) => {
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) {
promises.push(getAlbum({ id: artist.album[i].id }));
for (let i = 0; i < data.artist.album.length; i += 1) {
promises.push(api.get(`/getAlbum`, { params: { id: data.artist.album[i].id } }));
}
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 () => {

11
src/api/controller.ts

@ -8,7 +8,6 @@ import {
getAlbum,
getAlbums,
getArtist,
getArtistInfo,
getArtists,
getArtistSongs,
getDownloadUrl,
@ -34,6 +33,9 @@ import {
updatePlaylistSongsLg,
} from './api';
import {
getArtist as jfGetArtist,
getArtists as jfGetArtists,
getArtistSongs as jfGetArtistSongs,
getAlbum as jfGetAlbum,
getAlbums as jfGetAlbums,
getPlaylist as jfGetPlaylist,
@ -49,10 +51,9 @@ const endpoints = [
{ id: 'getAlbum', endpoint: { subsonic: getAlbum, jellyfin: jfGetAlbum } },
{ id: 'getAlbums', endpoint: { subsonic: getAlbums, jellyfin: jfGetAlbums } },
{ id: 'getRandomSongs', endpoint: { subsonic: getRandomSongs, jellyfin: undefined } },
{ id: 'getArtist', endpoint: { subsonic: getArtist, jellyfin: undefined } },
{ id: 'getArtists', endpoint: { subsonic: getArtists, jellyfin: undefined } },
{ id: 'getArtistInfo', endpoint: { subsonic: getArtistInfo, jellyfin: undefined } },
{ id: 'getArtistSongs', endpoint: { subsonic: getArtistSongs, jellyfin: undefined } },
{ id: 'getArtist', endpoint: { subsonic: getArtist, jellyfin: jfGetArtist } },
{ id: 'getArtists', endpoint: { subsonic: getArtists, jellyfin: jfGetArtists } },
{ id: 'getArtistSongs', endpoint: { subsonic: getArtistSongs, jellyfin: jfGetArtistSongs } },
{ id: 'startScan', endpoint: { subsonic: startScan, jellyfin: undefined } },
{ id: 'getScanStatus', endpoint: { subsonic: getScanStatus, 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) => {
return {
id: item.Id,
id: item.Id || item.Url,
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) => {
return {
id: item.Id,
@ -248,4 +267,49 @@ export const getAlbums = async (options: {
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

116
src/components/library/ArtistView.tsx

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

5
src/components/player/PlayerBar.tsx

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

9
src/types.ts

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

Loading…
Cancel
Save