mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-08 04:50:12 +02:00
Add image URL generation at runtime to allow for dynamic image sizes (#1439)
* add getImageUrl to domain endpoints * add new ItemImage component and hooks to generate image url * add configuration for image resolution based on types
This commit is contained in:
@@ -16,100 +16,6 @@ import { ServerListItem, ServerType } from '/@/shared/types/types';
|
||||
|
||||
const TICKS_PER_MS = 10000;
|
||||
|
||||
const getAlbumArtistCoverArtUrl = (args: {
|
||||
baseUrl: string;
|
||||
item: z.infer<typeof jfType._response.albumArtist>;
|
||||
size: number;
|
||||
}) => {
|
||||
const size = args.size ? args.size : 300;
|
||||
|
||||
if (!args.item.ImageTags?.Primary) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
`${args.baseUrl}/Items` +
|
||||
`/${args.item.Id}` +
|
||||
'/Images/Primary' +
|
||||
`?width=${size}` +
|
||||
'&quality=96'
|
||||
);
|
||||
};
|
||||
|
||||
const getAlbumCoverArtUrl = (args: {
|
||||
baseUrl: string;
|
||||
item: z.infer<typeof jfType._response.album>;
|
||||
size: number;
|
||||
}) => {
|
||||
const size = args.size ? args.size : 300;
|
||||
|
||||
if (!args.item.ImageTags?.Primary && !args.item?.AlbumPrimaryImageTag) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
`${args.baseUrl}/Items` +
|
||||
`/${args.item.Id}` +
|
||||
'/Images/Primary' +
|
||||
`?width=${size}` +
|
||||
'&quality=96'
|
||||
);
|
||||
};
|
||||
|
||||
const getSongCoverArtUrl = (args: {
|
||||
baseUrl: string;
|
||||
item: z.infer<typeof jfType._response.song>;
|
||||
size: number;
|
||||
}) => {
|
||||
const size = args.size ? args.size : 100;
|
||||
|
||||
if (args.item.ImageTags.Primary) {
|
||||
return (
|
||||
`${args.baseUrl}/Items` +
|
||||
`/${args.item.Id}` +
|
||||
'/Images/Primary' +
|
||||
`?width=${size}` +
|
||||
'&quality=96' +
|
||||
// Invalidate the cache if the image chances. This appears to be
|
||||
// how Jellyfin Web does it as well
|
||||
`&tag=${args.item.ImageTags.Primary}`
|
||||
);
|
||||
}
|
||||
|
||||
if (args.item?.AlbumPrimaryImageTag) {
|
||||
// Fall back to album art if no image embedded
|
||||
return (
|
||||
`${args.baseUrl}/Items` +
|
||||
`/${args.item?.AlbumId}` +
|
||||
'/Images/Primary' +
|
||||
`?width=${size}` +
|
||||
'&quality=96'
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const getPlaylistCoverArtUrl = (args: {
|
||||
baseUrl: string;
|
||||
item: z.infer<typeof jfType._response.playlist>;
|
||||
size: number;
|
||||
}) => {
|
||||
const size = args.size ? args.size : 300;
|
||||
|
||||
if (!args.item.ImageTags?.Primary) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
`${args.baseUrl}/Items` +
|
||||
`/${args.item.Id}` +
|
||||
'/Images/Primary' +
|
||||
`?width=${size}` +
|
||||
'&quality=96'
|
||||
);
|
||||
};
|
||||
|
||||
type AlbumOrSong = z.infer<typeof jfType._response.album> | z.infer<typeof jfType._response.song>;
|
||||
|
||||
const KEYS_TO_OMIT = new Set(['AlbumArtist', 'Artist']);
|
||||
@@ -128,6 +34,7 @@ const getPeople = (item: AlbumOrSong): null | Record<string, RelatedArtist[]> =>
|
||||
// for other roles, we just want to display this and not filter.
|
||||
// filtering (and links) would require a separate field, PersonIds
|
||||
id: '',
|
||||
imageId: null,
|
||||
imageUrl: null,
|
||||
name: person.Name,
|
||||
};
|
||||
@@ -158,10 +65,47 @@ const getTags = (item: AlbumOrSong): null | Record<string, string[]> => {
|
||||
return null;
|
||||
};
|
||||
|
||||
const getSongImageId = (item: z.infer<typeof jfType._response.song>): null | string => {
|
||||
if (item.ImageTags?.Primary) {
|
||||
return item.Id;
|
||||
}
|
||||
|
||||
if (item.AlbumPrimaryImageTag && item.AlbumId) {
|
||||
return item.AlbumId;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const getAlbumImageId = (item: z.infer<typeof jfType._response.album>): null | string => {
|
||||
if (item.ImageTags?.Primary) {
|
||||
return item.Id;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const getAlbumArtistImageId = (
|
||||
item: z.infer<typeof jfType._response.albumArtist>,
|
||||
): null | string => {
|
||||
if (item.ImageTags?.Primary) {
|
||||
return item.Id;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const getPlaylistImageId = (item: z.infer<typeof jfType._response.playlist>): null | string => {
|
||||
if (item.ImageTags?.Primary) {
|
||||
return item.Id;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const normalizeSong = (
|
||||
item: z.infer<typeof jfType._response.song>,
|
||||
server: null | ServerListItem,
|
||||
imageSize?: number,
|
||||
): Song => {
|
||||
let bitRate = 0;
|
||||
let channels: null | number = null;
|
||||
@@ -201,6 +145,7 @@ const normalizeSong = (
|
||||
album: item.Album,
|
||||
albumArtists: item.AlbumArtists?.map((entry) => ({
|
||||
id: entry.Id,
|
||||
imageId: entry.Id,
|
||||
imageUrl: null,
|
||||
name: entry.Name,
|
||||
})),
|
||||
@@ -209,6 +154,7 @@ const normalizeSong = (
|
||||
artists: (item?.ArtistItems?.length ? item.ArtistItems : item.AlbumArtists)?.map(
|
||||
(entry) => ({
|
||||
id: entry.Id,
|
||||
imageId: entry.Id,
|
||||
imageUrl: null,
|
||||
name: entry.Name,
|
||||
}),
|
||||
@@ -241,13 +187,14 @@ const normalizeSong = (
|
||||
_serverType: ServerType.JELLYFIN,
|
||||
albumCount: null,
|
||||
id: entry.Id,
|
||||
imageId: null,
|
||||
imageUrl: null,
|
||||
name: entry.Name,
|
||||
songCount: null,
|
||||
})),
|
||||
id: item.Id,
|
||||
imagePlaceholderUrl: null,
|
||||
imageUrl: getSongCoverArtUrl({ baseUrl: server?.url || '', item, size: imageSize || 100 }),
|
||||
imageId: getSongImageId(item),
|
||||
imageUrl: null,
|
||||
lastPlayedAt: null,
|
||||
lyrics: null,
|
||||
mbzRecordingId: null,
|
||||
@@ -273,7 +220,6 @@ const normalizeSong = (
|
||||
const normalizeAlbum = (
|
||||
item: z.infer<typeof jfType._response.album>,
|
||||
server: null | ServerListItem,
|
||||
imageSize?: number,
|
||||
): Album => {
|
||||
return {
|
||||
_itemType: LibraryItem.ALBUM,
|
||||
@@ -283,17 +229,18 @@ const normalizeAlbum = (
|
||||
albumArtists:
|
||||
item.AlbumArtists.map((entry) => ({
|
||||
id: entry.Id,
|
||||
imageId: entry.Id,
|
||||
imageUrl: null,
|
||||
name: entry.Name,
|
||||
})) || [],
|
||||
artists: (item.ArtistItems?.length ? item.ArtistItems : item.AlbumArtists)?.map(
|
||||
(entry) => ({
|
||||
id: entry.Id,
|
||||
imageId: entry.Id,
|
||||
imageUrl: null,
|
||||
name: entry.Name,
|
||||
}),
|
||||
),
|
||||
backdropImageUrl: null,
|
||||
comment: null,
|
||||
createdAt: item.DateCreated,
|
||||
duration: item.RunTimeTicks / TICKS_PER_MS,
|
||||
@@ -305,17 +252,14 @@ const normalizeAlbum = (
|
||||
_serverType: ServerType.JELLYFIN,
|
||||
albumCount: null,
|
||||
id: entry.Id,
|
||||
imageId: null,
|
||||
imageUrl: null,
|
||||
name: entry.Name,
|
||||
songCount: null,
|
||||
})) || [],
|
||||
id: item.Id,
|
||||
imagePlaceholderUrl: null,
|
||||
imageUrl: getAlbumCoverArtUrl({
|
||||
baseUrl: server?.url || '',
|
||||
item,
|
||||
size: imageSize || 300,
|
||||
}),
|
||||
imageId: getAlbumImageId(item),
|
||||
imageUrl: null,
|
||||
isCompilation: null,
|
||||
lastPlayedAt: null,
|
||||
mbzId: item.ProviderIds?.MusicBrainzAlbum || null,
|
||||
@@ -329,7 +273,7 @@ const normalizeAlbum = (
|
||||
releaseYear: item.ProductionYear || null,
|
||||
size: null,
|
||||
songCount: item?.ChildCount || null,
|
||||
songs: item.Songs?.map((song) => normalizeSong(song, server, imageSize)),
|
||||
songs: item.Songs?.map((song) => normalizeSong(song, server)),
|
||||
tags: getTags(item),
|
||||
updatedAt: item?.DateLastMediaAdded || item.DateCreated,
|
||||
userFavorite: item.UserData?.IsFavorite || false,
|
||||
@@ -343,17 +287,13 @@ const normalizeAlbumArtist = (
|
||||
similarArtists?: z.infer<typeof jfType._response.albumArtistList>;
|
||||
},
|
||||
server: null | ServerListItem,
|
||||
imageSize?: number,
|
||||
): AlbumArtist => {
|
||||
const similarArtists =
|
||||
item.similarArtists?.Items?.filter((entry) => entry.Name !== 'Various Artists').map(
|
||||
(entry) => ({
|
||||
id: entry.Id,
|
||||
imageUrl: getAlbumArtistCoverArtUrl({
|
||||
baseUrl: server?.url || '',
|
||||
item: entry,
|
||||
size: imageSize || 300,
|
||||
}),
|
||||
imageId: entry.Id,
|
||||
imageUrl: null,
|
||||
name: entry.Name,
|
||||
}),
|
||||
) || [];
|
||||
@@ -363,7 +303,6 @@ const normalizeAlbumArtist = (
|
||||
_serverId: server?.id || '',
|
||||
_serverType: ServerType.JELLYFIN,
|
||||
albumCount: item.AlbumCount ?? null,
|
||||
backgroundImageUrl: null,
|
||||
biography: item.Overview || null,
|
||||
duration: item.RunTimeTicks / TICKS_PER_MS,
|
||||
genres: item.GenreItems?.map((entry) => ({
|
||||
@@ -372,16 +311,14 @@ const normalizeAlbumArtist = (
|
||||
_serverType: ServerType.JELLYFIN,
|
||||
albumCount: null,
|
||||
id: entry.Id,
|
||||
imageId: null,
|
||||
imageUrl: null,
|
||||
name: entry.Name,
|
||||
songCount: null,
|
||||
})),
|
||||
id: item.Id,
|
||||
imageUrl: getAlbumArtistCoverArtUrl({
|
||||
baseUrl: server?.url || '',
|
||||
item,
|
||||
size: imageSize || 300,
|
||||
}),
|
||||
imageId: getAlbumArtistImageId(item),
|
||||
imageUrl: null,
|
||||
lastPlayedAt: null,
|
||||
mbz: item.ProviderIds?.MusicBrainzArtist || null,
|
||||
name: item.Name,
|
||||
@@ -396,16 +333,7 @@ const normalizeAlbumArtist = (
|
||||
const normalizePlaylist = (
|
||||
item: z.infer<typeof jfType._response.playlist>,
|
||||
server: null | ServerListItem,
|
||||
imageSize?: number,
|
||||
): Playlist => {
|
||||
const imageUrl = getPlaylistCoverArtUrl({
|
||||
baseUrl: server?.url || '',
|
||||
item,
|
||||
size: imageSize || 300,
|
||||
});
|
||||
|
||||
const imagePlaceholderUrl = null;
|
||||
|
||||
return {
|
||||
_itemType: LibraryItem.PLAYLIST,
|
||||
_serverId: server?.id || '',
|
||||
@@ -418,13 +346,14 @@ const normalizePlaylist = (
|
||||
_serverType: ServerType.JELLYFIN,
|
||||
albumCount: null,
|
||||
id: entry.Id,
|
||||
imageId: null,
|
||||
imageUrl: null,
|
||||
name: entry.Name,
|
||||
songCount: null,
|
||||
})),
|
||||
id: item.Id,
|
||||
imagePlaceholderUrl,
|
||||
imageUrl: imageUrl || null,
|
||||
imageId: getPlaylistImageId(item),
|
||||
imageUrl: null,
|
||||
name: item.Name,
|
||||
owner: null,
|
||||
ownerId: null,
|
||||
@@ -463,26 +392,6 @@ const normalizeMusicFolder = (item: z.infer<typeof jfType._response.musicFolder>
|
||||
// };
|
||||
// };
|
||||
|
||||
const getGenreCoverArtUrl = (args: {
|
||||
baseUrl: string;
|
||||
item: z.infer<typeof jfType._response.genre>;
|
||||
size: number;
|
||||
}) => {
|
||||
const size = args.size ? args.size : 300;
|
||||
|
||||
if (!args.item.ImageTags?.Primary) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
`${args.baseUrl}/Items` +
|
||||
`/${args.item.Id}` +
|
||||
'/Images/Primary' +
|
||||
`?width=${size}` +
|
||||
'&quality=96'
|
||||
);
|
||||
};
|
||||
|
||||
const normalizeGenre = (
|
||||
item: z.infer<typeof jfType._response.genre>,
|
||||
server: null | ServerListItem,
|
||||
@@ -493,7 +402,8 @@ const normalizeGenre = (
|
||||
_serverType: ServerType.JELLYFIN,
|
||||
albumCount: null,
|
||||
id: item.Id,
|
||||
imageUrl: getGenreCoverArtUrl({ baseUrl: server?.url || '', item, size: 200 }),
|
||||
imageId: item.Id,
|
||||
imageUrl: null,
|
||||
name: item.Name,
|
||||
songCount: null,
|
||||
};
|
||||
|
||||
@@ -24,32 +24,6 @@ const getImageUrl = (args: { url: null | string }) => {
|
||||
return url;
|
||||
};
|
||||
|
||||
const getCoverArtUrl = (args: {
|
||||
baseUrl: string | undefined;
|
||||
coverArtId: string;
|
||||
credential: string | undefined;
|
||||
size: number;
|
||||
updated: string;
|
||||
}) => {
|
||||
const size = args.size ? args.size : 250;
|
||||
|
||||
if (!args.coverArtId || args.coverArtId.match('2a96cbd8b46e442fc41c2b86b821562f')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
`${args.baseUrl}/rest/getCoverArt.view` +
|
||||
`?id=${args.coverArtId}` +
|
||||
`&${args.credential}` +
|
||||
'&v=1.13.0' +
|
||||
'&c=Feishin' +
|
||||
`&size=${size}` +
|
||||
// A dummy variable to invalidate the cached image if the item is updated
|
||||
// This is adapted from how Navidrome web does it
|
||||
`&_=${args.updated}`
|
||||
);
|
||||
};
|
||||
|
||||
interface WithDate {
|
||||
playDate?: string;
|
||||
}
|
||||
@@ -74,6 +48,7 @@ const getArtists = (
|
||||
if (role === 'albumartist' || role === 'artist') {
|
||||
const roleList = list.map((item) => ({
|
||||
id: item.id,
|
||||
imageId: null,
|
||||
imageUrl: null,
|
||||
name: item.name,
|
||||
}));
|
||||
@@ -89,6 +64,7 @@ const getArtists = (
|
||||
for (const artist of list) {
|
||||
const item: RelatedArtist = {
|
||||
id: artist.id,
|
||||
imageId: null,
|
||||
imageUrl: null,
|
||||
name: artist.name,
|
||||
};
|
||||
@@ -112,11 +88,13 @@ const getArtists = (
|
||||
}
|
||||
|
||||
if (albumArtists === undefined) {
|
||||
albumArtists = [{ id: item.albumArtistId, imageUrl: null, name: item.albumArtist }];
|
||||
albumArtists = [
|
||||
{ id: item.albumArtistId, imageId: null, imageUrl: null, name: item.albumArtist },
|
||||
];
|
||||
}
|
||||
|
||||
if (artists === undefined) {
|
||||
artists = [{ id: item.artistId, imageUrl: null, name: item.artist }];
|
||||
artists = [{ id: item.artistId, imageId: null, imageUrl: null, name: item.artist }];
|
||||
}
|
||||
|
||||
return { albumArtists, artists, participants };
|
||||
@@ -125,7 +103,6 @@ const getArtists = (
|
||||
const normalizeSong = (
|
||||
item: z.infer<typeof ndType._response.playlistSong> | z.infer<typeof ndType._response.song>,
|
||||
server?: null | ServerListItem,
|
||||
imageSize?: number,
|
||||
): Song => {
|
||||
let id;
|
||||
let playlistItemId;
|
||||
@@ -138,15 +115,6 @@ const normalizeSong = (
|
||||
id = item.id;
|
||||
}
|
||||
|
||||
const imageUrl = getCoverArtUrl({
|
||||
baseUrl: server?.url,
|
||||
coverArtId: id,
|
||||
credential: server?.credential,
|
||||
size: imageSize || 100,
|
||||
updated: item.updatedAt,
|
||||
});
|
||||
|
||||
const imagePlaceholderUrl = null;
|
||||
return {
|
||||
album: item.album,
|
||||
albumId: item.albumId,
|
||||
@@ -182,13 +150,14 @@ const normalizeSong = (
|
||||
_serverType: ServerType.NAVIDROME,
|
||||
albumCount: null,
|
||||
id: genre.id,
|
||||
imageId: null,
|
||||
imageUrl: null,
|
||||
name: genre.name,
|
||||
songCount: null,
|
||||
})),
|
||||
id,
|
||||
imagePlaceholderUrl,
|
||||
imageUrl,
|
||||
imageId: item.id,
|
||||
imageUrl: null,
|
||||
lastPlayedAt: normalizePlayDate(item),
|
||||
lyrics: item.lyrics ? item.lyrics : null,
|
||||
mbzRecordingId: item.mbzReleaseTrackId || null,
|
||||
@@ -261,20 +230,7 @@ const normalizeAlbum = (
|
||||
songs?: z.infer<typeof ndType._response.songList>;
|
||||
},
|
||||
server?: null | ServerListItem,
|
||||
imageSize?: number,
|
||||
): Album => {
|
||||
const imageUrl = getCoverArtUrl({
|
||||
baseUrl: server?.url,
|
||||
coverArtId: item.coverArtId || item.id,
|
||||
credential: server?.credential,
|
||||
size: imageSize || 300,
|
||||
updated: item.updatedAt,
|
||||
});
|
||||
|
||||
const imagePlaceholderUrl = null;
|
||||
|
||||
const imageBackdropUrl = imageUrl?.replace(/size=\d+/, 'size=1000') || null;
|
||||
|
||||
return {
|
||||
...parseAlbumTags(item),
|
||||
...getArtists(item),
|
||||
@@ -282,7 +238,6 @@ const normalizeAlbum = (
|
||||
_serverId: server?.id || 'unknown',
|
||||
_serverType: ServerType.NAVIDROME,
|
||||
albumArtist: item.albumArtist,
|
||||
backdropImageUrl: imageBackdropUrl,
|
||||
comment: item.comment || null,
|
||||
createdAt: item.createdAt,
|
||||
duration: item.duration !== undefined ? item.duration * 1000 : null,
|
||||
@@ -298,13 +253,14 @@ const normalizeAlbum = (
|
||||
_serverType: ServerType.NAVIDROME,
|
||||
albumCount: null,
|
||||
id: genre.id,
|
||||
imageId: null,
|
||||
imageUrl: null,
|
||||
name: genre.name,
|
||||
songCount: null,
|
||||
})),
|
||||
id: item.id,
|
||||
imagePlaceholderUrl,
|
||||
imageUrl,
|
||||
imageId: item.coverArtId || item.id,
|
||||
imageUrl: null,
|
||||
isCompilation: item.compilation,
|
||||
lastPlayedAt: normalizePlayDate(item),
|
||||
mbzId: item.mbzAlbumId || null,
|
||||
@@ -329,17 +285,7 @@ const normalizeAlbumArtist = (
|
||||
},
|
||||
server?: null | ServerListItem,
|
||||
): AlbumArtist => {
|
||||
let imageUrl = getImageUrl({ url: item?.largeImageUrl || null });
|
||||
|
||||
if (!imageUrl) {
|
||||
imageUrl = getCoverArtUrl({
|
||||
baseUrl: server?.url,
|
||||
coverArtId: `ar-${item.id}`,
|
||||
credential: server?.credential,
|
||||
size: 300,
|
||||
updated: item.updatedAt || '',
|
||||
});
|
||||
}
|
||||
const imageUrl = getImageUrl({ url: item?.largeImageUrl || null });
|
||||
|
||||
let albumCount: number;
|
||||
let songCount: number;
|
||||
@@ -363,7 +309,6 @@ const normalizeAlbumArtist = (
|
||||
_serverId: server?.id || 'unknown',
|
||||
_serverType: ServerType.NAVIDROME,
|
||||
albumCount,
|
||||
backgroundImageUrl: null,
|
||||
biography: item.biography || null,
|
||||
duration: null,
|
||||
genres: (item.genres || []).map((genre) => ({
|
||||
@@ -372,11 +317,13 @@ const normalizeAlbumArtist = (
|
||||
_serverType: ServerType.NAVIDROME,
|
||||
albumCount: null,
|
||||
id: genre.id,
|
||||
imageId: null,
|
||||
imageUrl: null,
|
||||
name: genre.name,
|
||||
songCount: null,
|
||||
})),
|
||||
id: item.id,
|
||||
imageId: item.id,
|
||||
imageUrl: imageUrl || null,
|
||||
lastPlayedAt: normalizePlayDate(item),
|
||||
mbz: item.mbzArtistId || null,
|
||||
@@ -385,6 +332,7 @@ const normalizeAlbumArtist = (
|
||||
similarArtists:
|
||||
item.similarArtists?.map((artist) => ({
|
||||
id: artist.id,
|
||||
imageId: null,
|
||||
imageUrl: artist?.artistImageUrl || null,
|
||||
name: artist.name,
|
||||
})) || null,
|
||||
@@ -397,18 +345,7 @@ const normalizeAlbumArtist = (
|
||||
const normalizePlaylist = (
|
||||
item: z.infer<typeof ndType._response.playlist>,
|
||||
server?: null | ServerListItem,
|
||||
imageSize?: number,
|
||||
): Playlist => {
|
||||
const imageUrl = getCoverArtUrl({
|
||||
baseUrl: server?.url,
|
||||
coverArtId: item.id,
|
||||
credential: server?.credential,
|
||||
size: imageSize || 300,
|
||||
updated: item.updatedAt,
|
||||
});
|
||||
|
||||
const imagePlaceholderUrl = null;
|
||||
|
||||
return {
|
||||
_itemType: LibraryItem.PLAYLIST,
|
||||
_serverId: server?.id || 'unknown',
|
||||
@@ -417,8 +354,8 @@ const normalizePlaylist = (
|
||||
duration: item.duration * 1000,
|
||||
genres: [],
|
||||
id: item.id,
|
||||
imagePlaceholderUrl,
|
||||
imageUrl,
|
||||
imageId: item.id,
|
||||
imageUrl: null,
|
||||
name: item.name,
|
||||
owner: item.ownerName,
|
||||
ownerId: item.ownerId,
|
||||
@@ -440,6 +377,7 @@ const normalizeGenre = (
|
||||
_serverType: ServerType.NAVIDROME,
|
||||
albumCount: item.albumCount ?? null,
|
||||
id: item.id,
|
||||
imageId: null,
|
||||
imageUrl: null,
|
||||
name: item.name,
|
||||
songCount: item.songCount ?? null,
|
||||
|
||||
Reference in New Issue
Block a user