feishin/src/shared/api/jellyfin/jellyfin-normalize.ts

486 lines
14 KiB
TypeScript
Raw Normal View History

import { nanoid } from 'nanoid';
import { z } from 'zod';
import { JFAlbum, JFGenre, JFMusicFolder, JFPlaylist } from '/@/shared/api/jellyfin.types';
import { jfType } from '/@/shared/api/jellyfin/jellyfin-types';
import {
2023-07-01 19:10:05 -07:00
Album,
AlbumArtist,
Genre,
LibraryItem,
MusicFolder,
Playlist,
RelatedArtist,
Song,
} from '/@/shared/types/domain-types';
import { ServerListItem, ServerType } from '/@/shared/types/types';
const getStreamUrl = (args: {
2023-07-01 19:10:05 -07:00
container?: string;
deviceId: string;
eTag?: string;
id: string;
mediaSourceId?: string;
server: null | ServerListItem;
}) => {
const { deviceId, id, server } = args;
2023-07-01 19:10:05 -07:00
return (
`${server?.url}/audio` +
`/${id}/universal` +
`?userId=${server?.userId}` +
`&deviceId=${deviceId}` +
'&audioCodec=aac' +
`&apiKey=${server?.credential}` +
2023-07-01 19:10:05 -07:00
`&playSessionId=${deviceId}` +
'&container=opus,mp3,aac,m4a,m4b,flac,wav,ogg' +
'&transcodingContainer=ts' +
2024-09-01 08:26:30 -07:00
'&transcodingProtocol=http'
2023-07-01 19:10:05 -07:00
);
};
const getAlbumArtistCoverArtUrl = (args: {
2023-07-01 19:10:05 -07:00
baseUrl: string;
item: z.infer<typeof jfType._response.albumArtist>;
size: number;
}) => {
2023-07-01 19:10:05 -07:00
const size = args.size ? args.size : 300;
2023-07-01 19:10:05 -07:00
if (!args.item.ImageTags?.Primary) {
return null;
}
2023-07-01 19:10:05 -07:00
return (
`${args.baseUrl}/Items` +
`/${args.item.Id}` +
'/Images/Primary' +
`?width=${size}` +
2023-07-01 19:10:05 -07:00
'&quality=96'
);
};
const getAlbumCoverArtUrl = (args: { baseUrl: string; item: JFAlbum; size: number }) => {
2023-07-01 19:10:05 -07:00
const size = args.size ? args.size : 300;
2023-07-01 19:10:05 -07:00
if (!args.item.ImageTags?.Primary && !args.item?.AlbumPrimaryImageTag) {
return null;
}
2023-07-01 19:10:05 -07:00
return (
`${args.baseUrl}/Items` +
`/${args.item.Id}` +
'/Images/Primary' +
`?width=${size}` +
2023-07-01 19:10:05 -07:00
'&quality=96'
);
};
const getSongCoverArtUrl = (args: {
2023-07-01 19:10:05 -07:00
baseUrl: string;
item: z.infer<typeof jfType._response.song>;
size: number;
}) => {
2023-07-01 19:10:05 -07:00
const size = args.size ? args.size : 100;
2023-07-01 19:10:05 -07:00
if (args.item.ImageTags.Primary) {
return (
`${args.baseUrl}/Items` +
`/${args.item.Id}` +
'/Images/Primary' +
`?width=${size}` +
2023-07-01 19:10:05 -07:00
'&quality=96'
);
}
2023-07-01 19:10:05 -07:00
if (args.item?.AlbumPrimaryImageTag) {
// Fall back to album art if no image embedded
return (
`${args.baseUrl}/Items` +
`/${args.item?.AlbumId}` +
'/Images/Primary' +
`?width=${size}` +
2023-07-01 19:10:05 -07:00
'&quality=96'
);
}
2023-07-01 19:10:05 -07:00
return null;
};
const getPlaylistCoverArtUrl = (args: { baseUrl: string; item: JFPlaylist; size: number }) => {
2023-07-01 19:10:05 -07:00
const size = args.size ? args.size : 300;
2023-07-01 19:10:05 -07:00
if (!args.item.ImageTags?.Primary) {
return null;
}
2023-07-01 19:10:05 -07:00
return (
`${args.baseUrl}/Items` +
`/${args.item.Id}` +
'/Images/Primary' +
`?width=${size}` +
2023-07-01 19:10:05 -07:00
'&quality=96'
);
};
type AlbumOrSong = z.infer<typeof jfType._response.album> | z.infer<typeof jfType._response.song>;
const getPeople = (item: AlbumOrSong): null | Record<string, RelatedArtist[]> => {
if (item.People) {
const participants: Record<string, RelatedArtist[]> = {};
for (const person of item.People) {
const key = person.Type || '';
const item: RelatedArtist = {
// for other roles, we just want to display this and not filter.
// filtering (and links) would require a separate field, PersonIds
id: '',
imageUrl: null,
name: person.Name,
};
if (key in participants) {
participants[key].push(item);
} else {
participants[key] = [item];
}
}
return participants;
}
return null;
};
const getTags = (item: AlbumOrSong): null | Record<string, string[]> => {
if (item.Tags) {
const tags: Record<string, string[]> = {};
for (const tag of item.Tags) {
tags[tag] = [];
}
return tags;
}
return null;
};
const normalizeSong = (
2023-07-01 19:10:05 -07:00
item: z.infer<typeof jfType._response.song>,
server: null | ServerListItem,
2023-07-01 19:10:05 -07:00
deviceId: string,
imageSize?: number,
): Song => {
2023-07-01 19:10:05 -07:00
return {
album: item.Album,
albumArtists: item.AlbumArtists?.map((entry) => ({
id: entry.Id,
imageUrl: null,
name: entry.Name,
})),
albumId: item.AlbumId || `dummy/${item.Id}`,
2023-07-01 19:10:05 -07:00
artistName: item?.ArtistItems?.[0]?.Name,
artists: item?.ArtistItems?.map((entry) => ({
id: entry.Id,
imageUrl: null,
name: entry.Name,
})),
2023-08-04 02:27:04 -07:00
bitRate:
item.MediaSources?.[0].Bitrate &&
Number(Math.trunc(item.MediaSources[0].Bitrate / 1000)),
2023-07-01 19:10:05 -07:00
bpm: null,
channels: null,
comment: null,
compilation: null,
container: (item.MediaSources && item.MediaSources[0]?.Container) || null,
createdAt: item.DateCreated,
discNumber: (item.ParentIndexNumber && item.ParentIndexNumber) || 1,
2023-09-22 15:12:23 -07:00
discSubtitle: null,
2023-09-15 16:52:14 -07:00
duration: item.RunTimeTicks / 10000,
gain:
item.NormalizationGain !== undefined
? {
track: item.NormalizationGain,
}
: item.LUFS
? {
track: -18 - item.LUFS,
}
: null,
2023-08-04 13:07:39 -07:00
genres: item.GenreItems?.map((entry) => ({
2023-08-04 02:27:04 -07:00
id: entry.Id,
imageUrl: null,
itemType: LibraryItem.GENRE,
name: entry.Name,
})),
2023-07-01 19:10:05 -07:00
id: item.Id,
imagePlaceholderUrl: null,
imageUrl: getSongCoverArtUrl({ baseUrl: server?.url || '', item, size: imageSize || 100 }),
itemType: LibraryItem.SONG,
lastPlayedAt: null,
lyrics: null,
name: item.Name,
participants: getPeople(item),
2023-07-01 19:10:05 -07:00
path: (item.MediaSources && item.MediaSources[0]?.Path) || null,
peak: null,
2023-07-01 19:10:05 -07:00
playCount: (item.UserData && item.UserData.PlayCount) || 0,
playlistItemId: item.PlaylistItemId,
2024-08-25 19:52:44 -07:00
releaseDate: item.PremiereDate
? new Date(item.PremiereDate).toISOString()
: item.ProductionYear
? new Date(item.ProductionYear, 0, 1).toISOString()
: null,
2023-07-01 19:10:05 -07:00
releaseYear: item.ProductionYear ? String(item.ProductionYear) : null,
serverId: server?.id || '',
serverType: ServerType.JELLYFIN,
size: item.MediaSources && item.MediaSources[0]?.Size,
streamUrl: getStreamUrl({
container: item.MediaSources?.[0]?.Container,
deviceId,
eTag: item.MediaSources?.[0]?.ETag,
id: item.Id,
mediaSourceId: item.MediaSources?.[0]?.Id,
server,
}),
tags: getTags(item),
2023-07-01 19:10:05 -07:00
trackNumber: item.IndexNumber,
uniqueId: nanoid(),
updatedAt: item.DateCreated,
userFavorite: (item.UserData && item.UserData.IsFavorite) || false,
userRating: null,
};
};
const normalizeAlbum = (
2023-07-01 19:10:05 -07:00
item: z.infer<typeof jfType._response.album>,
server: null | ServerListItem,
2023-07-01 19:10:05 -07:00
imageSize?: number,
): Album => {
2023-07-01 19:10:05 -07:00
return {
2024-01-15 22:10:50 -08:00
albumArtist: item.AlbumArtist,
2023-07-01 19:10:05 -07:00
albumArtists:
item.AlbumArtists.map((entry) => ({
id: entry.Id,
imageUrl: null,
name: entry.Name,
})) || [],
artists: item.ArtistItems?.map((entry) => ({
id: entry.Id,
imageUrl: null,
name: entry.Name,
})),
backdropImageUrl: null,
2024-01-15 20:46:06 -08:00
comment: null,
2023-07-01 19:10:05 -07:00
createdAt: item.DateCreated,
duration: item.RunTimeTicks / 10000,
2023-08-04 13:07:39 -07:00
genres: item.GenreItems?.map((entry) => ({
2023-08-04 02:27:04 -07:00
id: entry.Id,
imageUrl: null,
itemType: LibraryItem.GENRE,
name: entry.Name,
})),
2023-07-01 19:10:05 -07:00
id: item.Id,
imagePlaceholderUrl: null,
imageUrl: getAlbumCoverArtUrl({
baseUrl: server?.url || '',
item,
size: imageSize || 300,
}),
isCompilation: null,
itemType: LibraryItem.ALBUM,
lastPlayedAt: null,
2024-01-15 22:10:50 -08:00
mbzId: item.ProviderIds?.MusicBrainzAlbum || null,
2023-07-01 19:10:05 -07:00
name: item.Name,
2024-08-25 19:52:44 -07:00
originalDate: null,
participants: getPeople(item),
2023-07-01 19:10:05 -07:00
playCount: item.UserData?.PlayCount || 0,
releaseDate: item.PremiereDate?.split('T')[0] || null,
releaseYear: item.ProductionYear || null,
serverId: server?.id || '',
serverType: ServerType.JELLYFIN,
size: null,
songCount: item?.ChildCount || null,
songs: item.Songs?.map((song) => normalizeSong(song, server, '', imageSize)),
tags: getTags(item),
2023-07-01 19:10:05 -07:00
uniqueId: nanoid(),
updatedAt: item?.DateLastMediaAdded || item.DateCreated,
userFavorite: item.UserData?.IsFavorite || false,
userRating: null,
};
};
const normalizeAlbumArtist = (
2023-07-01 19:10:05 -07:00
item: z.infer<typeof jfType._response.albumArtist> & {
similarArtists?: z.infer<typeof jfType._response.albumArtistList>;
},
server: null | ServerListItem,
2023-07-01 19:10:05 -07:00
imageSize?: number,
): AlbumArtist => {
2023-07-01 19:10:05 -07:00
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,
}),
name: entry.Name,
}),
) || [];
return {
albumCount: item.AlbumCount ?? null,
2023-07-01 19:10:05 -07:00
backgroundImageUrl: null,
biography: item.Overview || null,
duration: item.RunTimeTicks / 10000,
2023-08-04 13:07:39 -07:00
genres: item.GenreItems?.map((entry) => ({
2023-08-04 02:27:04 -07:00
id: entry.Id,
imageUrl: null,
itemType: LibraryItem.GENRE,
name: entry.Name,
})),
2023-07-01 19:10:05 -07:00
id: item.Id,
imageUrl: getAlbumArtistCoverArtUrl({
2023-07-01 19:10:05 -07:00
baseUrl: server?.url || '',
item,
size: imageSize || 300,
}),
2023-07-01 19:10:05 -07:00
itemType: LibraryItem.ALBUM_ARTIST,
lastPlayedAt: null,
2024-01-15 22:10:50 -08:00
mbz: item.ProviderIds?.MusicBrainzArtist || null,
2023-07-01 19:10:05 -07:00
name: item.Name,
playCount: item.UserData?.PlayCount || 0,
serverId: server?.id || '',
serverType: ServerType.JELLYFIN,
similarArtists,
songCount: item.SongCount ?? null,
2023-07-01 19:10:05 -07:00
userFavorite: item.UserData?.IsFavorite || false,
userRating: null,
};
};
const normalizePlaylist = (
2023-07-01 19:10:05 -07:00
item: z.infer<typeof jfType._response.playlist>,
server: null | ServerListItem,
2023-07-01 19:10:05 -07:00
imageSize?: number,
): Playlist => {
2023-07-01 19:10:05 -07:00
const imageUrl = getPlaylistCoverArtUrl({
baseUrl: server?.url || '',
item,
size: imageSize || 300,
});
2023-07-01 19:10:05 -07:00
const imagePlaceholderUrl = null;
2023-07-01 19:10:05 -07:00
return {
description: item.Overview || null,
duration: item.RunTimeTicks / 10000,
2023-08-04 13:07:39 -07:00
genres: item.GenreItems?.map((entry) => ({
2023-08-04 02:27:04 -07:00
id: entry.Id,
imageUrl: null,
itemType: LibraryItem.GENRE,
name: entry.Name,
})),
2023-07-01 19:10:05 -07:00
id: item.Id,
imagePlaceholderUrl,
imageUrl: imageUrl || null,
itemType: LibraryItem.PLAYLIST,
name: item.Name,
owner: null,
ownerId: null,
public: null,
rules: null,
serverId: server?.id || '',
serverType: ServerType.JELLYFIN,
size: null,
songCount: item?.ChildCount || null,
sync: null,
};
};
const normalizeMusicFolder = (item: JFMusicFolder): MusicFolder => {
2023-07-01 19:10:05 -07:00
return {
id: item.Id,
name: item.Name,
};
};
// const normalizeArtist = (item: any) => {
// return {
// album: (item.album || []).map((entry: any) => normalizeAlbum(entry)),
// albumCount: item.AlbumCount,
// duration: item.RunTimeTicks / 10000000,
// genre: item.GenreItems && item.GenreItems.map((entry: any) => normalizeItem(entry)),
// id: item.Id,
// image: getCoverArtUrl(item),
// info: {
// biography: item.Overview,
// externalUrl: (item.ExternalUrls || []).map((entry: any) => normalizeItem(entry)),
// imageUrl: undefined,
// similarArtist: (item.similarArtist || []).map((entry: any) => normalizeArtist(entry)),
// },
// starred: item.UserData && item.UserData?.IsFavorite ? 'true' : undefined,
// title: item.Name,
// uniqueId: nanoid(),
// };
// };
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: JFGenre, server: null | ServerListItem): Genre => {
2023-07-01 19:10:05 -07:00
return {
albumCount: undefined,
id: item.Id,
imageUrl: getGenreCoverArtUrl({ baseUrl: server?.url || '', item, size: 200 }),
2023-08-04 02:27:04 -07:00
itemType: LibraryItem.GENRE,
2023-07-01 19:10:05 -07:00
name: item.Name,
songCount: undefined,
};
};
// const normalizeFolder = (item: any) => {
// return {
// created: item.DateCreated,
// id: item.Id,
// image: getCoverArtUrl(item, 150),
// isDir: true,
// title: item.Name,
// type: Item.Folder,
// uniqueId: nanoid(),
// };
// };
// const normalizeScanStatus = () => {
// return {
// count: 'N/a',
// scanning: false,
// };
// };
export const jfNormalize = {
2023-07-01 19:10:05 -07:00
album: normalizeAlbum,
albumArtist: normalizeAlbumArtist,
genre: normalizeGenre,
musicFolder: normalizeMusicFolder,
playlist: normalizePlaylist,
song: normalizeSong,
};