reorganize global types to shared directory

This commit is contained in:
jeffvli 2025-05-20 18:08:51 -07:00
parent 26c02e03c5
commit 9db2e51d2d
17 changed files with 160 additions and 144 deletions

View file

@ -0,0 +1,485 @@
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 {
Album,
AlbumArtist,
Genre,
LibraryItem,
MusicFolder,
Playlist,
RelatedArtist,
Song,
} from '/@/shared/types/domain-types';
import { ServerListItem, ServerType } from '/@/shared/types/types';
const getStreamUrl = (args: {
container?: string;
deviceId: string;
eTag?: string;
id: string;
mediaSourceId?: string;
server: null | ServerListItem;
}) => {
const { deviceId, id, server } = args;
return (
`${server?.url}/audio` +
`/${id}/universal` +
`?userId=${server?.userId}` +
`&deviceId=${deviceId}` +
'&audioCodec=aac' +
`&apiKey=${server?.credential}` +
`&playSessionId=${deviceId}` +
'&container=opus,mp3,aac,m4a,m4b,flac,wav,ogg' +
'&transcodingContainer=ts' +
'&transcodingProtocol=http'
);
};
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: JFAlbum; 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'
);
}
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: JFPlaylist; 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 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 = (
item: z.infer<typeof jfType._response.song>,
server: null | ServerListItem,
deviceId: string,
imageSize?: number,
): Song => {
return {
album: item.Album,
albumArtists: item.AlbumArtists?.map((entry) => ({
id: entry.Id,
imageUrl: null,
name: entry.Name,
})),
albumId: item.AlbumId || `dummy/${item.Id}`,
artistName: item?.ArtistItems?.[0]?.Name,
artists: item?.ArtistItems?.map((entry) => ({
id: entry.Id,
imageUrl: null,
name: entry.Name,
})),
bitRate:
item.MediaSources?.[0].Bitrate &&
Number(Math.trunc(item.MediaSources[0].Bitrate / 1000)),
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,
discSubtitle: null,
duration: item.RunTimeTicks / 10000,
gain:
item.NormalizationGain !== undefined
? {
track: item.NormalizationGain,
}
: item.LUFS
? {
track: -18 - item.LUFS,
}
: null,
genres: item.GenreItems?.map((entry) => ({
id: entry.Id,
imageUrl: null,
itemType: LibraryItem.GENRE,
name: entry.Name,
})),
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),
path: (item.MediaSources && item.MediaSources[0]?.Path) || null,
peak: null,
playCount: (item.UserData && item.UserData.PlayCount) || 0,
playlistItemId: item.PlaylistItemId,
releaseDate: item.PremiereDate
? new Date(item.PremiereDate).toISOString()
: item.ProductionYear
? new Date(item.ProductionYear, 0, 1).toISOString()
: null,
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),
trackNumber: item.IndexNumber,
uniqueId: nanoid(),
updatedAt: item.DateCreated,
userFavorite: (item.UserData && item.UserData.IsFavorite) || false,
userRating: null,
};
};
const normalizeAlbum = (
item: z.infer<typeof jfType._response.album>,
server: null | ServerListItem,
imageSize?: number,
): Album => {
return {
albumArtist: item.AlbumArtist,
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,
comment: null,
createdAt: item.DateCreated,
duration: item.RunTimeTicks / 10000,
genres: item.GenreItems?.map((entry) => ({
id: entry.Id,
imageUrl: null,
itemType: LibraryItem.GENRE,
name: entry.Name,
})),
id: item.Id,
imagePlaceholderUrl: null,
imageUrl: getAlbumCoverArtUrl({
baseUrl: server?.url || '',
item,
size: imageSize || 300,
}),
isCompilation: null,
itemType: LibraryItem.ALBUM,
lastPlayedAt: null,
mbzId: item.ProviderIds?.MusicBrainzAlbum || null,
name: item.Name,
originalDate: null,
participants: getPeople(item),
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),
uniqueId: nanoid(),
updatedAt: item?.DateLastMediaAdded || item.DateCreated,
userFavorite: item.UserData?.IsFavorite || false,
userRating: null,
};
};
const normalizeAlbumArtist = (
item: z.infer<typeof jfType._response.albumArtist> & {
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,
}),
name: entry.Name,
}),
) || [];
return {
albumCount: item.AlbumCount ?? null,
backgroundImageUrl: null,
biography: item.Overview || null,
duration: item.RunTimeTicks / 10000,
genres: item.GenreItems?.map((entry) => ({
id: entry.Id,
imageUrl: null,
itemType: LibraryItem.GENRE,
name: entry.Name,
})),
id: item.Id,
imageUrl: getAlbumArtistCoverArtUrl({
baseUrl: server?.url || '',
item,
size: imageSize || 300,
}),
itemType: LibraryItem.ALBUM_ARTIST,
lastPlayedAt: null,
mbz: item.ProviderIds?.MusicBrainzArtist || null,
name: item.Name,
playCount: item.UserData?.PlayCount || 0,
serverId: server?.id || '',
serverType: ServerType.JELLYFIN,
similarArtists,
songCount: item.SongCount ?? null,
userFavorite: item.UserData?.IsFavorite || false,
userRating: null,
};
};
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 {
description: item.Overview || null,
duration: item.RunTimeTicks / 10000,
genres: item.GenreItems?.map((entry) => ({
id: entry.Id,
imageUrl: null,
itemType: LibraryItem.GENRE,
name: entry.Name,
})),
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 => {
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 => {
return {
albumCount: undefined,
id: item.Id,
imageUrl: getGenreCoverArtUrl({ baseUrl: server?.url || '', item, size: 200 }),
itemType: LibraryItem.GENRE,
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 = {
album: normalizeAlbum,
albumArtist: normalizeAlbumArtist,
genre: normalizeGenre,
musicFolder: normalizeMusicFolder,
playlist: normalizePlaylist,
song: normalizeSong,
};

View file

@ -0,0 +1,778 @@
import { z } from 'zod';
const sortOrderValues = ['Ascending', 'Descending'] as const;
const jfExternal = {
IMDB: 'Imdb',
MUSIC_BRAINZ: 'MusicBrainz',
THE_AUDIO_DB: 'TheAudioDb',
THE_MOVIE_DB: 'TheMovieDb',
TVDB: 'Tvdb',
};
const jfImage = {
BACKDROP: 'Backdrop',
BANNER: 'Banner',
BOX: 'Box',
CHAPTER: 'Chapter',
DISC: 'Disc',
LOGO: 'Logo',
PRIMARY: 'Primary',
THUMB: 'Thumb',
} as const;
const jfCollection = {
MUSIC: 'music',
PLAYLISTS: 'playlists',
} as const;
const error = z.object({
errors: z.object({
recursive: z.array(z.string()),
}),
status: z.number(),
title: z.string(),
traceId: z.string(),
type: z.string(),
});
const baseParameters = z.object({
AlbumArtistIds: z.string().optional(),
ArtistIds: z.string().optional(),
ContributingArtistIds: z.string().optional(),
EnableImageTypes: z.string().optional(),
EnableTotalRecordCount: z.boolean().optional(),
EnableUserData: z.boolean().optional(),
EnableUserDataTypes: z.boolean().optional(),
ExcludeArtistIds: z.string().optional(),
ExcludeItemIds: z.string().optional(),
ExcludeItemTypes: z.string().optional(),
Fields: z.string().optional(),
ImageTypeLimit: z.number().optional(),
IncludeArtists: z.boolean().optional(),
IncludeGenres: z.boolean().optional(),
IncludeItemTypes: z.string().optional(),
IncludeMedia: z.boolean().optional(),
IncludePeople: z.boolean().optional(),
IncludeStudios: z.boolean().optional(),
IsFavorite: z.boolean().optional(),
Limit: z.number().optional(),
MediaTypes: z.string().optional(),
NameStartsWith: z.string().optional(),
ParentId: z.string().optional(),
Recursive: z.boolean().optional(),
SearchTerm: z.string().optional(),
SortBy: z.string().optional(),
SortOrder: z.enum(sortOrderValues).optional(),
StartIndex: z.number().optional(),
Tags: z.string().optional(),
UserId: z.string().optional(),
Years: z.string().optional(),
});
const paginationParameters = z.object({
Limit: z.number().optional(),
SortOrder: z.enum(sortOrderValues).optional(),
StartIndex: z.number().optional(),
});
const pagination = z.object({
StartIndex: z.number(),
TotalRecordCount: z.number(),
});
const imageTags = z.object({
Logo: z.string().optional(),
Primary: z.string().optional(),
});
const imageBlurHashes = z.object({
Backdrop: z.record(z.string(), z.string()).optional(),
Logo: z.record(z.string(), z.string()).optional(),
Primary: z.record(z.string(), z.string()).optional(),
});
const userData = z.object({
IsFavorite: z.boolean(),
Key: z.string(),
PlaybackPositionTicks: z.number(),
PlayCount: z.number(),
Played: z.boolean(),
});
const externalUrl = z.object({
Name: z.string(),
Url: z.string(),
});
const mediaStream = z.object({
AspectRatio: z.string().optional(),
BitDepth: z.number().optional(),
BitRate: z.number().optional(),
ChannelLayout: z.string().optional(),
Channels: z.number().optional(),
Codec: z.string(),
CodecTimeBase: z.string(),
ColorSpace: z.string().optional(),
Comment: z.string().optional(),
DisplayTitle: z.string().optional(),
Height: z.number().optional(),
Index: z.number(),
IsDefault: z.boolean(),
IsExternal: z.boolean(),
IsForced: z.boolean(),
IsInterlaced: z.boolean(),
IsTextSubtitleStream: z.boolean(),
Level: z.number(),
PixelFormat: z.string().optional(),
Profile: z.string().optional(),
RealFrameRate: z.number().optional(),
RefFrames: z.number().optional(),
SampleRate: z.number().optional(),
SupportsExternalStream: z.boolean(),
TimeBase: z.string(),
Type: z.string(),
Width: z.number().optional(),
});
const mediaSources = z.object({
Bitrate: z.number(),
Container: z.string(),
DefaultAudioStreamIndex: z.number(),
ETag: z.string(),
Formats: z.array(z.any()),
GenPtsInput: z.boolean(),
Id: z.string(),
IgnoreDts: z.boolean(),
IgnoreIndex: z.boolean(),
IsInfiniteStream: z.boolean(),
IsRemote: z.boolean(),
MediaAttachments: z.array(z.any()),
MediaStreams: z.array(mediaStream),
Name: z.string(),
Path: z.string(),
Protocol: z.string(),
ReadAtNativeFramerate: z.boolean(),
RequiredHttpHeaders: z.any(),
RequiresClosing: z.boolean(),
RequiresLooping: z.boolean(),
RequiresOpening: z.boolean(),
RunTimeTicks: z.number(),
Size: z.number(),
SupportsDirectPlay: z.boolean(),
SupportsDirectStream: z.boolean(),
SupportsProbing: z.boolean(),
SupportsTranscoding: z.boolean(),
Type: z.string(),
});
const sessionInfo = z.object({
AdditionalUsers: z.array(z.any()),
ApplicationVersion: z.string(),
Capabilities: z.object({
PlayableMediaTypes: z.array(z.any()),
SupportedCommands: z.array(z.any()),
SupportsContentUploading: z.boolean(),
SupportsMediaControl: z.boolean(),
SupportsPersistentIdentifier: z.boolean(),
SupportsSync: z.boolean(),
}),
Client: z.string(),
DeviceId: z.string(),
DeviceName: z.string(),
HasCustomDeviceName: z.boolean(),
Id: z.string(),
IsActive: z.boolean(),
LastActivityDate: z.string(),
LastPlaybackCheckIn: z.string(),
NowPlayingQueue: z.array(z.any()),
NowPlayingQueueFullItems: z.array(z.any()),
PlayableMediaTypes: z.array(z.any()),
PlayState: z.object({
CanSeek: z.boolean(),
IsMuted: z.boolean(),
IsPaused: z.boolean(),
RepeatMode: z.string(),
}),
RemoteEndPoint: z.string(),
ServerId: z.string(),
SupportedCommands: z.array(z.any()),
SupportsMediaControl: z.boolean(),
SupportsRemoteControl: z.boolean(),
UserId: z.string(),
UserName: z.string(),
});
const configuration = z.object({
DisplayCollectionsView: z.boolean(),
DisplayMissingEpisodes: z.boolean(),
EnableLocalPassword: z.boolean(),
EnableNextEpisodeAutoPlay: z.boolean(),
GroupedFolders: z.array(z.any()),
HidePlayedInLatest: z.boolean(),
LatestItemsExcludes: z.array(z.any()),
MyMediaExcludes: z.array(z.any()),
OrderedViews: z.array(z.any()),
PlayDefaultAudioTrack: z.boolean(),
RememberAudioSelections: z.boolean(),
RememberSubtitleSelections: z.boolean(),
SubtitleLanguagePreference: z.string(),
SubtitleMode: z.string(),
});
const policy = z.object({
AccessSchedules: z.array(z.any()),
AuthenticationProviderId: z.string(),
BlockedChannels: z.array(z.any()),
BlockedMediaFolders: z.array(z.any()),
BlockedTags: z.array(z.any()),
BlockUnratedItems: z.array(z.any()),
EnableAllChannels: z.boolean(),
EnableAllDevices: z.boolean(),
EnableAllFolders: z.boolean(),
EnableAudioPlaybackTranscoding: z.boolean(),
EnableContentDeletion: z.boolean(),
EnableContentDeletionFromFolders: z.array(z.any()),
EnableContentDownloading: z.boolean(),
EnabledChannels: z.array(z.any()),
EnabledDevices: z.array(z.any()),
EnabledFolders: z.array(z.any()),
EnableLiveTvAccess: z.boolean(),
EnableLiveTvManagement: z.boolean(),
EnableMediaConversion: z.boolean(),
EnableMediaPlayback: z.boolean(),
EnablePlaybackRemuxing: z.boolean(),
EnablePublicSharing: z.boolean(),
EnableRemoteAccess: z.boolean(),
EnableRemoteControlOfOtherUsers: z.boolean(),
EnableSharedDeviceControl: z.boolean(),
EnableSyncTranscoding: z.boolean(),
EnableUserPreferenceAccess: z.boolean(),
EnableVideoPlaybackTranscoding: z.boolean(),
ForceRemoteSourceTranscoding: z.boolean(),
InvalidLoginAttemptCount: z.number(),
IsAdministrator: z.boolean(),
IsDisabled: z.boolean(),
IsHidden: z.boolean(),
LoginAttemptsBeforeLockout: z.number(),
MaxActiveSessions: z.number(),
PasswordResetProviderId: z.string(),
RemoteClientBitrateLimit: z.number(),
SyncPlayAccess: z.string(),
});
const user = z.object({
Configuration: configuration,
EnableAutoLogin: z.boolean(),
HasConfiguredEasyPassword: z.boolean(),
HasConfiguredPassword: z.boolean(),
HasPassword: z.boolean(),
Id: z.string(),
LastActivityDate: z.string(),
LastLoginDate: z.string(),
Name: z.string(),
Policy: policy,
ServerId: z.string(),
});
const authenticateParameters = z.object({
Pw: z.string(),
Username: z.string(),
});
const authenticate = z.object({
AccessToken: z.string(),
ServerId: z.string(),
SessionInfo: sessionInfo,
User: user,
});
const genreItem = z.object({
Id: z.string(),
Name: z.string(),
});
const genre = z.object({
BackdropImageTags: z.array(z.any()),
ChannelId: z.null(),
Id: z.string(),
ImageBlurHashes: imageBlurHashes,
ImageTags: imageTags,
LocationType: z.string(),
Name: z.string(),
ServerId: z.string(),
Type: z.string(),
});
const genreList = pagination.extend({
Items: z.array(genre),
});
const genreListSort = {
NAME: 'SortName',
} as const;
const genreListParameters = paginationParameters.merge(
baseParameters.extend({
SearchTerm: z.string().optional(),
SortBy: z.nativeEnum(genreListSort).optional(),
}),
);
const musicFolder = z.object({
BackdropImageTags: z.array(z.string()),
ChannelId: z.null(),
CollectionType: z.string(),
Id: z.string(),
ImageBlurHashes: imageBlurHashes,
ImageTags: imageTags,
IsFolder: z.boolean(),
LocationType: z.string(),
Name: z.string(),
ServerId: z.string(),
Type: z.string(),
UserData: userData,
});
const musicFolderListParameters = z.object({
UserId: z.string(),
});
const musicFolderList = z.object({
Items: z.array(musicFolder),
});
const playlist = z.object({
BackdropImageTags: z.array(z.string()),
ChannelId: z.null(),
ChildCount: z.number().optional(),
DateCreated: z.string(),
GenreItems: z.array(genreItem),
Genres: z.array(z.string()),
Id: z.string(),
ImageBlurHashes: imageBlurHashes,
ImageTags: imageTags,
IsFolder: z.boolean(),
LocationType: z.string(),
MediaType: z.string(),
Name: z.string(),
Overview: z.string().optional(),
RunTimeTicks: z.number(),
ServerId: z.string(),
Type: z.string(),
UserData: userData,
});
const playlistListSort = {
ALBUM_ARTIST: 'AlbumArtist,SortName',
DURATION: 'Runtime',
NAME: 'SortName',
RECENTLY_ADDED: 'DateCreated,SortName',
SONG_COUNT: 'ChildCount',
} as const;
const playlistListParameters = paginationParameters.merge(
baseParameters.extend({
IncludeItemTypes: z.literal('Playlist'),
SortBy: z.nativeEnum(playlistListSort).optional(),
}),
);
const playlistList = pagination.extend({
Items: z.array(playlist),
});
const genericItem = z.object({
Id: z.string(),
Name: z.string(),
});
const participant = z.object({
Id: z.string(),
Name: z.string(),
Type: z.string().optional(),
});
const songDetailParameters = baseParameters;
const song = z.object({
Album: z.string(),
AlbumArtist: z.string(),
AlbumArtists: z.array(genericItem),
AlbumId: z.string().optional(),
AlbumPrimaryImageTag: z.string(),
ArtistItems: z.array(genericItem),
Artists: z.array(z.string()),
BackdropImageTags: z.array(z.string()),
ChannelId: z.null(),
DateCreated: z.string(),
ExternalUrls: z.array(externalUrl),
GenreItems: z.array(genericItem),
Genres: z.array(z.string()),
Id: z.string(),
ImageBlurHashes: imageBlurHashes,
ImageTags: imageTags,
IndexNumber: z.number(),
IsFolder: z.boolean(),
LocationType: z.string(),
LUFS: z.number().optional(),
MediaSources: z.array(mediaSources),
MediaType: z.string(),
Name: z.string(),
NormalizationGain: z.number().optional(),
ParentIndexNumber: z.number(),
People: participant.array().optional(),
PlaylistItemId: z.string().optional(),
PremiereDate: z.string().optional(),
ProductionYear: z.number(),
RunTimeTicks: z.number(),
ServerId: z.string(),
SortName: z.string(),
Tags: z.string().array().optional(),
Type: z.string(),
UserData: userData.optional(),
});
const providerIds = z.object({
MusicBrainzAlbum: z.string().optional(),
MusicBrainzArtist: z.string().optional(),
});
const albumArtist = z.object({
AlbumCount: z.number().optional(),
BackdropImageTags: z.array(z.string()),
ChannelId: z.null(),
DateCreated: z.string(),
ExternalUrls: z.array(externalUrl),
GenreItems: z.array(genreItem),
Genres: z.array(z.string()),
Id: z.string(),
ImageBlurHashes: imageBlurHashes,
ImageTags: imageTags,
LocationType: z.string(),
Name: z.string(),
Overview: z.string(),
ProviderIds: providerIds.optional(),
RunTimeTicks: z.number(),
ServerId: z.string(),
SongCount: z.number().optional(),
Type: z.string(),
UserData: userData.optional(),
});
const albumDetailParameters = baseParameters;
const album = z.object({
AlbumArtist: z.string(),
AlbumArtists: z.array(genericItem),
AlbumPrimaryImageTag: z.string(),
ArtistItems: z.array(genericItem),
Artists: z.array(z.string()),
ChannelId: z.null(),
ChildCount: z.number().optional(),
DateCreated: z.string(),
DateLastMediaAdded: z.string().optional(),
ExternalUrls: z.array(externalUrl),
GenreItems: z.array(genericItem),
Genres: z.array(z.string()),
Id: z.string(),
ImageBlurHashes: imageBlurHashes,
ImageTags: imageTags,
IsFolder: z.boolean(),
LocationType: z.string(),
Name: z.string(),
ParentLogoImageTag: z.string(),
ParentLogoItemId: z.string(),
People: participant.array().optional(),
PremiereDate: z.string().optional(),
ProductionYear: z.number(),
ProviderIds: providerIds.optional(),
RunTimeTicks: z.number(),
ServerId: z.string(),
Songs: z.array(song).optional(), // This is not a native Jellyfin property -- this is used for combined album detail
Tags: z.string().array().optional(),
Type: z.string(),
UserData: userData.optional(),
});
const albumListSort = {
ALBUM_ARTIST: 'AlbumArtist,SortName',
COMMUNITY_RATING: 'CommunityRating,SortName',
CRITIC_RATING: 'CriticRating,SortName',
NAME: 'SortName',
PLAY_COUNT: 'PlayCount',
RANDOM: 'Random,SortName',
RECENTLY_ADDED: 'DateCreated,SortName',
RELEASE_DATE: 'ProductionYear,PremiereDate,SortName',
} as const;
const albumListParameters = paginationParameters.merge(
baseParameters.extend({
Filters: z.string().optional(),
GenreIds: z.string().optional(),
Genres: z.string().optional(),
IncludeItemTypes: z.literal('MusicAlbum'),
IsFavorite: z.boolean().optional(),
SearchTerm: z.string().optional(),
SortBy: z.nativeEnum(albumListSort).optional(),
Tags: z.string().optional(),
Years: z.string().optional(),
}),
);
const albumList = pagination.extend({
Items: z.array(album),
});
const albumArtistListSort = {
ALBUM: 'Album,SortName',
DURATION: 'Runtime,AlbumArtist,Album,SortName',
NAME: 'SortName,Name',
RANDOM: 'Random,SortName',
RECENTLY_ADDED: 'DateCreated,SortName',
RELEASE_DATE: 'PremiereDate,AlbumArtist,Album,SortName',
} as const;
const albumArtistListParameters = paginationParameters.merge(
baseParameters.extend({
Filters: z.string().optional(),
Genres: z.string().optional(),
SortBy: z.nativeEnum(albumArtistListSort).optional(),
Years: z.string().optional(),
}),
);
const albumArtistList = pagination.extend({
Items: z.array(albumArtist),
});
const similarArtistListParameters = baseParameters.extend({
Limit: z.number().optional(),
});
const songListSort = {
ALBUM: 'Album,SortName',
ALBUM_ARTIST: 'AlbumArtist,Album,SortName',
ALBUM_DETAIL: 'ParentIndexNumber,IndexNumber,SortName',
ARTIST: 'Artist,Album,SortName',
COMMUNITY_RATING: 'CommunityRating,SortName',
DURATION: 'Runtime,AlbumArtist,Album,SortName',
NAME: 'Name',
PLAY_COUNT: 'PlayCount,SortName',
RANDOM: 'Random,SortName',
RECENTLY_ADDED: 'DateCreated,SortName',
RECENTLY_PLAYED: 'DatePlayed,SortName',
RELEASE_DATE: 'PremiereDate,AlbumArtist,Album,SortName',
} as const;
const songListParameters = paginationParameters.merge(
baseParameters.extend({
AlbumArtistIds: z.string().optional(),
AlbumIds: z.string().optional(),
ArtistIds: z.string().optional(),
Filters: z.string().optional(),
GenreIds: z.string().optional(),
Genres: z.string().optional(),
IsFavorite: z.boolean().optional(),
IsPlayed: z.boolean().optional(),
SearchTerm: z.string().optional(),
SortBy: z.nativeEnum(songListSort).optional(),
Tags: z.string().optional(),
Years: z.string().optional(),
}),
);
const songList = pagination.extend({
Items: z.array(song),
});
const playlistSongList = songList;
const topSongsList = songList;
const playlistDetailParameters = baseParameters.extend({
Ids: z.string(),
});
const createPlaylistParameters = z.object({
IsPublic: z.boolean().optional(),
MediaType: z.literal('Audio'),
Name: z.string(),
UserId: z.string(),
});
const createPlaylist = z.object({
Id: z.string(),
});
const updatePlaylist = z.null();
const updatePlaylistParameters = z.object({
Genres: z.array(genreItem),
IsPublic: z.boolean().optional(),
MediaType: z.literal('Audio'),
Name: z.string(),
PremiereDate: z.null(),
ProviderIds: z.object({}),
Tags: z.array(genericItem),
UserId: z.string(),
});
const addToPlaylist = z.object({
Added: z.number(),
});
const addToPlaylistParameters = z.object({
Ids: z.string(),
UserId: z.string(),
});
const removeFromPlaylist = z.null();
const removeFromPlaylistParameters = z.object({
EntryIds: z.string(),
});
const deletePlaylist = z.null();
const deletePlaylistParameters = z.object({
Id: z.string(),
});
const scrobbleParameters = z.object({
EventName: z.string().optional(),
IsPaused: z.boolean().optional(),
ItemId: z.string(),
PositionTicks: z.number().optional(),
});
const scrobble = z.any();
const favorite = z.object({
IsFavorite: z.boolean(),
ItemId: z.string(),
Key: z.string(),
LastPlayedDate: z.string(),
Likes: z.boolean(),
PlaybackPositionTicks: z.number(),
PlayCount: z.number(),
Played: z.boolean(),
PlayedPercentage: z.number(),
Rating: z.number(),
UnplayedItemCount: z.number(),
});
const favoriteParameters = z.object({});
const searchParameters = paginationParameters.merge(baseParameters);
const search = z.any();
const lyricText = z.object({
Start: z.number().optional(),
Text: z.string(),
});
const lyrics = z.object({
Lyrics: z.array(lyricText),
});
const serverInfo = z.object({
Version: z.string(),
});
const similarSongsParameters = z.object({
Fields: z.string().optional(),
Limit: z.number().optional(),
UserId: z.string().optional(),
});
const similarSongs = pagination.extend({
Items: z.array(song),
});
export enum JellyfinExtensions {
SONG_LYRICS = 'songLyrics',
}
const moveItem = z.null();
const filterListParameters = z.object({
IncludeItemTypes: z.string().optional(),
ParentId: z.string().optional(),
UserId: z.string().optional(),
});
const filters = z.object({
Genres: z.string().array().optional(),
Tags: z.string().array().optional(),
Years: z.number().array().optional(),
});
export const jfType = {
_enum: {
albumArtistList: albumArtistListSort,
albumList: albumListSort,
collection: jfCollection,
external: jfExternal,
genreList: genreListSort,
image: jfImage,
playlistList: playlistListSort,
songList: songListSort,
},
_parameters: {
addToPlaylist: addToPlaylistParameters,
albumArtistDetail: baseParameters,
albumArtistList: albumArtistListParameters,
albumDetail: albumDetailParameters,
albumList: albumListParameters,
authenticate: authenticateParameters,
createPlaylist: createPlaylistParameters,
deletePlaylist: deletePlaylistParameters,
favorite: favoriteParameters,
filterList: filterListParameters,
genreList: genreListParameters,
musicFolderList: musicFolderListParameters,
playlistDetail: playlistDetailParameters,
playlistList: playlistListParameters,
removeFromPlaylist: removeFromPlaylistParameters,
scrobble: scrobbleParameters,
search: searchParameters,
similarArtistList: similarArtistListParameters,
similarSongs: similarSongsParameters,
songDetail: songDetailParameters,
songList: songListParameters,
updatePlaylist: updatePlaylistParameters,
},
_response: {
addToPlaylist,
album,
albumArtist,
albumArtistList,
albumList,
authenticate,
createPlaylist,
deletePlaylist,
error,
favorite,
filters,
genre,
genreList,
lyrics,
moveItem,
musicFolderList,
playlist,
playlistList,
playlistSongList,
removeFromPlaylist,
scrobble,
search,
serverInfo,
similarSongs,
song,
songList,
topSongsList,
updatePlaylist,
user,
},
};