mirror of
https://github.com/antebudimir/feishin.git
synced 2025-12-31 18:13:31 +00:00
reorganize global types to shared directory
This commit is contained in:
parent
26c02e03c5
commit
9db2e51d2d
17 changed files with 160 additions and 144 deletions
639
src/shared/api/jellyfin.types.ts
Normal file
639
src/shared/api/jellyfin.types.ts
Normal file
|
|
@ -0,0 +1,639 @@
|
|||
export enum JFAlbumArtistListSort {
|
||||
ALBUM = 'Album,SortName',
|
||||
DURATION = 'Runtime,AlbumArtist,Album,SortName',
|
||||
NAME = 'SortName,Name',
|
||||
RANDOM = 'Random,SortName',
|
||||
RECENTLY_ADDED = 'DateCreated,SortName',
|
||||
RELEASE_DATE = 'PremiereDate,AlbumArtist,Album,SortName',
|
||||
}
|
||||
|
||||
export enum JFAlbumListSort {
|
||||
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',
|
||||
}
|
||||
|
||||
export enum JFArtistListSort {
|
||||
ALBUM = 'Album,SortName',
|
||||
DURATION = 'Runtime,AlbumArtist,Album,SortName',
|
||||
NAME = 'SortName,Name',
|
||||
RANDOM = 'Random,SortName',
|
||||
RECENTLY_ADDED = 'DateCreated,SortName',
|
||||
RELEASE_DATE = 'PremiereDate,AlbumArtist,Album,SortName',
|
||||
}
|
||||
|
||||
export enum JFCollectionType {
|
||||
MUSIC = 'music',
|
||||
PLAYLISTS = 'playlists',
|
||||
}
|
||||
|
||||
export enum JFExternalType {
|
||||
MUSICBRAINZ = 'MusicBrainz',
|
||||
THEAUDIODB = 'TheAudioDb',
|
||||
}
|
||||
|
||||
export enum JFGenreListSort {
|
||||
NAME = 'SortName',
|
||||
}
|
||||
|
||||
export enum JFImageType {
|
||||
LOGO = 'Logo',
|
||||
PRIMARY = 'Primary',
|
||||
}
|
||||
|
||||
export enum JFItemType {
|
||||
AUDIO = 'Audio',
|
||||
MUSICALBUM = 'MusicAlbum',
|
||||
}
|
||||
|
||||
export enum JFPlaylistListSort {
|
||||
ALBUM_ARTIST = 'AlbumArtist,SortName',
|
||||
DURATION = 'Runtime',
|
||||
NAME = 'SortName',
|
||||
RECENTLY_ADDED = 'DateCreated,SortName',
|
||||
SONG_COUNT = 'ChildCount',
|
||||
}
|
||||
|
||||
export enum JFSongListSort {
|
||||
ALBUM = 'Album,SortName',
|
||||
ALBUM_ARTIST = 'AlbumArtist,Album,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',
|
||||
}
|
||||
|
||||
export enum JFSortOrder {
|
||||
ASC = 'Ascending',
|
||||
DESC = 'Descending',
|
||||
}
|
||||
|
||||
export type JFAddToPlaylist = null;
|
||||
|
||||
export type JFAddToPlaylistParams = {
|
||||
ids: string[];
|
||||
userId: string;
|
||||
};
|
||||
|
||||
export type JFAddToPlaylistResponse = {
|
||||
added: number;
|
||||
};
|
||||
|
||||
export type JFAlbum = {
|
||||
AlbumArtist: string;
|
||||
AlbumArtists: JFGenericItem[];
|
||||
AlbumPrimaryImageTag: string;
|
||||
ArtistItems: JFGenericItem[];
|
||||
Artists: string[];
|
||||
ChannelId: null;
|
||||
ChildCount?: number;
|
||||
DateCreated: string;
|
||||
DateLastMediaAdded?: string;
|
||||
ExternalUrls: ExternalURL[];
|
||||
GenreItems: JFGenericItem[];
|
||||
Genres: string[];
|
||||
Id: string;
|
||||
ImageBlurHashes: ImageBlurHashes;
|
||||
ImageTags: ImageTags;
|
||||
IsFolder: boolean;
|
||||
LocationType: string;
|
||||
Name: string;
|
||||
ParentLogoImageTag: string;
|
||||
ParentLogoItemId: string;
|
||||
PremiereDate?: string;
|
||||
ProductionYear: number;
|
||||
RunTimeTicks: number;
|
||||
ServerId: string;
|
||||
Type: string;
|
||||
UserData?: UserData;
|
||||
} & {
|
||||
songs?: JFSong[];
|
||||
};
|
||||
|
||||
export type JFAlbumArtist = {
|
||||
BackdropImageTags: string[];
|
||||
ChannelId: null;
|
||||
DateCreated: string;
|
||||
ExternalUrls: ExternalURL[];
|
||||
GenreItems: GenreItem[];
|
||||
Genres: string[];
|
||||
Id: string;
|
||||
ImageBlurHashes: any;
|
||||
ImageTags: ImageTags;
|
||||
LocationType: string;
|
||||
Name: string;
|
||||
Overview?: string;
|
||||
RunTimeTicks: number;
|
||||
ServerId: string;
|
||||
Type: string;
|
||||
UserData: {
|
||||
IsFavorite: boolean;
|
||||
Key: string;
|
||||
PlaybackPositionTicks: number;
|
||||
PlayCount: number;
|
||||
Played: boolean;
|
||||
};
|
||||
} & {
|
||||
similarArtists: {
|
||||
items: JFAlbumArtist[];
|
||||
};
|
||||
};
|
||||
|
||||
export type JFAlbumArtistDetail = JFAlbumArtistDetailResponse;
|
||||
|
||||
export type JFAlbumArtistDetailResponse = JFAlbumArtist;
|
||||
|
||||
export type JFAlbumArtistList = {
|
||||
items: JFAlbumArtist[];
|
||||
startIndex: number;
|
||||
totalRecordCount: number;
|
||||
};
|
||||
|
||||
export type JFAlbumArtistListParams = JFBaseParams &
|
||||
JFPaginationParams & {
|
||||
filters?: string;
|
||||
genres?: string;
|
||||
sortBy?: JFAlbumArtistListSort;
|
||||
years?: string;
|
||||
};
|
||||
|
||||
export interface JFAlbumArtistListResponse extends JFBasePaginatedResponse {
|
||||
Items: JFAlbumArtist[];
|
||||
}
|
||||
|
||||
export type JFAlbumDetail = JFAlbum & { songs?: JFSong[] };
|
||||
|
||||
export type JFAlbumDetailResponse = JFAlbum;
|
||||
|
||||
export type JFAlbumList = {
|
||||
items: JFAlbum[];
|
||||
startIndex: number;
|
||||
totalRecordCount: number;
|
||||
};
|
||||
|
||||
export type JFAlbumListParams = JFBaseParams &
|
||||
JFPaginationParams & {
|
||||
albumArtistIds?: string;
|
||||
artistIds?: string;
|
||||
filters?: string;
|
||||
genreIds?: string;
|
||||
genres?: string;
|
||||
includeItemTypes: 'MusicAlbum';
|
||||
isFavorite?: boolean;
|
||||
searchTerm?: string;
|
||||
sortBy?: JFAlbumListSort;
|
||||
tags?: string;
|
||||
years?: string;
|
||||
};
|
||||
|
||||
export interface JFAlbumListResponse extends JFBasePaginatedResponse {
|
||||
Items: JFAlbum[];
|
||||
}
|
||||
|
||||
export type JFArtist = {
|
||||
BackdropImageTags: string[];
|
||||
ChannelId: null;
|
||||
DateCreated: string;
|
||||
ExternalUrls: ExternalURL[];
|
||||
GenreItems: GenreItem[];
|
||||
Genres: string[];
|
||||
Id: string;
|
||||
ImageBlurHashes: any;
|
||||
ImageTags: string[];
|
||||
LocationType: string;
|
||||
Name: string;
|
||||
Overview?: string;
|
||||
RunTimeTicks: number;
|
||||
ServerId: string;
|
||||
Type: string;
|
||||
};
|
||||
|
||||
export type JFArtistList = JFArtistListResponse;
|
||||
|
||||
export type JFArtistListParams = JFBaseParams &
|
||||
JFPaginationParams & {
|
||||
filters?: string;
|
||||
genres?: string;
|
||||
sortBy?: JFArtistListSort;
|
||||
years?: string;
|
||||
};
|
||||
|
||||
export interface JFArtistListResponse extends JFBasePaginatedResponse {
|
||||
Items: JFAlbumArtist[];
|
||||
}
|
||||
|
||||
export interface JFAuthenticate {
|
||||
AccessToken: string;
|
||||
ServerId: string;
|
||||
SessionInfo: SessionInfo;
|
||||
User: User;
|
||||
}
|
||||
|
||||
export type JFBasePaginatedResponse = {
|
||||
StartIndex: number;
|
||||
TotalRecordCount: number;
|
||||
};
|
||||
|
||||
export type JFCreatePlaylist = JFCreatePlaylistResponse;
|
||||
|
||||
export type JFCreatePlaylistResponse = {
|
||||
Id: string;
|
||||
};
|
||||
|
||||
export type JFGenericItem = {
|
||||
Id: string;
|
||||
Name: string;
|
||||
};
|
||||
|
||||
export type JFGenre = {
|
||||
BackdropImageTags: any[];
|
||||
ChannelId: null;
|
||||
Id: string;
|
||||
ImageBlurHashes: any;
|
||||
ImageTags: ImageTags;
|
||||
LocationType: string;
|
||||
Name: string;
|
||||
ServerId: string;
|
||||
Type: string;
|
||||
};
|
||||
|
||||
export type JFGenreList = JFGenreListResponse;
|
||||
|
||||
export interface JFGenreListResponse extends JFBasePaginatedResponse {
|
||||
Items: JFGenre[];
|
||||
}
|
||||
|
||||
export type JFMusicFolder = {
|
||||
BackdropImageTags: string[];
|
||||
ChannelId: null;
|
||||
CollectionType: string;
|
||||
Id: string;
|
||||
ImageBlurHashes: ImageBlurHashes;
|
||||
ImageTags: ImageTags;
|
||||
IsFolder: boolean;
|
||||
LocationType: string;
|
||||
Name: string;
|
||||
ServerId: string;
|
||||
Type: string;
|
||||
UserData: UserData;
|
||||
};
|
||||
|
||||
export type JFMusicFolderList = JFMusicFolder[];
|
||||
|
||||
export interface JFMusicFolderListResponse extends JFBasePaginatedResponse {
|
||||
Items: JFMusicFolder[];
|
||||
}
|
||||
|
||||
export type JFPlaylist = {
|
||||
BackdropImageTags: string[];
|
||||
ChannelId: null;
|
||||
ChildCount?: number;
|
||||
DateCreated: string;
|
||||
GenreItems: GenreItem[];
|
||||
Genres: string[];
|
||||
Id: string;
|
||||
ImageBlurHashes: ImageBlurHashes;
|
||||
ImageTags: ImageTags;
|
||||
IsFolder: boolean;
|
||||
LocationType: string;
|
||||
MediaType: string;
|
||||
Name: string;
|
||||
Overview?: string;
|
||||
RunTimeTicks: number;
|
||||
ServerId: string;
|
||||
Type: string;
|
||||
UserData: UserData;
|
||||
};
|
||||
|
||||
export type JFPlaylistDetail = JFPlaylist & { songs?: JFSong[] };
|
||||
|
||||
export type JFPlaylistDetailResponse = JFPlaylist;
|
||||
|
||||
export type JFPlaylistList = {
|
||||
items: JFPlaylist[];
|
||||
startIndex: number;
|
||||
totalRecordCount: number;
|
||||
};
|
||||
|
||||
export interface JFPlaylistListResponse extends JFBasePaginatedResponse {
|
||||
Items: JFPlaylist[];
|
||||
}
|
||||
|
||||
export type JFRemoveFromPlaylist = null;
|
||||
|
||||
export type JFRemoveFromPlaylistParams = {
|
||||
entryIds: string[];
|
||||
};
|
||||
|
||||
export type JFRemoveFromPlaylistResponse = null;
|
||||
|
||||
export type JFRequestParams = {
|
||||
albumArtistIds?: string;
|
||||
artistIds?: string;
|
||||
enableImageTypes?: string;
|
||||
enableTotalRecordCount?: boolean;
|
||||
enableUserData?: boolean;
|
||||
excludeItemTypes?: string;
|
||||
fields?: string;
|
||||
imageTypeLimit?: number;
|
||||
includeItemTypes?: string;
|
||||
isFavorite?: boolean;
|
||||
limit?: number;
|
||||
parentId?: string;
|
||||
recursive?: boolean;
|
||||
searchTerm?: string;
|
||||
sortBy?: string;
|
||||
sortOrder?: 'Ascending' | 'Descending';
|
||||
startIndex?: number;
|
||||
userId?: string;
|
||||
};
|
||||
|
||||
export type JFSong = {
|
||||
Album: string;
|
||||
AlbumArtist: string;
|
||||
AlbumArtists: JFGenericItem[];
|
||||
AlbumId: string;
|
||||
AlbumPrimaryImageTag: string;
|
||||
ArtistItems: JFGenericItem[];
|
||||
Artists: string[];
|
||||
BackdropImageTags: string[];
|
||||
ChannelId: null;
|
||||
DateCreated: string;
|
||||
ExternalUrls: ExternalURL[];
|
||||
GenreItems: JFGenericItem[];
|
||||
Genres: string[];
|
||||
Id: string;
|
||||
ImageBlurHashes: ImageBlurHashes;
|
||||
ImageTags: ImageTags;
|
||||
IndexNumber: number;
|
||||
IsFolder: boolean;
|
||||
LocationType: string;
|
||||
MediaSources: MediaSources[];
|
||||
MediaType: string;
|
||||
Name: string;
|
||||
ParentIndexNumber: number;
|
||||
PlaylistItemId?: string;
|
||||
PremiereDate?: string;
|
||||
ProductionYear: number;
|
||||
RunTimeTicks: number;
|
||||
ServerId: string;
|
||||
SortName: string;
|
||||
Type: string;
|
||||
UserData?: UserData;
|
||||
};
|
||||
|
||||
export type JFSongList = {
|
||||
items: JFSong[];
|
||||
startIndex: number;
|
||||
totalRecordCount: number;
|
||||
};
|
||||
|
||||
export type JFSongListParams = JFBaseParams &
|
||||
JFPaginationParams & {
|
||||
albumArtistIds?: string;
|
||||
albumIds?: string;
|
||||
artistIds?: string;
|
||||
contributingArtistIds?: string;
|
||||
filters?: string;
|
||||
genreIds?: string;
|
||||
genres?: string;
|
||||
ids?: string;
|
||||
includeItemTypes: 'Audio';
|
||||
searchTerm?: string;
|
||||
sortBy?: JFSongListSort;
|
||||
years?: string;
|
||||
};
|
||||
|
||||
export interface JFSongListResponse extends JFBasePaginatedResponse {
|
||||
Items: JFSong[];
|
||||
}
|
||||
|
||||
type Capabilities = {
|
||||
PlayableMediaTypes: any[];
|
||||
SupportedCommands: any[];
|
||||
SupportsContentUploading: boolean;
|
||||
SupportsMediaControl: boolean;
|
||||
SupportsPersistentIdentifier: boolean;
|
||||
SupportsSync: boolean;
|
||||
};
|
||||
|
||||
type Configuration = {
|
||||
DisplayCollectionsView: boolean;
|
||||
DisplayMissingEpisodes: boolean;
|
||||
EnableLocalPassword: boolean;
|
||||
EnableNextEpisodeAutoPlay: boolean;
|
||||
GroupedFolders: any[];
|
||||
HidePlayedInLatest: boolean;
|
||||
LatestItemsExcludes: any[];
|
||||
MyMediaExcludes: any[];
|
||||
OrderedViews: any[];
|
||||
PlayDefaultAudioTrack: boolean;
|
||||
RememberAudioSelections: boolean;
|
||||
RememberSubtitleSelections: boolean;
|
||||
SubtitleLanguagePreference: string;
|
||||
SubtitleMode: string;
|
||||
};
|
||||
|
||||
type ExternalURL = {
|
||||
Name: string;
|
||||
Url: string;
|
||||
};
|
||||
|
||||
type GenreItem = {
|
||||
Id: string;
|
||||
Name: string;
|
||||
};
|
||||
|
||||
type ImageBlurHashes = {
|
||||
Backdrop?: any;
|
||||
Logo?: any;
|
||||
Primary?: any;
|
||||
};
|
||||
|
||||
type ImageTags = {
|
||||
Logo?: string;
|
||||
Primary?: string;
|
||||
};
|
||||
|
||||
type JFBaseParams = {
|
||||
enableImageTypes?: JFImageType[];
|
||||
fields?: string;
|
||||
imageTypeLimit?: number;
|
||||
parentId?: string;
|
||||
recursive?: boolean;
|
||||
searchTerm?: string;
|
||||
userId?: string;
|
||||
};
|
||||
|
||||
type JFPaginationParams = {
|
||||
limit?: number;
|
||||
nameStartsWith?: string;
|
||||
sortOrder?: JFSortOrder;
|
||||
startIndex?: number;
|
||||
};
|
||||
|
||||
type MediaSources = {
|
||||
Bitrate: number;
|
||||
Container: string;
|
||||
DefaultAudioStreamIndex: number;
|
||||
ETag: string;
|
||||
Formats: any[];
|
||||
GenPtsInput: boolean;
|
||||
Id: string;
|
||||
IgnoreDts: boolean;
|
||||
IgnoreIndex: boolean;
|
||||
IsInfiniteStream: boolean;
|
||||
IsRemote: boolean;
|
||||
MediaAttachments: any[];
|
||||
MediaStreams: MediaStream[];
|
||||
Name: string;
|
||||
Path: string;
|
||||
Protocol: string;
|
||||
ReadAtNativeFramerate: boolean;
|
||||
RequiredHttpHeaders: any;
|
||||
RequiresClosing: boolean;
|
||||
RequiresLooping: boolean;
|
||||
RequiresOpening: boolean;
|
||||
RunTimeTicks: number;
|
||||
Size: number;
|
||||
SupportsDirectPlay: boolean;
|
||||
SupportsDirectStream: boolean;
|
||||
SupportsProbing: boolean;
|
||||
SupportsTranscoding: boolean;
|
||||
Type: string;
|
||||
};
|
||||
|
||||
type MediaStream = {
|
||||
AspectRatio?: string;
|
||||
BitDepth?: number;
|
||||
BitRate?: number;
|
||||
ChannelLayout?: string;
|
||||
Channels?: number;
|
||||
Codec: string;
|
||||
CodecTimeBase: string;
|
||||
ColorSpace?: string;
|
||||
Comment?: string;
|
||||
DisplayTitle?: string;
|
||||
Height?: number;
|
||||
Index: number;
|
||||
IsDefault: boolean;
|
||||
IsExternal: boolean;
|
||||
IsForced: boolean;
|
||||
IsInterlaced: boolean;
|
||||
IsTextSubtitleStream: boolean;
|
||||
Level: number;
|
||||
PixelFormat?: string;
|
||||
Profile?: string;
|
||||
RealFrameRate?: number;
|
||||
RefFrames?: number;
|
||||
SampleRate?: number;
|
||||
SupportsExternalStream: boolean;
|
||||
TimeBase: string;
|
||||
Type: string;
|
||||
Width?: number;
|
||||
};
|
||||
|
||||
type PlayState = {
|
||||
CanSeek: boolean;
|
||||
IsMuted: boolean;
|
||||
IsPaused: boolean;
|
||||
RepeatMode: string;
|
||||
};
|
||||
|
||||
type Policy = {
|
||||
AccessSchedules: any[];
|
||||
AuthenticationProviderId: string;
|
||||
BlockedChannels: any[];
|
||||
BlockedMediaFolders: any[];
|
||||
BlockedTags: any[];
|
||||
BlockUnratedItems: any[];
|
||||
EnableAllChannels: boolean;
|
||||
EnableAllDevices: boolean;
|
||||
EnableAllFolders: boolean;
|
||||
EnableAudioPlaybackTranscoding: boolean;
|
||||
EnableContentDeletion: boolean;
|
||||
EnableContentDeletionFromFolders: any[];
|
||||
EnableContentDownloading: boolean;
|
||||
EnabledChannels: any[];
|
||||
EnabledDevices: any[];
|
||||
EnabledFolders: any[];
|
||||
EnableLiveTvAccess: boolean;
|
||||
EnableLiveTvManagement: boolean;
|
||||
EnableMediaConversion: boolean;
|
||||
EnableMediaPlayback: boolean;
|
||||
EnablePlaybackRemuxing: boolean;
|
||||
EnablePublicSharing: boolean;
|
||||
EnableRemoteAccess: boolean;
|
||||
EnableRemoteControlOfOtherUsers: boolean;
|
||||
EnableSharedDeviceControl: boolean;
|
||||
EnableSyncTranscoding: boolean;
|
||||
EnableUserPreferenceAccess: boolean;
|
||||
EnableVideoPlaybackTranscoding: boolean;
|
||||
ForceRemoteSourceTranscoding: boolean;
|
||||
InvalidLoginAttemptCount: number;
|
||||
IsAdministrator: boolean;
|
||||
IsDisabled: boolean;
|
||||
IsHidden: boolean;
|
||||
LoginAttemptsBeforeLockout: number;
|
||||
MaxActiveSessions: number;
|
||||
PasswordResetProviderId: string;
|
||||
RemoteClientBitrateLimit: number;
|
||||
SyncPlayAccess: string;
|
||||
};
|
||||
|
||||
type SessionInfo = {
|
||||
AdditionalUsers: any[];
|
||||
ApplicationVersion: string;
|
||||
Capabilities: Capabilities;
|
||||
Client: string;
|
||||
DeviceId: string;
|
||||
DeviceName: string;
|
||||
HasCustomDeviceName: boolean;
|
||||
Id: string;
|
||||
IsActive: boolean;
|
||||
LastActivityDate: string;
|
||||
LastPlaybackCheckIn: string;
|
||||
NowPlayingQueue: any[];
|
||||
NowPlayingQueueFullItems: any[];
|
||||
PlayableMediaTypes: any[];
|
||||
PlayState: PlayState;
|
||||
RemoteEndPoint: string;
|
||||
ServerId: string;
|
||||
SupportedCommands: any[];
|
||||
SupportsMediaControl: boolean;
|
||||
SupportsRemoteControl: boolean;
|
||||
UserId: string;
|
||||
UserName: string;
|
||||
};
|
||||
|
||||
type User = {
|
||||
Configuration: Configuration;
|
||||
EnableAutoLogin: boolean;
|
||||
HasConfiguredEasyPassword: boolean;
|
||||
HasConfiguredPassword: boolean;
|
||||
HasPassword: boolean;
|
||||
Id: string;
|
||||
LastActivityDate: string;
|
||||
LastLoginDate: string;
|
||||
Name: string;
|
||||
Policy: Policy;
|
||||
ServerId: string;
|
||||
};
|
||||
|
||||
type UserData = {
|
||||
IsFavorite: boolean;
|
||||
Key: string;
|
||||
PlaybackPositionTicks: number;
|
||||
PlayCount: number;
|
||||
Played: boolean;
|
||||
};
|
||||
485
src/shared/api/jellyfin/jellyfin-normalize.ts
Normal file
485
src/shared/api/jellyfin/jellyfin-normalize.ts
Normal 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,
|
||||
};
|
||||
778
src/shared/api/jellyfin/jellyfin-types.ts
Normal file
778
src/shared/api/jellyfin/jellyfin-types.ts
Normal 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,
|
||||
},
|
||||
};
|
||||
533
src/shared/api/navidrome.types.ts
Normal file
533
src/shared/api/navidrome.types.ts
Normal file
|
|
@ -0,0 +1,533 @@
|
|||
import { SSArtistInfo } from '/@/shared/api/subsonic.types';
|
||||
|
||||
export enum NDAlbumArtistListSort {
|
||||
ALBUM_COUNT = 'albumCount',
|
||||
FAVORITED = 'starred_at',
|
||||
NAME = 'name',
|
||||
PLAY_COUNT = 'playCount',
|
||||
RATING = 'rating',
|
||||
SONG_COUNT = 'songCount',
|
||||
}
|
||||
|
||||
export enum NDAlbumListSort {
|
||||
ALBUM_ARTIST = 'album_artist',
|
||||
ARTIST = 'artist',
|
||||
DURATION = 'duration',
|
||||
NAME = 'name',
|
||||
PLAY_COUNT = 'play_count',
|
||||
PLAY_DATE = 'play_date',
|
||||
RANDOM = 'random',
|
||||
RATING = 'rating',
|
||||
RECENTLY_ADDED = 'recently_added',
|
||||
SONG_COUNT = 'songCount',
|
||||
STARRED = 'starred_at',
|
||||
YEAR = 'max_year',
|
||||
}
|
||||
|
||||
export enum NDGenreListSort {
|
||||
NAME = 'name',
|
||||
}
|
||||
|
||||
export enum NDPlaylistListSort {
|
||||
DURATION = 'duration',
|
||||
NAME = 'name',
|
||||
OWNER = 'owner_name',
|
||||
PUBLIC = 'public',
|
||||
SONG_COUNT = 'songCount',
|
||||
UPDATED_AT = 'updatedAt',
|
||||
}
|
||||
|
||||
export enum NDSongListSort {
|
||||
ALBUM = 'album',
|
||||
ALBUM_ARTIST = 'order_album_artist_name',
|
||||
ALBUM_SONGS = 'album',
|
||||
ARTIST = 'artist',
|
||||
BPM = 'bpm',
|
||||
CHANNELS = 'channels',
|
||||
COMMENT = 'comment',
|
||||
DURATION = 'duration',
|
||||
FAVORITED = 'starred_at',
|
||||
GENRE = 'genre',
|
||||
ID = 'id',
|
||||
PLAY_COUNT = 'playCount',
|
||||
PLAY_DATE = 'playDate',
|
||||
RANDOM = 'random',
|
||||
RATING = 'rating',
|
||||
RECENTLY_ADDED = 'createdAt',
|
||||
TITLE = 'title',
|
||||
TRACK = 'track',
|
||||
YEAR = 'year',
|
||||
}
|
||||
|
||||
export enum NDSortOrder {
|
||||
ASC = 'ASC',
|
||||
DESC = 'DESC',
|
||||
}
|
||||
|
||||
export type NDAddToPlaylist = null;
|
||||
|
||||
export type NDAddToPlaylistBody = {
|
||||
ids: string[];
|
||||
};
|
||||
|
||||
export type NDAddToPlaylistResponse = {
|
||||
added: number;
|
||||
};
|
||||
|
||||
export type NDAlbum = {
|
||||
albumArtist: string;
|
||||
albumArtistId: string;
|
||||
allArtistIds: string;
|
||||
artist: string;
|
||||
artistId: string;
|
||||
compilation: boolean;
|
||||
coverArtId?: string; // Removed after v0.48.0
|
||||
coverArtPath?: string; // Removed after v0.48.0
|
||||
createdAt: string;
|
||||
duration: number;
|
||||
fullText: string;
|
||||
genre: string;
|
||||
genres: NDGenre[];
|
||||
id: string;
|
||||
maxYear: number;
|
||||
mbzAlbumArtistId: string;
|
||||
mbzAlbumId: string;
|
||||
minYear: number;
|
||||
name: string;
|
||||
orderAlbumArtistName: string;
|
||||
orderAlbumName: string;
|
||||
playCount: number;
|
||||
playDate: string;
|
||||
rating: number;
|
||||
size: number;
|
||||
songCount: number;
|
||||
sortAlbumArtistName: string;
|
||||
sortArtistName: string;
|
||||
starred: boolean;
|
||||
starredAt: string;
|
||||
updatedAt: string;
|
||||
} & { songs?: NDSong[] };
|
||||
|
||||
export type NDAlbumArtist = {
|
||||
albumCount: number;
|
||||
biography: string;
|
||||
externalInfoUpdatedAt: string;
|
||||
externalUrl: string;
|
||||
fullText: string;
|
||||
genres: NDGenre[];
|
||||
id: string;
|
||||
largeImageUrl?: string;
|
||||
mbzArtistId: string;
|
||||
mediumImageUrl?: string;
|
||||
name: string;
|
||||
orderArtistName: string;
|
||||
playCount: number;
|
||||
playDate: string;
|
||||
rating: number;
|
||||
size: number;
|
||||
smallImageUrl?: string;
|
||||
songCount: number;
|
||||
starred: boolean;
|
||||
starredAt: string;
|
||||
} & {
|
||||
similarArtists?: SSArtistInfo['similarArtist'];
|
||||
};
|
||||
|
||||
export type NDAlbumArtistDetail = NDAlbumArtist;
|
||||
|
||||
export type NDAlbumArtistDetailResponse = NDAlbumArtist;
|
||||
|
||||
export type NDAlbumArtistList = {
|
||||
items: NDAlbumArtist[];
|
||||
startIndex: number;
|
||||
totalRecordCount: number;
|
||||
};
|
||||
|
||||
export type NDAlbumArtistListParams = NDOrder &
|
||||
NDPagination & {
|
||||
_sort?: NDAlbumArtistListSort;
|
||||
genre_id?: string;
|
||||
starred?: boolean;
|
||||
};
|
||||
|
||||
export type NDAlbumDetail = NDAlbum & { songs?: NDSongListResponse };
|
||||
|
||||
export type NDAlbumDetailResponse = NDAlbum;
|
||||
|
||||
export type NDAlbumList = {
|
||||
items: NDAlbum[];
|
||||
startIndex: number;
|
||||
totalRecordCount: number;
|
||||
};
|
||||
|
||||
export type NDAlbumListParams = NDOrder &
|
||||
NDPagination & {
|
||||
_sort?: NDAlbumListSort;
|
||||
album_id?: string;
|
||||
artist_id?: string;
|
||||
compilation?: boolean;
|
||||
genre_id?: string;
|
||||
has_rating?: boolean;
|
||||
id?: string;
|
||||
name?: string;
|
||||
recently_played?: boolean;
|
||||
starred?: boolean;
|
||||
year?: number;
|
||||
};
|
||||
|
||||
export type NDAlbumListResponse = NDAlbum[];
|
||||
|
||||
export type NDArtistListResponse = NDAlbumArtist[];
|
||||
|
||||
export type NDAuthenticate = {
|
||||
id: string;
|
||||
isAdmin: boolean;
|
||||
name: string;
|
||||
subsonicSalt: string;
|
||||
subsonicToken: string;
|
||||
token: string;
|
||||
username: string;
|
||||
};
|
||||
|
||||
export type NDAuthenticationResponse = NDAuthenticate;
|
||||
|
||||
export type NDCreatePlaylist = NDCreatePlaylistResponse;
|
||||
|
||||
export type NDCreatePlaylistParams = {
|
||||
comment?: string;
|
||||
name: string;
|
||||
public?: boolean;
|
||||
rules?: null | Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type NDCreatePlaylistResponse = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
export type NDDeletePlaylist = NDDeletePlaylistResponse;
|
||||
|
||||
export type NDDeletePlaylistParams = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
export type NDDeletePlaylistResponse = null;
|
||||
|
||||
export type NDGenre = {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type NDGenreList = NDGenre[];
|
||||
|
||||
export type NDGenreListParams = NDOrder &
|
||||
NDPagination & {
|
||||
_sort?: NDGenreListSort;
|
||||
id?: string;
|
||||
};
|
||||
|
||||
export type NDGenreListResponse = NDGenre[];
|
||||
|
||||
export type NDOrder = {
|
||||
_order?: NDSortOrder;
|
||||
};
|
||||
|
||||
export type NDPagination = {
|
||||
_end?: number;
|
||||
_start?: number;
|
||||
};
|
||||
|
||||
export type NDPlaylist = {
|
||||
comment: string;
|
||||
createdAt: string;
|
||||
duration: number;
|
||||
evaluatedAt: string;
|
||||
id: string;
|
||||
name: string;
|
||||
ownerId: string;
|
||||
ownerName: string;
|
||||
path: string;
|
||||
public: boolean;
|
||||
rules: null | Record<string, unknown>;
|
||||
size: number;
|
||||
songCount: number;
|
||||
sync: boolean;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export type NDPlaylistDetail = NDPlaylist;
|
||||
|
||||
export type NDPlaylistDetailResponse = NDPlaylist;
|
||||
|
||||
export type NDPlaylistList = {
|
||||
items: NDPlaylist[];
|
||||
startIndex: number;
|
||||
totalRecordCount: number;
|
||||
};
|
||||
|
||||
export type NDPlaylistListParams = NDOrder &
|
||||
NDPagination & {
|
||||
_sort?: NDPlaylistListSort;
|
||||
owner_id?: string;
|
||||
};
|
||||
|
||||
export type NDPlaylistListResponse = NDPlaylist[];
|
||||
|
||||
export type NDPlaylistSong = NDSong & {
|
||||
mediaFileId: string;
|
||||
playlistId: string;
|
||||
};
|
||||
|
||||
export type NDPlaylistSongList = {
|
||||
items: NDPlaylistSong[];
|
||||
startIndex: number;
|
||||
totalRecordCount: number;
|
||||
};
|
||||
|
||||
export type NDPlaylistSongListResponse = NDPlaylistSong[];
|
||||
|
||||
export type NDRemoveFromPlaylist = null;
|
||||
|
||||
export type NDRemoveFromPlaylistParams = {
|
||||
id: string[];
|
||||
};
|
||||
|
||||
export type NDRemoveFromPlaylistResponse = {
|
||||
ids: string[];
|
||||
};
|
||||
|
||||
export type NDSong = {
|
||||
album: string;
|
||||
albumArtist: string;
|
||||
albumArtistId: string;
|
||||
albumId: string;
|
||||
artist: string;
|
||||
artistId: string;
|
||||
bitRate: number;
|
||||
bookmarkPosition: number;
|
||||
bpm?: number;
|
||||
channels?: number;
|
||||
comment?: string;
|
||||
compilation: boolean;
|
||||
createdAt: string;
|
||||
discNumber: number;
|
||||
duration: number;
|
||||
fullText: string;
|
||||
genre: string;
|
||||
genres: NDGenre[];
|
||||
hasCoverArt: boolean;
|
||||
id: string;
|
||||
lyrics?: string;
|
||||
mbzAlbumArtistId: string;
|
||||
mbzAlbumId: string;
|
||||
mbzArtistId: string;
|
||||
mbzTrackId: string;
|
||||
orderAlbumArtistName: string;
|
||||
orderAlbumName: string;
|
||||
orderArtistName: string;
|
||||
orderTitle: string;
|
||||
path: string;
|
||||
playCount: number;
|
||||
playDate: string;
|
||||
rating: number;
|
||||
size: number;
|
||||
sortAlbumArtistName: string;
|
||||
sortArtistName: string;
|
||||
starred: boolean;
|
||||
starredAt: string;
|
||||
suffix: string;
|
||||
title: string;
|
||||
trackNumber: number;
|
||||
updatedAt: string;
|
||||
year: number;
|
||||
};
|
||||
|
||||
export type NDSongDetail = NDSong;
|
||||
|
||||
export type NDSongDetailResponse = NDSong;
|
||||
|
||||
export type NDSongList = {
|
||||
items: NDSong[];
|
||||
startIndex: number;
|
||||
totalRecordCount: number;
|
||||
};
|
||||
|
||||
export type NDSongListParams = NDOrder &
|
||||
NDPagination & {
|
||||
_sort?: NDSongListSort;
|
||||
album_id?: string[];
|
||||
artist_id?: string[];
|
||||
genre_id?: string;
|
||||
starred?: boolean;
|
||||
};
|
||||
|
||||
export type NDSongListResponse = NDSong[];
|
||||
|
||||
export type NDUpdatePlaylistParams = Partial<NDPlaylist>;
|
||||
|
||||
export type NDUpdatePlaylistResponse = NDPlaylist;
|
||||
|
||||
export type NDUser = {
|
||||
createdAt: string;
|
||||
email: string;
|
||||
id: string;
|
||||
isAdmin: boolean;
|
||||
lastAccessAt: string;
|
||||
lastLoginAt: string;
|
||||
name: string;
|
||||
updatedAt: string;
|
||||
userName: string;
|
||||
};
|
||||
|
||||
export const NDSongQueryFields = [
|
||||
{ label: 'Album', type: 'string', value: 'album' },
|
||||
{ label: 'Album Artist', type: 'string', value: 'albumartist' },
|
||||
{ label: 'Album Artists', type: 'string', value: 'albumartists' },
|
||||
{ label: 'Album Comment', type: 'string', value: 'albumcomment' },
|
||||
{ label: 'Album Type', type: 'string', value: 'albumtype' },
|
||||
{ label: 'Album Version', type: 'string', value: 'albumversion' },
|
||||
{ label: 'Arranger', type: 'string', value: 'arranger' },
|
||||
{ label: 'Artist', type: 'string', value: 'artist' },
|
||||
{ label: 'Artists', type: 'string', value: 'artists' },
|
||||
{ label: 'Barcode', type: 'string', value: 'barcode' },
|
||||
{ label: 'Bitrate', type: 'number', value: 'bitrate' },
|
||||
{ label: 'BPM', type: 'number', value: 'bpm' },
|
||||
{ label: 'Catalog Number', type: 'string', value: 'catalognumber' },
|
||||
{ label: 'Channels', type: 'number', value: 'channels' },
|
||||
{ label: 'Comment', type: 'string', value: 'comment' },
|
||||
{ label: 'Composer', type: 'string', value: 'composer' },
|
||||
{ label: 'Conductor', type: 'string', value: 'conductor' },
|
||||
{ label: 'Copyright', type: 'string', value: 'copyright' },
|
||||
{ label: 'Date Added', type: 'date', value: 'dateadded' },
|
||||
{ label: 'Date Favorited', type: 'date', value: 'dateloved' },
|
||||
{ label: 'Date Last Played', type: 'date', value: 'lastplayed' },
|
||||
{ label: 'Date Modified', type: 'date', value: 'datemodified' },
|
||||
{ label: 'DJ Mixer', type: 'string', value: 'djmixer' },
|
||||
{ label: 'Director', type: 'string', value: 'director' },
|
||||
{ label: 'Disc Number', type: 'number', value: 'discnumber' },
|
||||
{ label: 'Disc Subtitle', type: 'string', value: 'discsubtitle' },
|
||||
{ label: 'Disc Total', type: 'number', value: 'disctotal' },
|
||||
{ label: 'Duration', type: 'number', value: 'duration' },
|
||||
{ label: 'Encoded By', type: 'string', value: 'encodedby' },
|
||||
{ label: 'Encoder Settings', type: 'string', value: 'encodersettings' },
|
||||
{ label: 'Engineer', type: 'string', value: 'engineer' },
|
||||
{ label: 'Explicit Status', type: 'string', value: 'explicitstatus' },
|
||||
{ label: 'File Path', type: 'string', value: 'filepath' },
|
||||
{ label: 'File Type', type: 'string', value: 'filetype' },
|
||||
{ label: 'Genre', type: 'string', value: 'genre' },
|
||||
{ label: 'Grouping', type: 'string', value: 'grouping' },
|
||||
{ label: 'Has CoverArt', type: 'boolean', value: 'hascoverart' },
|
||||
{ label: 'Is Compilation', type: 'boolean', value: 'compilation' },
|
||||
{ label: 'Is Favorite', type: 'boolean', value: 'loved' },
|
||||
{ label: 'ISRC', type: 'string', value: 'isrc' },
|
||||
{ label: 'Key', type: 'string', value: 'key' },
|
||||
{ label: 'Language', type: 'string', value: 'language' },
|
||||
{ label: 'License', type: 'string', value: 'license' },
|
||||
{ label: 'Lyricist', type: 'string', value: 'lyricist' },
|
||||
{ label: 'Lyrics', type: 'string', value: 'lyrics' },
|
||||
{ label: 'Media', type: 'string', value: 'media' },
|
||||
{ label: 'Mixer', type: 'string', value: 'mixer' },
|
||||
{ label: 'Mood', type: 'string', value: 'mood' },
|
||||
{ label: 'Movement', type: 'string', value: 'movement' },
|
||||
{ label: 'Movement Name', type: 'string', value: 'movementname' },
|
||||
{ label: 'Movement Total', type: 'number', value: 'movementtotal' },
|
||||
{ label: 'MusicBrainz Artist Id', type: 'string', value: 'musicbrainz_albumartistid' },
|
||||
{ label: 'MusicBrainz Album Artist Id', type: 'string', value: 'musicbrainz_albumartistid' },
|
||||
{ label: 'MusicBrainz Album Id', type: 'string', value: 'musicbrainz_albumid' },
|
||||
{ label: 'MusicBrainz Disc Id', type: 'string', value: 'musicbrainz_discid' },
|
||||
{ label: 'MusicBrainz Recording Id', type: 'string', value: 'musicbrainz_recordingid' },
|
||||
{ label: 'MusicBrainz Release Group Id', type: 'string', value: 'musicbrainz_releasegroupid' },
|
||||
{ label: 'MusicBrainz Track Id', type: 'string', value: 'musicbrainz_trackid' },
|
||||
{ label: 'MusicBrainz Work Id', type: 'string', value: 'musicbrainz_workid' },
|
||||
{ label: 'Name', type: 'string', value: 'title' },
|
||||
{ label: 'Original Date', type: 'date', value: 'originaldate' },
|
||||
{ label: 'Performer', type: 'string', value: 'performer' },
|
||||
{ label: 'Play Count', type: 'number', value: 'playcount' },
|
||||
{ label: 'Playlist', type: 'playlist', value: 'id' },
|
||||
{ label: 'Producer', type: 'string', value: 'producer' },
|
||||
{ label: 'R128 Album Gain', type: 'number', value: 'r128_album_gain' },
|
||||
{ label: 'R128 Track Gain', type: 'number', value: 'r128_track_gain' },
|
||||
{ label: 'Rating', type: 'number', value: 'rating' },
|
||||
{ label: 'Record Label', type: 'string', value: 'recordlabel' },
|
||||
{ label: 'Recording Date', type: 'date', value: 'recordingdate' },
|
||||
{ label: 'Release Country', type: 'string', value: 'releasecountry' },
|
||||
{ label: 'Release Date', type: 'date', value: 'releasedate' },
|
||||
{ label: 'Release Status', type: 'string', value: 'releasestatus' },
|
||||
{ label: 'Release Type', type: 'string', value: 'releasetype' },
|
||||
{ label: 'ReplayGain Album Gain', type: 'number', value: 'replaygain_album_gain' },
|
||||
{ label: 'ReplayGain Album Peak', type: 'number', value: 'replaygain_album_peak' },
|
||||
{ label: 'ReplayGain Track Gain', type: 'number', value: 'replaygain_track_gain' },
|
||||
{ label: 'ReplayGain Track Peak', type: 'number', value: 'replaygain_track_peak' },
|
||||
{ label: 'Remixer', type: 'string', value: 'remixer' },
|
||||
{ label: 'Script', type: 'string', value: 'script' },
|
||||
{ label: 'Size', type: 'number', value: 'size' },
|
||||
{ label: 'Sort Album', type: 'string', value: 'albumsort' },
|
||||
{ label: 'Sort Album Artist', type: 'string', value: 'albumartistsort' },
|
||||
{ label: 'Sort Album Artists', type: 'string', value: 'albumartistssort' },
|
||||
{ label: 'Sort Artist', type: 'string', value: 'artistsort' },
|
||||
{ label: 'Sort Artists', type: 'string', value: 'artistssort' },
|
||||
{ label: 'Sort Composer', type: 'string', value: 'composersort' },
|
||||
{ label: 'Sort Lyricist', type: 'string', value: 'lyricistsort' },
|
||||
{ label: 'Sort Name', type: 'string', value: 'titlesort' },
|
||||
{ label: 'Subtitle', type: 'string', value: 'subtitle' },
|
||||
{ label: 'Track Number', type: 'number', value: 'track' },
|
||||
{ label: 'Track Total', type: 'number', value: 'tracktotal' },
|
||||
{ label: 'Year', type: 'number', value: 'year' },
|
||||
{ label: 'Website', type: 'string', value: 'website' },
|
||||
{ label: 'Work', type: 'string', value: 'work' },
|
||||
];
|
||||
|
||||
export const NDSongQueryPlaylistOperators = [
|
||||
{ label: 'is in', value: 'inPlaylist' },
|
||||
{ label: 'is not in', value: 'notInPlaylist' },
|
||||
];
|
||||
|
||||
export const NDSongQueryDateOperators = [
|
||||
{ label: 'is', value: 'is' },
|
||||
{ label: 'is not', value: 'isNot' },
|
||||
{ label: 'is before', value: 'before' },
|
||||
{ label: 'is after', value: 'after' },
|
||||
{ label: 'is in the last', value: 'inTheLast' },
|
||||
{ label: 'is not in the last', value: 'notInTheLast' },
|
||||
{ label: 'is in the range', value: 'inTheRange' },
|
||||
];
|
||||
|
||||
export const NDSongQueryStringOperators = [
|
||||
{ label: 'is', value: 'is' },
|
||||
{ label: 'is not', value: 'isNot' },
|
||||
{ label: 'contains', value: 'contains' },
|
||||
{ label: 'does not contain', value: 'notContains' },
|
||||
{ label: 'starts with', value: 'startsWith' },
|
||||
{ label: 'ends with', value: 'endsWith' },
|
||||
];
|
||||
|
||||
export const NDSongQueryBooleanOperators = [
|
||||
{ label: 'is', value: 'is' },
|
||||
{ label: 'is not', value: 'isNot' },
|
||||
];
|
||||
|
||||
export const NDSongQueryNumberOperators = [
|
||||
{ label: 'is', value: 'is' },
|
||||
{ label: 'is not', value: 'isNot' },
|
||||
{ label: 'contains', value: 'contains' },
|
||||
{ label: 'does not contain', value: 'notContains' },
|
||||
{ label: 'is greater than', value: 'gt' },
|
||||
{ label: 'is less than', value: 'lt' },
|
||||
{ label: 'is in the range', value: 'inTheRange' },
|
||||
];
|
||||
|
||||
export enum NDUserListSort {
|
||||
NAME = 'name',
|
||||
}
|
||||
|
||||
export type NDUserList = {
|
||||
items: NDUser[];
|
||||
startIndex: number;
|
||||
totalRecordCount: number;
|
||||
};
|
||||
|
||||
export type NDUserListParams = NDOrder &
|
||||
NDPagination & {
|
||||
_sort?: NDUserListSort;
|
||||
};
|
||||
|
||||
export type NDUserListResponse = NDUser[];
|
||||
398
src/shared/api/navidrome/navidrome-normalize.ts
Normal file
398
src/shared/api/navidrome/navidrome-normalize.ts
Normal file
|
|
@ -0,0 +1,398 @@
|
|||
import { nanoid } from 'nanoid';
|
||||
import z from 'zod';
|
||||
|
||||
import { NDGenre } from '/@/shared/api/navidrome.types';
|
||||
import { ndType } from '/@/shared/api/navidrome/navidrome-types';
|
||||
import { ssType } from '/@/shared/api/subsonic/subsonic-types';
|
||||
import {
|
||||
Album,
|
||||
AlbumArtist,
|
||||
Genre,
|
||||
LibraryItem,
|
||||
Playlist,
|
||||
RelatedArtist,
|
||||
Song,
|
||||
User,
|
||||
} from '/@/shared/types/domain-types';
|
||||
import { ServerListItem, ServerType } from '/@/shared/types/types';
|
||||
|
||||
const getImageUrl = (args: { url: null | string }) => {
|
||||
const { url } = args;
|
||||
if (url === '/app/artist-placeholder.webp') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return url;
|
||||
};
|
||||
|
||||
const getCoverArtUrl = (args: {
|
||||
baseUrl: string | undefined;
|
||||
coverArtId: string;
|
||||
credential: string | undefined;
|
||||
size: number;
|
||||
}) => {
|
||||
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}`
|
||||
);
|
||||
};
|
||||
|
||||
interface WithDate {
|
||||
playDate?: string;
|
||||
}
|
||||
|
||||
const normalizePlayDate = (item: WithDate): null | string => {
|
||||
return !item.playDate || item.playDate.includes('0001-') ? null : item.playDate;
|
||||
};
|
||||
|
||||
const getArtists = (
|
||||
item:
|
||||
| z.infer<typeof ndType._response.album>
|
||||
| z.infer<typeof ndType._response.playlistSong>
|
||||
| z.infer<typeof ndType._response.song>,
|
||||
) => {
|
||||
let albumArtists: RelatedArtist[] | undefined;
|
||||
let artists: RelatedArtist[] | undefined;
|
||||
let participants: null | Record<string, RelatedArtist[]> = null;
|
||||
|
||||
if (item.participants) {
|
||||
participants = {};
|
||||
for (const [role, list] of Object.entries(item.participants)) {
|
||||
if (role === 'albumartist' || role === 'artist') {
|
||||
const roleList = list.map((item) => ({
|
||||
id: item.id,
|
||||
imageUrl: null,
|
||||
name: item.name,
|
||||
}));
|
||||
|
||||
if (role === 'albumartist') {
|
||||
albumArtists = roleList;
|
||||
} else {
|
||||
artists = roleList;
|
||||
}
|
||||
} else {
|
||||
const subRoles = new Map<string | undefined, RelatedArtist[]>();
|
||||
|
||||
for (const artist of list) {
|
||||
const item: RelatedArtist = {
|
||||
id: artist.id,
|
||||
imageUrl: null,
|
||||
name: artist.name,
|
||||
};
|
||||
|
||||
if (subRoles.has(artist.subRole)) {
|
||||
subRoles.get(artist.subRole)!.push(item);
|
||||
} else {
|
||||
subRoles.set(artist.subRole, [item]);
|
||||
}
|
||||
}
|
||||
|
||||
for (const [subRole, items] of subRoles.entries()) {
|
||||
if (subRole) {
|
||||
participants[`${role} (${subRole})`] = items;
|
||||
} else {
|
||||
participants[role] = items;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (albumArtists === undefined) {
|
||||
albumArtists = [{ id: item.albumArtistId, imageUrl: null, name: item.albumArtist }];
|
||||
}
|
||||
|
||||
if (artists === undefined) {
|
||||
artists = [{ id: item.artistId, imageUrl: null, name: item.artist }];
|
||||
}
|
||||
|
||||
return { albumArtists, artists, participants };
|
||||
};
|
||||
|
||||
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;
|
||||
|
||||
// Dynamically determine the id field based on whether or not the item is a playlist song
|
||||
if ('mediaFileId' in item) {
|
||||
id = item.mediaFileId;
|
||||
playlistItemId = item.id;
|
||||
} else {
|
||||
id = item.id;
|
||||
}
|
||||
|
||||
const imageUrl = getCoverArtUrl({
|
||||
baseUrl: server?.url,
|
||||
coverArtId: id,
|
||||
credential: server?.credential,
|
||||
size: imageSize || 100,
|
||||
});
|
||||
|
||||
const imagePlaceholderUrl = null;
|
||||
return {
|
||||
album: item.album,
|
||||
albumId: item.albumId,
|
||||
...getArtists(item),
|
||||
artistName: item.artist,
|
||||
bitRate: item.bitRate,
|
||||
bpm: item.bpm ? item.bpm : null,
|
||||
channels: item.channels ? item.channels : null,
|
||||
comment: item.comment ? item.comment : null,
|
||||
compilation: item.compilation,
|
||||
container: item.suffix,
|
||||
createdAt: item.createdAt.split('T')[0],
|
||||
discNumber: item.discNumber,
|
||||
discSubtitle: item.discSubtitle ? item.discSubtitle : null,
|
||||
duration: item.duration * 1000,
|
||||
gain:
|
||||
item.rgAlbumGain || item.rgTrackGain
|
||||
? { album: item.rgAlbumGain, track: item.rgTrackGain }
|
||||
: null,
|
||||
genres: (item.genres || []).map((genre) => ({
|
||||
id: genre.id,
|
||||
imageUrl: null,
|
||||
itemType: LibraryItem.GENRE,
|
||||
name: genre.name,
|
||||
})),
|
||||
id,
|
||||
imagePlaceholderUrl,
|
||||
imageUrl,
|
||||
itemType: LibraryItem.SONG,
|
||||
lastPlayedAt: normalizePlayDate(item),
|
||||
lyrics: item.lyrics ? item.lyrics : null,
|
||||
name: item.title,
|
||||
path: item.path,
|
||||
peak:
|
||||
item.rgAlbumPeak || item.rgTrackPeak
|
||||
? { album: item.rgAlbumPeak, track: item.rgTrackPeak }
|
||||
: null,
|
||||
playCount: item.playCount || 0,
|
||||
playlistItemId,
|
||||
releaseDate: (item.releaseDate
|
||||
? new Date(item.releaseDate)
|
||||
: new Date(Date.UTC(item.year, 0, 1))
|
||||
).toISOString(),
|
||||
releaseYear: String(item.year),
|
||||
serverId: server?.id || 'unknown',
|
||||
serverType: ServerType.NAVIDROME,
|
||||
size: item.size,
|
||||
streamUrl: `${server?.url}/rest/stream.view?id=${id}&v=1.13.0&c=Feishin&${server?.credential}`,
|
||||
tags: item.tags || null,
|
||||
trackNumber: item.trackNumber,
|
||||
uniqueId: nanoid(),
|
||||
updatedAt: item.updatedAt,
|
||||
userFavorite: item.starred || false,
|
||||
userRating: item.rating || null,
|
||||
};
|
||||
};
|
||||
|
||||
const normalizeAlbum = (
|
||||
item: z.infer<typeof ndType._response.album> & {
|
||||
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,
|
||||
});
|
||||
|
||||
const imagePlaceholderUrl = null;
|
||||
|
||||
const imageBackdropUrl = imageUrl?.replace(/size=\d+/, 'size=1000') || null;
|
||||
|
||||
return {
|
||||
albumArtist: item.albumArtist,
|
||||
...getArtists(item),
|
||||
backdropImageUrl: imageBackdropUrl,
|
||||
comment: item.comment || null,
|
||||
createdAt: item.createdAt.split('T')[0],
|
||||
duration: item.duration !== undefined ? item.duration * 1000 : null,
|
||||
genres: (item.genres || []).map((genre) => ({
|
||||
id: genre.id,
|
||||
imageUrl: null,
|
||||
itemType: LibraryItem.GENRE,
|
||||
name: genre.name,
|
||||
})),
|
||||
id: item.id,
|
||||
imagePlaceholderUrl,
|
||||
imageUrl,
|
||||
isCompilation: item.compilation,
|
||||
itemType: LibraryItem.ALBUM,
|
||||
lastPlayedAt: normalizePlayDate(item),
|
||||
|
||||
mbzId: item.mbzAlbumId || null,
|
||||
name: item.name,
|
||||
originalDate: item.originalDate
|
||||
? new Date(item.originalDate).toISOString()
|
||||
: item.originalYear
|
||||
? new Date(Date.UTC(item.originalYear, 0, 1)).toISOString()
|
||||
: null,
|
||||
playCount: item.playCount || 0,
|
||||
releaseDate: (item.releaseDate
|
||||
? new Date(item.releaseDate)
|
||||
: new Date(Date.UTC(item.minYear, 0, 1))
|
||||
).toISOString(),
|
||||
releaseYear: item.minYear,
|
||||
serverId: server?.id || 'unknown',
|
||||
serverType: ServerType.NAVIDROME,
|
||||
size: item.size,
|
||||
songCount: item.songCount,
|
||||
songs: item.songs ? item.songs.map((song) => normalizeSong(song, server)) : undefined,
|
||||
tags: item.tags || null,
|
||||
uniqueId: nanoid(),
|
||||
updatedAt: item.updatedAt,
|
||||
userFavorite: item.starred,
|
||||
userRating: item.rating || null,
|
||||
};
|
||||
};
|
||||
|
||||
const normalizeAlbumArtist = (
|
||||
item: z.infer<typeof ndType._response.albumArtist> & {
|
||||
similarArtists?: z.infer<typeof ssType._response.artistInfo>['artistInfo']['similarArtist'];
|
||||
},
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
let albumCount: number;
|
||||
let songCount: number;
|
||||
|
||||
if (item.stats) {
|
||||
albumCount = Math.max(
|
||||
item.stats.albumartist?.albumCount ?? 0,
|
||||
item.stats.artist?.albumCount ?? 0,
|
||||
);
|
||||
songCount = Math.max(
|
||||
item.stats.albumartist?.songCount ?? 0,
|
||||
item.stats.artist?.songCount ?? 0,
|
||||
);
|
||||
} else {
|
||||
albumCount = item.albumCount;
|
||||
songCount = item.songCount;
|
||||
}
|
||||
|
||||
return {
|
||||
albumCount,
|
||||
backgroundImageUrl: null,
|
||||
biography: item.biography || null,
|
||||
duration: null,
|
||||
genres: (item.genres || []).map((genre) => ({
|
||||
id: genre.id,
|
||||
imageUrl: null,
|
||||
itemType: LibraryItem.GENRE,
|
||||
name: genre.name,
|
||||
})),
|
||||
id: item.id,
|
||||
imageUrl: imageUrl || null,
|
||||
itemType: LibraryItem.ALBUM_ARTIST,
|
||||
lastPlayedAt: normalizePlayDate(item),
|
||||
mbz: item.mbzArtistId || null,
|
||||
name: item.name,
|
||||
playCount: item.playCount || 0,
|
||||
serverId: server?.id || 'unknown',
|
||||
serverType: ServerType.NAVIDROME,
|
||||
similarArtists:
|
||||
item.similarArtists?.map((artist) => ({
|
||||
id: artist.id,
|
||||
imageUrl: artist?.artistImageUrl || null,
|
||||
name: artist.name,
|
||||
})) || null,
|
||||
songCount,
|
||||
userFavorite: item.starred,
|
||||
userRating: item.rating,
|
||||
};
|
||||
};
|
||||
|
||||
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,
|
||||
});
|
||||
|
||||
const imagePlaceholderUrl = null;
|
||||
|
||||
return {
|
||||
description: item.comment,
|
||||
duration: item.duration * 1000,
|
||||
genres: [],
|
||||
id: item.id,
|
||||
imagePlaceholderUrl,
|
||||
imageUrl,
|
||||
itemType: LibraryItem.PLAYLIST,
|
||||
name: item.name,
|
||||
owner: item.ownerName,
|
||||
ownerId: item.ownerId,
|
||||
public: item.public,
|
||||
rules: item?.rules || null,
|
||||
serverId: server?.id || 'unknown',
|
||||
serverType: ServerType.NAVIDROME,
|
||||
size: item.size,
|
||||
songCount: item.songCount,
|
||||
sync: item.sync,
|
||||
};
|
||||
};
|
||||
|
||||
const normalizeGenre = (item: NDGenre): Genre => {
|
||||
return {
|
||||
albumCount: undefined,
|
||||
id: item.id,
|
||||
imageUrl: null,
|
||||
itemType: LibraryItem.GENRE,
|
||||
name: item.name,
|
||||
songCount: undefined,
|
||||
};
|
||||
};
|
||||
|
||||
const normalizeUser = (item: z.infer<typeof ndType._response.user>): User => {
|
||||
return {
|
||||
createdAt: item.createdAt,
|
||||
email: item.email || null,
|
||||
id: item.id,
|
||||
isAdmin: item.isAdmin,
|
||||
lastLoginAt: item.lastLoginAt,
|
||||
name: item.userName,
|
||||
updatedAt: item.updatedAt,
|
||||
};
|
||||
};
|
||||
|
||||
export const ndNormalize = {
|
||||
album: normalizeAlbum,
|
||||
albumArtist: normalizeAlbumArtist,
|
||||
genre: normalizeGenre,
|
||||
playlist: normalizePlaylist,
|
||||
song: normalizeSong,
|
||||
user: normalizeUser,
|
||||
};
|
||||
402
src/shared/api/navidrome/navidrome-types.ts
Normal file
402
src/shared/api/navidrome/navidrome-types.ts
Normal file
|
|
@ -0,0 +1,402 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
import {
|
||||
NDAlbumArtistListSort,
|
||||
NDAlbumListSort,
|
||||
NDPlaylistListSort,
|
||||
NDSongListSort,
|
||||
} from '/@/shared/api/navidrome.types';
|
||||
|
||||
const sortOrderValues = ['ASC', 'DESC'] as const;
|
||||
|
||||
const error = z.string();
|
||||
|
||||
const paginationParameters = z.object({
|
||||
_end: z.number().optional(),
|
||||
_order: z.enum(sortOrderValues),
|
||||
_start: z.number().optional(),
|
||||
});
|
||||
|
||||
const authenticate = z.object({
|
||||
id: z.string(),
|
||||
isAdmin: z.boolean(),
|
||||
name: z.string(),
|
||||
subsonicSalt: z.string(),
|
||||
subsonicToken: z.string(),
|
||||
token: z.string(),
|
||||
username: z.string(),
|
||||
});
|
||||
|
||||
const authenticateParameters = z.object({
|
||||
password: z.string(),
|
||||
username: z.string(),
|
||||
});
|
||||
|
||||
const user = z.object({
|
||||
createdAt: z.string(),
|
||||
email: z.string().optional(),
|
||||
id: z.string(),
|
||||
isAdmin: z.boolean(),
|
||||
lastAccessAt: z.string(),
|
||||
lastLoginAt: z.string(),
|
||||
name: z.string(),
|
||||
updatedAt: z.string(),
|
||||
userName: z.string(),
|
||||
});
|
||||
|
||||
const userList = z.array(user);
|
||||
|
||||
const ndUserListSort = {
|
||||
NAME: 'name',
|
||||
} as const;
|
||||
|
||||
const userListParameters = paginationParameters.extend({
|
||||
_sort: z.nativeEnum(ndUserListSort).optional(),
|
||||
});
|
||||
|
||||
const genre = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
});
|
||||
|
||||
const genreListSort = {
|
||||
NAME: 'name',
|
||||
SONG_COUNT: 'songCount',
|
||||
} as const;
|
||||
|
||||
const genreListParameters = paginationParameters.extend({
|
||||
_sort: z.nativeEnum(genreListSort).optional(),
|
||||
name: z.string().optional(),
|
||||
});
|
||||
|
||||
const genreList = z.array(genre);
|
||||
|
||||
const stats = z.object({
|
||||
albumCount: z.number(),
|
||||
size: z.number(),
|
||||
songCount: z.number(),
|
||||
});
|
||||
|
||||
const albumArtist = z.object({
|
||||
albumCount: z.number(),
|
||||
biography: z.string(),
|
||||
externalInfoUpdatedAt: z.string(),
|
||||
externalUrl: z.string(),
|
||||
fullText: z.string(),
|
||||
genres: z.array(genre).nullable(),
|
||||
id: z.string(),
|
||||
largeImageUrl: z.string().optional(),
|
||||
mbzArtistId: z.string().optional(),
|
||||
mediumImageUrl: z.string().optional(),
|
||||
name: z.string(),
|
||||
orderArtistName: z.string(),
|
||||
playCount: z.number().optional(),
|
||||
playDate: z.string().optional(),
|
||||
rating: z.number(),
|
||||
size: z.number(),
|
||||
smallImageUrl: z.string().optional(),
|
||||
songCount: z.number(),
|
||||
starred: z.boolean(),
|
||||
starredAt: z.string(),
|
||||
stats: z.record(z.string(), stats).optional(),
|
||||
});
|
||||
|
||||
const albumArtistList = z.array(albumArtist);
|
||||
|
||||
const albumArtistListParameters = paginationParameters.extend({
|
||||
_sort: z.nativeEnum(NDAlbumArtistListSort).optional(),
|
||||
genre_id: z.string().optional(),
|
||||
missing: z.boolean().optional(),
|
||||
name: z.string().optional(),
|
||||
role: z.string().optional(),
|
||||
starred: z.boolean().optional(),
|
||||
});
|
||||
|
||||
const participant = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
subRole: z.string().optional(),
|
||||
});
|
||||
|
||||
const participants = z.record(z.string(), z.array(participant));
|
||||
|
||||
const album = z.object({
|
||||
albumArtist: z.string(),
|
||||
albumArtistId: z.string(),
|
||||
allArtistIds: z.string(),
|
||||
artist: z.string(),
|
||||
artistId: z.string(),
|
||||
comment: z.string().optional(),
|
||||
compilation: z.boolean(),
|
||||
coverArtId: z.string().optional(), // Removed after v0.48.0
|
||||
coverArtPath: z.string().optional(), // Removed after v0.48.0
|
||||
createdAt: z.string(),
|
||||
duration: z.number().optional(),
|
||||
fullText: z.string(),
|
||||
genre: z.string(),
|
||||
genres: z.array(genre).nullable(),
|
||||
id: z.string(),
|
||||
maxYear: z.number(),
|
||||
mbzAlbumArtistId: z.string().optional(),
|
||||
mbzAlbumId: z.string().optional(),
|
||||
minYear: z.number(),
|
||||
name: z.string(),
|
||||
orderAlbumArtistName: z.string(),
|
||||
orderAlbumName: z.string(),
|
||||
originalDate: z.string().optional(),
|
||||
originalYear: z.number().optional(),
|
||||
participants: z.optional(participants),
|
||||
playCount: z.number().optional(),
|
||||
playDate: z.string().optional(),
|
||||
rating: z.number().optional(),
|
||||
releaseDate: z.string().optional(),
|
||||
size: z.number(),
|
||||
songCount: z.number(),
|
||||
sortAlbumArtistName: z.string(),
|
||||
sortArtistName: z.string(),
|
||||
starred: z.boolean(),
|
||||
starredAt: z.string().optional(),
|
||||
tags: z.record(z.string(), z.array(z.string())).optional(),
|
||||
updatedAt: z.string(),
|
||||
});
|
||||
|
||||
const albumList = z.array(album);
|
||||
|
||||
const albumListParameters = paginationParameters.extend({
|
||||
_sort: z.nativeEnum(NDAlbumListSort).optional(),
|
||||
album_id: z.string().optional(),
|
||||
artist_id: z.string().optional(),
|
||||
compilation: z.boolean().optional(),
|
||||
genre_id: z.string().optional(),
|
||||
has_rating: z.boolean().optional(),
|
||||
id: z.string().optional(),
|
||||
name: z.string().optional(),
|
||||
recently_added: z.boolean().optional(),
|
||||
recently_played: z.boolean().optional(),
|
||||
starred: z.boolean().optional(),
|
||||
year: z.number().optional(),
|
||||
});
|
||||
|
||||
const song = z.object({
|
||||
album: z.string(),
|
||||
albumArtist: z.string(),
|
||||
albumArtistId: z.string(),
|
||||
albumId: z.string(),
|
||||
artist: z.string(),
|
||||
artistId: z.string(),
|
||||
bitRate: z.number(),
|
||||
bookmarkPosition: z.number(),
|
||||
bpm: z.number().optional(),
|
||||
catalogNum: z.string().optional(),
|
||||
channels: z.number().optional(),
|
||||
comment: z.string().optional(),
|
||||
compilation: z.boolean(),
|
||||
createdAt: z.string(),
|
||||
discNumber: z.number(),
|
||||
discSubtitle: z.string().optional(),
|
||||
duration: z.number(),
|
||||
embedArtPath: z.string().optional(),
|
||||
externalInfoUpdatedAt: z.string().optional(),
|
||||
externalUrl: z.string().optional(),
|
||||
fullText: z.string(),
|
||||
genre: z.string(),
|
||||
genres: z.array(genre).nullable(),
|
||||
hasCoverArt: z.boolean(),
|
||||
id: z.string(),
|
||||
imageFiles: z.string().optional(),
|
||||
largeImageUrl: z.string().optional(),
|
||||
lyrics: z.string().optional(),
|
||||
mbzAlbumArtistId: z.string().optional(),
|
||||
mbzAlbumId: z.string().optional(),
|
||||
mbzArtistId: z.string().optional(),
|
||||
mbzTrackId: z.string().optional(),
|
||||
mediumImageUrl: z.string().optional(),
|
||||
orderAlbumArtistName: z.string(),
|
||||
orderAlbumName: z.string(),
|
||||
orderArtistName: z.string(),
|
||||
orderTitle: z.string(),
|
||||
participants: z.optional(participants),
|
||||
path: z.string(),
|
||||
playCount: z.number().optional(),
|
||||
playDate: z.string().optional(),
|
||||
rating: z.number().optional(),
|
||||
releaseDate: z.string().optional(),
|
||||
rgAlbumGain: z.number().optional(),
|
||||
rgAlbumPeak: z.number().optional(),
|
||||
rgTrackGain: z.number().optional(),
|
||||
rgTrackPeak: z.number().optional(),
|
||||
size: z.number(),
|
||||
smallImageUrl: z.string().optional(),
|
||||
sortAlbumArtistName: z.string(),
|
||||
sortArtistName: z.string(),
|
||||
starred: z.boolean(),
|
||||
starredAt: z.string().optional(),
|
||||
suffix: z.string(),
|
||||
tags: z.record(z.string(), z.array(z.string())).optional(),
|
||||
title: z.string(),
|
||||
trackNumber: z.number(),
|
||||
updatedAt: z.string(),
|
||||
year: z.number(),
|
||||
});
|
||||
|
||||
const songList = z.array(song);
|
||||
|
||||
const songListParameters = paginationParameters.extend({
|
||||
_sort: z.nativeEnum(NDSongListSort).optional(),
|
||||
album_artist_id: z.array(z.string()).optional(),
|
||||
album_id: z.array(z.string()).optional(),
|
||||
artist_id: z.array(z.string()).optional(),
|
||||
genre_id: z.array(z.string()).optional(),
|
||||
path: z.string().optional(),
|
||||
starred: z.boolean().optional(),
|
||||
title: z.string().optional(),
|
||||
year: z.number().optional(),
|
||||
});
|
||||
|
||||
const playlist = z.object({
|
||||
comment: z.string(),
|
||||
createdAt: z.string(),
|
||||
duration: z.number(),
|
||||
evaluatedAt: z.string(),
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
ownerId: z.string(),
|
||||
ownerName: z.string(),
|
||||
path: z.string(),
|
||||
public: z.boolean(),
|
||||
rules: z.record(z.string(), z.any()),
|
||||
size: z.number(),
|
||||
songCount: z.number(),
|
||||
sync: z.boolean(),
|
||||
updatedAt: z.string(),
|
||||
});
|
||||
|
||||
const playlistList = z.array(playlist);
|
||||
|
||||
const playlistListParameters = paginationParameters.extend({
|
||||
_sort: z.nativeEnum(NDPlaylistListSort).optional(),
|
||||
owner_id: z.string().optional(),
|
||||
q: z.string().optional(),
|
||||
smart: z.boolean().optional(),
|
||||
});
|
||||
|
||||
const playlistSong = song.extend({
|
||||
mediaFileId: z.string(),
|
||||
playlistId: z.string(),
|
||||
});
|
||||
|
||||
const playlistSongList = z.array(playlistSong);
|
||||
|
||||
const createPlaylist = playlist.pick({
|
||||
id: true,
|
||||
});
|
||||
|
||||
const createPlaylistParameters = z.object({
|
||||
comment: z.string().optional(),
|
||||
name: z.string(),
|
||||
public: z.boolean().optional(),
|
||||
rules: z.record(z.any()).optional(),
|
||||
sync: z.boolean().optional(),
|
||||
});
|
||||
|
||||
const updatePlaylist = playlist;
|
||||
|
||||
const updatePlaylistParameters = createPlaylistParameters.partial();
|
||||
|
||||
const deletePlaylist = z.null();
|
||||
|
||||
const addToPlaylist = z.object({
|
||||
added: z.number(),
|
||||
});
|
||||
|
||||
const addToPlaylistParameters = z.object({
|
||||
ids: z.array(z.string()),
|
||||
});
|
||||
|
||||
const removeFromPlaylist = z.object({
|
||||
ids: z.array(z.string()),
|
||||
});
|
||||
|
||||
const removeFromPlaylistParameters = z.object({
|
||||
id: z.array(z.string()),
|
||||
});
|
||||
|
||||
const shareItem = z.object({
|
||||
id: z.string(),
|
||||
});
|
||||
|
||||
const shareItemParameters = z.object({
|
||||
description: z.string(),
|
||||
downloadable: z.boolean(),
|
||||
expires: z.number(),
|
||||
resourceIds: z.string(),
|
||||
resourceType: z.string(),
|
||||
});
|
||||
|
||||
const moveItemParameters = z.object({
|
||||
insert_before: z.string(),
|
||||
});
|
||||
|
||||
const moveItem = z.null();
|
||||
|
||||
const tag = z.object({
|
||||
albumCount: z.number().optional(),
|
||||
id: z.string(),
|
||||
songCount: z.number().optional(),
|
||||
tagName: z.string(),
|
||||
tagValue: z.string(),
|
||||
});
|
||||
|
||||
const tags = z.array(tag);
|
||||
|
||||
export const ndType = {
|
||||
_enum: {
|
||||
albumArtistList: NDAlbumArtistListSort,
|
||||
albumList: NDAlbumListSort,
|
||||
genreList: genreListSort,
|
||||
playlistList: NDPlaylistListSort,
|
||||
songList: NDSongListSort,
|
||||
userList: ndUserListSort,
|
||||
},
|
||||
_parameters: {
|
||||
addToPlaylist: addToPlaylistParameters,
|
||||
albumArtistList: albumArtistListParameters,
|
||||
albumList: albumListParameters,
|
||||
authenticate: authenticateParameters,
|
||||
createPlaylist: createPlaylistParameters,
|
||||
genreList: genreListParameters,
|
||||
moveItem: moveItemParameters,
|
||||
playlistList: playlistListParameters,
|
||||
removeFromPlaylist: removeFromPlaylistParameters,
|
||||
shareItem: shareItemParameters,
|
||||
songList: songListParameters,
|
||||
updatePlaylist: updatePlaylistParameters,
|
||||
userList: userListParameters,
|
||||
},
|
||||
_response: {
|
||||
addToPlaylist,
|
||||
album,
|
||||
albumArtist,
|
||||
albumArtistList,
|
||||
albumList,
|
||||
authenticate,
|
||||
createPlaylist,
|
||||
deletePlaylist,
|
||||
error,
|
||||
genre,
|
||||
genreList,
|
||||
moveItem,
|
||||
playlist,
|
||||
playlistList,
|
||||
playlistSong,
|
||||
playlistSongList,
|
||||
removeFromPlaylist,
|
||||
shareItem,
|
||||
song,
|
||||
songList,
|
||||
tags,
|
||||
updatePlaylist,
|
||||
user,
|
||||
userList,
|
||||
},
|
||||
};
|
||||
224
src/shared/api/subsonic.types.ts
Normal file
224
src/shared/api/subsonic.types.ts
Normal file
|
|
@ -0,0 +1,224 @@
|
|||
export type SSAlbum = SSAlbumListEntry & {
|
||||
song: SSSong[];
|
||||
};
|
||||
|
||||
export type SSAlbumArtistDetail = SSAlbumArtistListEntry & { album: SSAlbumListEntry[] };
|
||||
|
||||
export type SSAlbumArtistDetailParams = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
export type SSAlbumArtistDetailResponse = {
|
||||
artist: SSAlbumArtistListEntry & {
|
||||
album: SSAlbumListEntry[];
|
||||
};
|
||||
};
|
||||
|
||||
export type SSAlbumArtistList = {
|
||||
items: SSAlbumArtistListEntry[];
|
||||
startIndex: number;
|
||||
totalRecordCount: null | number;
|
||||
};
|
||||
|
||||
export type SSAlbumArtistListEntry = {
|
||||
albumCount: string;
|
||||
artistImageUrl?: string;
|
||||
coverArt?: string;
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type SSAlbumArtistListParams = {
|
||||
musicFolderId?: string;
|
||||
};
|
||||
|
||||
export type SSAlbumArtistListResponse = {
|
||||
artists: {
|
||||
ignoredArticles: string;
|
||||
index: SSArtistIndex[];
|
||||
lastModified: number;
|
||||
};
|
||||
};
|
||||
|
||||
export type SSAlbumDetail = Omit<SSAlbum, 'song'> & { songs: SSSong[] };
|
||||
|
||||
export type SSAlbumDetailResponse = {
|
||||
album: SSAlbum;
|
||||
};
|
||||
|
||||
export type SSAlbumList = {
|
||||
items: SSAlbumListEntry[];
|
||||
startIndex: number;
|
||||
totalRecordCount: null | number;
|
||||
};
|
||||
|
||||
export type SSAlbumListEntry = {
|
||||
album: string;
|
||||
artist: string;
|
||||
artistId: string;
|
||||
coverArt: string;
|
||||
created: string;
|
||||
duration: number;
|
||||
genre?: string;
|
||||
id: string;
|
||||
isDir: boolean;
|
||||
isVideo: boolean;
|
||||
name: string;
|
||||
parent: string;
|
||||
songCount: number;
|
||||
starred?: boolean;
|
||||
title: string;
|
||||
userRating?: number;
|
||||
year: number;
|
||||
};
|
||||
|
||||
export type SSAlbumListParams = {
|
||||
fromYear?: number;
|
||||
genre?: string;
|
||||
musicFolderId?: string;
|
||||
offset?: number;
|
||||
size?: number;
|
||||
toYear?: number;
|
||||
type: string;
|
||||
};
|
||||
|
||||
export type SSAlbumListResponse = {
|
||||
albumList2: {
|
||||
album: SSAlbumListEntry[];
|
||||
};
|
||||
};
|
||||
|
||||
export type SSArtistIndex = {
|
||||
artist: SSAlbumArtistListEntry[];
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type SSArtistInfo = {
|
||||
biography: string;
|
||||
largeImageUrl?: string;
|
||||
lastFmUrl?: string;
|
||||
mediumImageUrl?: string;
|
||||
musicBrainzId?: string;
|
||||
similarArtist?: {
|
||||
albumCount: string;
|
||||
artistImageUrl?: string;
|
||||
coverArt?: string;
|
||||
id: string;
|
||||
name: string;
|
||||
}[];
|
||||
smallImageUrl?: string;
|
||||
};
|
||||
|
||||
export type SSArtistInfoParams = {
|
||||
count?: number;
|
||||
id: string;
|
||||
includeNotPresent?: boolean;
|
||||
};
|
||||
|
||||
export type SSArtistInfoResponse = {
|
||||
artistInfo2: SSArtistInfo;
|
||||
};
|
||||
|
||||
export type SSBaseResponse = {
|
||||
serverVersion?: 'string';
|
||||
status: 'string';
|
||||
type?: 'string';
|
||||
version: 'string';
|
||||
};
|
||||
|
||||
export type SSFavorite = null;
|
||||
|
||||
export type SSFavoriteParams = {
|
||||
albumId?: string;
|
||||
artistId?: string;
|
||||
id?: string;
|
||||
};
|
||||
|
||||
export type SSFavoriteResponse = null;
|
||||
|
||||
export type SSGenre = {
|
||||
albumCount?: number;
|
||||
songCount?: number;
|
||||
value: string;
|
||||
};
|
||||
|
||||
export type SSGenreList = SSGenre[];
|
||||
|
||||
export type SSGenreListResponse = {
|
||||
genres: {
|
||||
genre: SSGenre[];
|
||||
};
|
||||
};
|
||||
|
||||
export type SSMusicFolder = {
|
||||
id: number;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type SSMusicFolderList = SSMusicFolder[];
|
||||
|
||||
export type SSMusicFolderListResponse = {
|
||||
musicFolders: {
|
||||
musicFolder: SSMusicFolder[];
|
||||
};
|
||||
};
|
||||
|
||||
export type SSRating = null;
|
||||
|
||||
export type SSRatingParams = {
|
||||
id: string;
|
||||
rating: number;
|
||||
};
|
||||
|
||||
export type SSRatingResponse = null;
|
||||
|
||||
export type SSScrobbleParams = {
|
||||
id: string;
|
||||
submission?: boolean;
|
||||
time?: number;
|
||||
};
|
||||
|
||||
export type SSSong = {
|
||||
album: string;
|
||||
albumId: string;
|
||||
artist: string;
|
||||
artistId?: string;
|
||||
bitRate: number;
|
||||
contentType: string;
|
||||
coverArt: string;
|
||||
created: string;
|
||||
discNumber?: number;
|
||||
duration: number;
|
||||
genre: string;
|
||||
id: string;
|
||||
isDir: boolean;
|
||||
isVideo: boolean;
|
||||
parent: string;
|
||||
path: string;
|
||||
playCount: number;
|
||||
size: number;
|
||||
starred?: boolean;
|
||||
suffix: string;
|
||||
title: string;
|
||||
track: number;
|
||||
type: string;
|
||||
userRating?: number;
|
||||
year: number;
|
||||
};
|
||||
|
||||
export type SSTopSongList = {
|
||||
items: SSSong[];
|
||||
startIndex: number;
|
||||
totalRecordCount: null | number;
|
||||
};
|
||||
|
||||
export type SSTopSongListParams = {
|
||||
artist: string;
|
||||
count?: number;
|
||||
};
|
||||
|
||||
export type SSTopSongListResponse = {
|
||||
topSongs: {
|
||||
song: SSSong[];
|
||||
};
|
||||
};
|
||||
327
src/shared/api/subsonic/subsonic-normalize.ts
Normal file
327
src/shared/api/subsonic/subsonic-normalize.ts
Normal file
|
|
@ -0,0 +1,327 @@
|
|||
import { nanoid } from 'nanoid';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { ssType } from '/@/shared/api/subsonic/subsonic-types';
|
||||
import {
|
||||
Album,
|
||||
AlbumArtist,
|
||||
Genre,
|
||||
LibraryItem,
|
||||
Playlist,
|
||||
QueueSong,
|
||||
RelatedArtist,
|
||||
ServerListItem,
|
||||
ServerType,
|
||||
} from '/@/shared/types/domain-types';
|
||||
|
||||
const getCoverArtUrl = (args: {
|
||||
baseUrl: string | undefined;
|
||||
coverArtId?: string;
|
||||
credential: string | undefined;
|
||||
size: number;
|
||||
}) => {
|
||||
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}`
|
||||
);
|
||||
};
|
||||
|
||||
const getArtists = (
|
||||
item:
|
||||
| z.infer<typeof ssType._response.album>
|
||||
| z.infer<typeof ssType._response.albumListEntry>
|
||||
| z.infer<typeof ssType._response.song>,
|
||||
) => {
|
||||
const albumArtists: RelatedArtist[] = item.albumArtists
|
||||
? item.albumArtists.map((item) => ({
|
||||
id: item.id.toString(),
|
||||
imageUrl: null,
|
||||
name: item.name,
|
||||
}))
|
||||
: [
|
||||
{
|
||||
id: item.artistId?.toString() || '',
|
||||
imageUrl: null,
|
||||
name: item.artist || '',
|
||||
},
|
||||
];
|
||||
|
||||
const artists: RelatedArtist[] = item.artists
|
||||
? item.artists.map((item) => ({
|
||||
id: item.id.toString(),
|
||||
imageUrl: null,
|
||||
name: item.name,
|
||||
}))
|
||||
: [
|
||||
{
|
||||
id: item.artistId?.toString() || '',
|
||||
imageUrl: null,
|
||||
name: item.artist || '',
|
||||
},
|
||||
];
|
||||
|
||||
let participants: null | Record<string, RelatedArtist[]> = null;
|
||||
|
||||
if (item.contributors) {
|
||||
participants = {};
|
||||
|
||||
for (const contributor of item.contributors) {
|
||||
const artist = {
|
||||
id: contributor.artist.id?.toString() || '',
|
||||
imageUrl: null,
|
||||
name: contributor.artist.name || '',
|
||||
};
|
||||
|
||||
const role = contributor.subRole
|
||||
? `${contributor.role} (${contributor.subRole})`
|
||||
: contributor.role;
|
||||
|
||||
if (role in participants) {
|
||||
participants[role].push(artist);
|
||||
} else {
|
||||
participants[role] = [artist];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { albumArtists, artists, participants };
|
||||
};
|
||||
|
||||
const getGenres = (
|
||||
item:
|
||||
| z.infer<typeof ssType._response.album>
|
||||
| z.infer<typeof ssType._response.albumListEntry>
|
||||
| z.infer<typeof ssType._response.song>,
|
||||
): Genre[] => {
|
||||
return item.genres
|
||||
? item.genres.map((genre) => ({
|
||||
id: genre.name,
|
||||
imageUrl: null,
|
||||
itemType: LibraryItem.GENRE,
|
||||
name: genre.name,
|
||||
}))
|
||||
: item.genre
|
||||
? [
|
||||
{
|
||||
id: item.genre,
|
||||
imageUrl: null,
|
||||
itemType: LibraryItem.GENRE,
|
||||
name: item.genre,
|
||||
},
|
||||
]
|
||||
: [];
|
||||
};
|
||||
|
||||
const normalizeSong = (
|
||||
item: z.infer<typeof ssType._response.song>,
|
||||
server: null | ServerListItem,
|
||||
size?: number,
|
||||
): QueueSong => {
|
||||
const imageUrl =
|
||||
getCoverArtUrl({
|
||||
baseUrl: server?.url,
|
||||
coverArtId: item.coverArt?.toString(),
|
||||
credential: server?.credential,
|
||||
size: size || 300,
|
||||
}) || null;
|
||||
|
||||
const streamUrl = `${server?.url}/rest/stream.view?id=${item.id}&v=1.13.0&c=Feishin&${server?.credential}`;
|
||||
|
||||
return {
|
||||
album: item.album || '',
|
||||
albumId: item.albumId?.toString() || '',
|
||||
artistName: item.artist || '',
|
||||
...getArtists(item),
|
||||
bitRate: item.bitRate || 0,
|
||||
bpm: item.bpm || null,
|
||||
channels: null,
|
||||
comment: null,
|
||||
compilation: null,
|
||||
container: item.contentType,
|
||||
createdAt: item.created,
|
||||
discNumber: item.discNumber || 1,
|
||||
discSubtitle: null,
|
||||
duration: item.duration ? item.duration * 1000 : 0,
|
||||
gain:
|
||||
item.replayGain && (item.replayGain.albumGain || item.replayGain.trackGain)
|
||||
? {
|
||||
album: item.replayGain.albumGain,
|
||||
track: item.replayGain.trackGain,
|
||||
}
|
||||
: null,
|
||||
genres: getGenres(item),
|
||||
id: item.id.toString(),
|
||||
imagePlaceholderUrl: null,
|
||||
imageUrl,
|
||||
itemType: LibraryItem.SONG,
|
||||
lastPlayedAt: null,
|
||||
lyrics: null,
|
||||
name: item.title,
|
||||
path: item.path,
|
||||
peak:
|
||||
item.replayGain && (item.replayGain.albumPeak || item.replayGain.trackPeak)
|
||||
? {
|
||||
album: item.replayGain.albumPeak,
|
||||
track: item.replayGain.trackPeak,
|
||||
}
|
||||
: null,
|
||||
playCount: item?.playCount || 0,
|
||||
releaseDate: null,
|
||||
releaseYear: item.year ? String(item.year) : null,
|
||||
serverId: server?.id || 'unknown',
|
||||
serverType: ServerType.SUBSONIC,
|
||||
size: item.size,
|
||||
streamUrl,
|
||||
tags: null,
|
||||
trackNumber: item.track || 1,
|
||||
uniqueId: nanoid(),
|
||||
updatedAt: '',
|
||||
userFavorite: item.starred || false,
|
||||
userRating: item.userRating || null,
|
||||
};
|
||||
};
|
||||
|
||||
const normalizeAlbumArtist = (
|
||||
item:
|
||||
| z.infer<typeof ssType._response.albumArtist>
|
||||
| z.infer<typeof ssType._response.artistListEntry>,
|
||||
server: null | ServerListItem,
|
||||
imageSize?: number,
|
||||
): AlbumArtist => {
|
||||
const imageUrl =
|
||||
getCoverArtUrl({
|
||||
baseUrl: server?.url,
|
||||
coverArtId: item.coverArt?.toString(),
|
||||
credential: server?.credential,
|
||||
size: imageSize || 100,
|
||||
}) || null;
|
||||
|
||||
return {
|
||||
albumCount: item.albumCount ? Number(item.albumCount) : 0,
|
||||
backgroundImageUrl: null,
|
||||
biography: null,
|
||||
duration: null,
|
||||
genres: [],
|
||||
id: item.id.toString(),
|
||||
imageUrl,
|
||||
itemType: LibraryItem.ALBUM_ARTIST,
|
||||
lastPlayedAt: null,
|
||||
mbz: null,
|
||||
name: item.name,
|
||||
playCount: null,
|
||||
serverId: server?.id || 'unknown',
|
||||
serverType: ServerType.SUBSONIC,
|
||||
similarArtists: [],
|
||||
songCount: null,
|
||||
userFavorite: false,
|
||||
userRating: null,
|
||||
};
|
||||
};
|
||||
|
||||
const normalizeAlbum = (
|
||||
item: z.infer<typeof ssType._response.album> | z.infer<typeof ssType._response.albumListEntry>,
|
||||
server: null | ServerListItem,
|
||||
imageSize?: number,
|
||||
): Album => {
|
||||
const imageUrl =
|
||||
getCoverArtUrl({
|
||||
baseUrl: server?.url,
|
||||
coverArtId: item.coverArt?.toString(),
|
||||
credential: server?.credential,
|
||||
size: imageSize || 300,
|
||||
}) || null;
|
||||
|
||||
return {
|
||||
albumArtist: item.artist,
|
||||
...getArtists(item),
|
||||
backdropImageUrl: null,
|
||||
comment: null,
|
||||
createdAt: item.created,
|
||||
duration: item.duration * 1000,
|
||||
genres: getGenres(item),
|
||||
id: item.id.toString(),
|
||||
imagePlaceholderUrl: null,
|
||||
imageUrl,
|
||||
isCompilation: null,
|
||||
itemType: LibraryItem.ALBUM,
|
||||
lastPlayedAt: null,
|
||||
mbzId: null,
|
||||
name: item.name,
|
||||
originalDate: null,
|
||||
playCount: null,
|
||||
releaseDate: item.year ? new Date(Date.UTC(item.year, 0, 1)).toISOString() : null,
|
||||
releaseYear: item.year ? Number(item.year) : null,
|
||||
serverId: server?.id || 'unknown',
|
||||
serverType: ServerType.SUBSONIC,
|
||||
size: null,
|
||||
songCount: item.songCount,
|
||||
songs:
|
||||
(item as z.infer<typeof ssType._response.album>).song?.map((song) =>
|
||||
normalizeSong(song, server),
|
||||
) || [],
|
||||
tags: item.tags || null,
|
||||
uniqueId: nanoid(),
|
||||
updatedAt: item.created,
|
||||
userFavorite: item.starred || false,
|
||||
userRating: item.userRating || null,
|
||||
};
|
||||
};
|
||||
|
||||
const normalizePlaylist = (
|
||||
item:
|
||||
| z.infer<typeof ssType._response.playlist>
|
||||
| z.infer<typeof ssType._response.playlistListEntry>,
|
||||
server: null | ServerListItem,
|
||||
): Playlist => {
|
||||
return {
|
||||
description: item.comment || null,
|
||||
duration: item.duration,
|
||||
genres: [],
|
||||
id: item.id.toString(),
|
||||
imagePlaceholderUrl: null,
|
||||
imageUrl: getCoverArtUrl({
|
||||
baseUrl: server?.url,
|
||||
coverArtId: item.coverArt?.toString(),
|
||||
credential: server?.credential,
|
||||
size: 300,
|
||||
}),
|
||||
itemType: LibraryItem.PLAYLIST,
|
||||
name: item.name,
|
||||
owner: item.owner,
|
||||
ownerId: item.owner,
|
||||
public: item.public,
|
||||
serverId: server?.id || 'unknown',
|
||||
serverType: ServerType.SUBSONIC,
|
||||
size: null,
|
||||
songCount: item.songCount,
|
||||
};
|
||||
};
|
||||
|
||||
const normalizeGenre = (item: z.infer<typeof ssType._response.genre>): Genre => {
|
||||
return {
|
||||
albumCount: item.albumCount,
|
||||
id: item.value,
|
||||
imageUrl: null,
|
||||
itemType: LibraryItem.GENRE,
|
||||
name: item.value,
|
||||
songCount: item.songCount,
|
||||
};
|
||||
};
|
||||
|
||||
export const ssNormalize = {
|
||||
album: normalizeAlbum,
|
||||
albumArtist: normalizeAlbumArtist,
|
||||
genre: normalizeGenre,
|
||||
playlist: normalizePlaylist,
|
||||
song: normalizeSong,
|
||||
};
|
||||
609
src/shared/api/subsonic/subsonic-types.ts
Normal file
609
src/shared/api/subsonic/subsonic-types.ts
Normal file
|
|
@ -0,0 +1,609 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
const baseResponse = z.object({
|
||||
'subsonic-response': z.object({
|
||||
status: z.string(),
|
||||
version: z.string(),
|
||||
}),
|
||||
});
|
||||
|
||||
const authenticate = z.null();
|
||||
|
||||
const authenticateParameters = z.object({
|
||||
c: z.string(),
|
||||
f: z.string(),
|
||||
p: z.string().optional(),
|
||||
s: z.string().optional(),
|
||||
t: z.string().optional(),
|
||||
u: z.string(),
|
||||
v: z.string(),
|
||||
});
|
||||
|
||||
const id = z.number().or(z.string());
|
||||
|
||||
const createFavoriteParameters = z.object({
|
||||
albumId: z.array(z.string()).optional(),
|
||||
artistId: z.array(z.string()).optional(),
|
||||
id: z.array(z.string()).optional(),
|
||||
});
|
||||
|
||||
const createFavorite = z.null();
|
||||
|
||||
const removeFavoriteParameters = z.object({
|
||||
albumId: z.array(z.string()).optional(),
|
||||
artistId: z.array(z.string()).optional(),
|
||||
id: z.array(z.string()).optional(),
|
||||
});
|
||||
|
||||
const removeFavorite = z.null();
|
||||
|
||||
const setRatingParameters = z.object({
|
||||
id: z.string(),
|
||||
rating: z.number(),
|
||||
});
|
||||
|
||||
const setRating = z.null();
|
||||
|
||||
const musicFolder = z.object({
|
||||
id,
|
||||
name: z.string(),
|
||||
});
|
||||
|
||||
const musicFolderList = z.object({
|
||||
musicFolders: z.object({
|
||||
musicFolder: z.array(musicFolder),
|
||||
}),
|
||||
});
|
||||
|
||||
const songGain = z.object({
|
||||
albumGain: z.number().optional(),
|
||||
albumPeak: z.number().optional(),
|
||||
trackGain: z.number().optional(),
|
||||
trackPeak: z.number().optional(),
|
||||
});
|
||||
|
||||
const genreItem = z.object({
|
||||
name: z.string(),
|
||||
});
|
||||
|
||||
const simpleArtist = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
});
|
||||
|
||||
const contributor = z.object({
|
||||
artist: simpleArtist,
|
||||
role: z.string(),
|
||||
subRole: z.string().optional(),
|
||||
});
|
||||
|
||||
const song = z.object({
|
||||
album: z.string().optional(),
|
||||
albumArtists: z.array(simpleArtist),
|
||||
albumId: id.optional(),
|
||||
artist: z.string().optional(),
|
||||
artistId: id.optional(),
|
||||
artists: z.array(simpleArtist),
|
||||
averageRating: z.number().optional(),
|
||||
bitRate: z.number().optional(),
|
||||
bpm: z.number().optional(),
|
||||
contentType: z.string(),
|
||||
contributors: z.array(contributor).optional(),
|
||||
coverArt: z.string().optional(),
|
||||
created: z.string(),
|
||||
discNumber: z.number(),
|
||||
duration: z.number().optional(),
|
||||
genre: z.string().optional(),
|
||||
genres: z.array(genreItem).optional(),
|
||||
id,
|
||||
isDir: z.boolean(),
|
||||
isVideo: z.boolean(),
|
||||
musicBrainzId: z.string().optional(),
|
||||
parent: z.string(),
|
||||
path: z.string(),
|
||||
playCount: z.number().optional(),
|
||||
replayGain: songGain.optional(),
|
||||
size: z.number(),
|
||||
starred: z.boolean().optional(),
|
||||
suffix: z.string(),
|
||||
title: z.string(),
|
||||
track: z.number().optional(),
|
||||
type: z.string(),
|
||||
userRating: z.number().optional(),
|
||||
year: z.number().optional(),
|
||||
});
|
||||
|
||||
const album = z.object({
|
||||
album: z.string(),
|
||||
albumArtists: z.array(simpleArtist),
|
||||
artist: z.string(),
|
||||
artistId: id,
|
||||
artists: z.array(simpleArtist),
|
||||
contributors: z.array(contributor).optional(),
|
||||
coverArt: z.string(),
|
||||
created: z.string(),
|
||||
duration: z.number(),
|
||||
genre: z.string().optional(),
|
||||
genres: z.array(genreItem).optional(),
|
||||
id,
|
||||
isCompilation: z.boolean().optional(),
|
||||
isDir: z.boolean(),
|
||||
isVideo: z.boolean(),
|
||||
name: z.string(),
|
||||
parent: z.string(),
|
||||
song: z.array(song),
|
||||
songCount: z.number(),
|
||||
starred: z.boolean().optional(),
|
||||
title: z.string(),
|
||||
userRating: z.number().optional(),
|
||||
year: z.number().optional(),
|
||||
});
|
||||
|
||||
const albumListEntry = album.omit({
|
||||
song: true,
|
||||
});
|
||||
|
||||
const albumListParameters = z.object({
|
||||
fromYear: z.number().optional(),
|
||||
genre: z.string().optional(),
|
||||
musicFolderId: z.string().optional(),
|
||||
offset: z.number().optional(),
|
||||
size: z.number().optional(),
|
||||
toYear: z.number().optional(),
|
||||
type: z.string().optional(),
|
||||
});
|
||||
|
||||
const albumList = z.array(album.omit({ song: true }));
|
||||
|
||||
const albumArtist = z.object({
|
||||
album: z.array(album).optional(),
|
||||
albumCount: z.string(),
|
||||
artistImageUrl: z.string().optional(),
|
||||
coverArt: z.string().optional(),
|
||||
id,
|
||||
name: z.string(),
|
||||
roles: z.array(z.string()).optional(),
|
||||
starred: z.string().optional(),
|
||||
});
|
||||
|
||||
const albumArtistList = z.object({
|
||||
artist: z.array(albumArtist),
|
||||
name: z.string(),
|
||||
});
|
||||
|
||||
const artistListEntry = albumArtist.pick({
|
||||
albumCount: true,
|
||||
coverArt: true,
|
||||
id: true,
|
||||
name: true,
|
||||
roles: true,
|
||||
starred: true,
|
||||
});
|
||||
|
||||
const artistInfoParameters = z.object({
|
||||
count: z.number().optional(),
|
||||
id: z.string(),
|
||||
includeNotPresent: z.boolean().optional(),
|
||||
});
|
||||
|
||||
const artistInfo = z.object({
|
||||
artistInfo: z.object({
|
||||
biography: z.string().optional(),
|
||||
largeImageUrl: z.string().optional(),
|
||||
lastFmUrl: z.string().optional(),
|
||||
mediumImageUrl: z.string().optional(),
|
||||
musicBrainzId: z.string().optional(),
|
||||
similarArtist: z.array(
|
||||
z.object({
|
||||
albumCount: z.string(),
|
||||
artistImageUrl: z.string().optional(),
|
||||
coverArt: z.string().optional(),
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
}),
|
||||
),
|
||||
smallImageUrl: z.string().optional(),
|
||||
}),
|
||||
});
|
||||
|
||||
const topSongsListParameters = z.object({
|
||||
artist: z.string(), // The name of the artist, not the artist ID
|
||||
count: z.number().optional(),
|
||||
});
|
||||
|
||||
const topSongsList = z.object({
|
||||
topSongs: z
|
||||
.object({
|
||||
song: z.array(song),
|
||||
})
|
||||
.optional(),
|
||||
});
|
||||
|
||||
const scrobbleParameters = z.object({
|
||||
id: z.string(),
|
||||
submission: z.boolean().optional(),
|
||||
time: z.number().optional(), // The time (in milliseconds since 1 Jan 1970) at which the song was listened to.
|
||||
});
|
||||
|
||||
const scrobble = z.null();
|
||||
|
||||
const search3 = z.object({
|
||||
searchResult3: z
|
||||
.object({
|
||||
album: z.array(album).optional(),
|
||||
artist: z.array(albumArtist).optional(),
|
||||
song: z.array(song).optional(),
|
||||
})
|
||||
.optional(),
|
||||
});
|
||||
|
||||
const search3Parameters = z.object({
|
||||
albumCount: z.number().optional(),
|
||||
albumOffset: z.number().optional(),
|
||||
artistCount: z.number().optional(),
|
||||
artistOffset: z.number().optional(),
|
||||
musicFolderId: z.string().optional(),
|
||||
query: z.string().optional(),
|
||||
songCount: z.number().optional(),
|
||||
songOffset: z.number().optional(),
|
||||
});
|
||||
|
||||
const randomSongListParameters = z.object({
|
||||
fromYear: z.number().optional(),
|
||||
genre: z.string().optional(),
|
||||
musicFolderId: z.string().optional(),
|
||||
size: z.number().optional(),
|
||||
toYear: z.number().optional(),
|
||||
});
|
||||
|
||||
const randomSongList = z.object({
|
||||
randomSongs: z
|
||||
.object({
|
||||
song: z.array(song),
|
||||
})
|
||||
.optional(),
|
||||
});
|
||||
|
||||
const ping = z.object({
|
||||
openSubsonic: z.boolean().optional(),
|
||||
serverVersion: z.string().optional(),
|
||||
version: z.string(),
|
||||
});
|
||||
|
||||
const extension = z.object({
|
||||
name: z.string(),
|
||||
versions: z.number().array(),
|
||||
});
|
||||
|
||||
const serverInfo = z.object({
|
||||
openSubsonicExtensions: z.array(extension).optional(),
|
||||
});
|
||||
|
||||
const structuredLyricsParameters = z.object({
|
||||
id: z.string(),
|
||||
});
|
||||
|
||||
const lyricLine = z.object({
|
||||
start: z.number().optional(),
|
||||
value: z.string(),
|
||||
});
|
||||
|
||||
const structuredLyric = z.object({
|
||||
displayArtist: z.string().optional(),
|
||||
displayTitle: z.string().optional(),
|
||||
lang: z.string(),
|
||||
line: z.array(lyricLine),
|
||||
offset: z.number().optional(),
|
||||
synced: z.boolean(),
|
||||
});
|
||||
|
||||
const structuredLyrics = z.object({
|
||||
lyricsList: z
|
||||
.object({
|
||||
structuredLyrics: z.array(structuredLyric).optional(),
|
||||
})
|
||||
.optional(),
|
||||
});
|
||||
|
||||
const similarSongsParameters = z.object({
|
||||
count: z.number().optional(),
|
||||
id: z.string(),
|
||||
});
|
||||
|
||||
const similarSongs = z.object({
|
||||
similarSongs: z
|
||||
.object({
|
||||
song: z.array(song),
|
||||
})
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export enum SubsonicExtensions {
|
||||
FORM_POST = 'formPost',
|
||||
SONG_LYRICS = 'songLyrics',
|
||||
TRANSCODE_OFFSET = 'transcodeOffset',
|
||||
}
|
||||
|
||||
const updatePlaylistParameters = z.object({
|
||||
comment: z.string().optional(),
|
||||
name: z.string().optional(),
|
||||
playlistId: z.string(),
|
||||
public: z.boolean().optional(),
|
||||
songIdToAdd: z.array(z.string()).optional(),
|
||||
songIndexToRemove: z.array(z.string()).optional(),
|
||||
});
|
||||
|
||||
const getStarredParameters = z.object({
|
||||
musicFolderId: z.string().optional(),
|
||||
});
|
||||
|
||||
const getStarred = z.object({
|
||||
starred: z
|
||||
.object({
|
||||
album: z.array(albumListEntry),
|
||||
artist: z.array(artistListEntry),
|
||||
song: z.array(song),
|
||||
})
|
||||
.optional(),
|
||||
});
|
||||
|
||||
const getSongsByGenreParameters = z.object({
|
||||
count: z.number().optional(),
|
||||
genre: z.string(),
|
||||
musicFolderId: z.string().optional(),
|
||||
offset: z.number().optional(),
|
||||
});
|
||||
|
||||
const getSongsByGenre = z.object({
|
||||
songsByGenre: z
|
||||
.object({
|
||||
song: z.array(song),
|
||||
})
|
||||
.optional(),
|
||||
});
|
||||
|
||||
const getAlbumParameters = z.object({
|
||||
id: z.string(),
|
||||
musicFolderId: z.string().optional(),
|
||||
});
|
||||
|
||||
const getAlbum = z.object({
|
||||
album,
|
||||
});
|
||||
|
||||
const getArtistParameters = z.object({
|
||||
id: z.string(),
|
||||
});
|
||||
|
||||
const getArtist = z.object({
|
||||
artist: albumArtist,
|
||||
});
|
||||
|
||||
const getSongParameters = z.object({
|
||||
id: z.string(),
|
||||
});
|
||||
|
||||
const getSong = z.object({
|
||||
song,
|
||||
});
|
||||
|
||||
const getArtistsParameters = z.object({
|
||||
musicFolderId: z.string().optional(),
|
||||
});
|
||||
|
||||
const getArtists = z.object({
|
||||
artists: z.object({
|
||||
ignoredArticles: z.string(),
|
||||
index: z.array(
|
||||
z.object({
|
||||
artist: z.array(artistListEntry),
|
||||
name: z.string(),
|
||||
}),
|
||||
),
|
||||
}),
|
||||
});
|
||||
|
||||
const deletePlaylistParameters = z.object({
|
||||
id: z.string(),
|
||||
});
|
||||
|
||||
const createPlaylistParameters = z.object({
|
||||
name: z.string(),
|
||||
playlistId: z.string().optional(),
|
||||
songId: z.array(z.string()).optional(),
|
||||
});
|
||||
|
||||
const playlist = z.object({
|
||||
changed: z.string().optional(),
|
||||
comment: z.string().optional(),
|
||||
coverArt: z.string().optional(),
|
||||
created: z.string(),
|
||||
duration: z.number(),
|
||||
entry: z.array(song).optional(),
|
||||
id,
|
||||
name: z.string(),
|
||||
owner: z.string(),
|
||||
public: z.boolean(),
|
||||
songCount: z.number(),
|
||||
});
|
||||
|
||||
const createPlaylist = z.object({
|
||||
playlist,
|
||||
});
|
||||
|
||||
const getPlaylistsParameters = z.object({
|
||||
username: z.string().optional(),
|
||||
});
|
||||
|
||||
const playlistListEntry = playlist.omit({
|
||||
entry: true,
|
||||
});
|
||||
|
||||
const getPlaylists = z.object({
|
||||
playlists: z
|
||||
.object({
|
||||
playlist: z.array(playlistListEntry),
|
||||
})
|
||||
.optional(),
|
||||
});
|
||||
|
||||
const getPlaylistParameters = z.object({
|
||||
id: z.string(),
|
||||
});
|
||||
|
||||
const getPlaylist = z.object({
|
||||
playlist,
|
||||
});
|
||||
|
||||
const genre = z.object({
|
||||
albumCount: z.number(),
|
||||
songCount: z.number(),
|
||||
value: z.string(),
|
||||
});
|
||||
|
||||
const getGenresParameters = z.object({});
|
||||
|
||||
const getGenres = z.object({
|
||||
genres: z
|
||||
.object({
|
||||
genre: z.array(genre),
|
||||
})
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export enum AlbumListSortType {
|
||||
ALPHABETICAL_BY_ARTIST = 'alphabeticalByArtist',
|
||||
ALPHABETICAL_BY_NAME = 'alphabeticalByName',
|
||||
BY_GENRE = 'byGenre',
|
||||
BY_YEAR = 'byYear',
|
||||
FREQUENT = 'frequent',
|
||||
NEWEST = 'newest',
|
||||
RANDOM = 'random',
|
||||
RECENT = 'recent',
|
||||
STARRED = 'starred',
|
||||
}
|
||||
|
||||
const getAlbumList2Parameters = z
|
||||
.object({
|
||||
fromYear: z.number().optional(),
|
||||
genre: z.string().optional(),
|
||||
musicFolderId: z.string().optional(),
|
||||
offset: z.number().optional(),
|
||||
size: z.number().optional(),
|
||||
toYear: z.number().optional(),
|
||||
type: z.nativeEnum(AlbumListSortType),
|
||||
})
|
||||
.refine(
|
||||
(val) => {
|
||||
if (val.type === AlbumListSortType.BY_YEAR) {
|
||||
return val.fromYear !== undefined && val.toYear !== undefined;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
{
|
||||
message: 'Parameters "fromYear" and "toYear" are required when using sort "byYear"',
|
||||
},
|
||||
)
|
||||
.refine(
|
||||
(val) => {
|
||||
if (val.type === AlbumListSortType.BY_GENRE) {
|
||||
return val.genre !== undefined;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
{ message: 'Parameter "genre" is required when using sort "byGenre"' },
|
||||
);
|
||||
|
||||
const getAlbumList2 = z.object({
|
||||
albumList2: z.object({
|
||||
album: z.array(albumListEntry),
|
||||
}),
|
||||
});
|
||||
|
||||
const albumInfoParameters = z.object({
|
||||
id: z.string(),
|
||||
});
|
||||
|
||||
const albumInfo = z.object({
|
||||
albumInfo: z.object({
|
||||
largeImageUrl: z.string().optional(),
|
||||
lastFmUrl: z.string().optional(),
|
||||
mediumImageUrl: z.string().optional(),
|
||||
musicBrainzId: z.string().optional(),
|
||||
notes: z.string().optional(),
|
||||
smallImageUrl: z.string().optional(),
|
||||
}),
|
||||
});
|
||||
|
||||
export const ssType = {
|
||||
_parameters: {
|
||||
albumInfo: albumInfoParameters,
|
||||
albumList: albumListParameters,
|
||||
artistInfo: artistInfoParameters,
|
||||
authenticate: authenticateParameters,
|
||||
createFavorite: createFavoriteParameters,
|
||||
createPlaylist: createPlaylistParameters,
|
||||
deletePlaylist: deletePlaylistParameters,
|
||||
getAlbum: getAlbumParameters,
|
||||
getAlbumList2: getAlbumList2Parameters,
|
||||
getArtist: getArtistParameters,
|
||||
getArtists: getArtistsParameters,
|
||||
getGenre: getGenresParameters,
|
||||
getGenres: getGenresParameters,
|
||||
getPlaylist: getPlaylistParameters,
|
||||
getPlaylists: getPlaylistsParameters,
|
||||
getSong: getSongParameters,
|
||||
getSongsByGenre: getSongsByGenreParameters,
|
||||
getStarred: getStarredParameters,
|
||||
randomSongList: randomSongListParameters,
|
||||
removeFavorite: removeFavoriteParameters,
|
||||
scrobble: scrobbleParameters,
|
||||
search3: search3Parameters,
|
||||
setRating: setRatingParameters,
|
||||
similarSongs: similarSongsParameters,
|
||||
structuredLyrics: structuredLyricsParameters,
|
||||
topSongsList: topSongsListParameters,
|
||||
updatePlaylist: updatePlaylistParameters,
|
||||
},
|
||||
_response: {
|
||||
album,
|
||||
albumArtist,
|
||||
albumArtistList,
|
||||
albumInfo,
|
||||
albumList,
|
||||
albumListEntry,
|
||||
artistInfo,
|
||||
artistListEntry,
|
||||
authenticate,
|
||||
baseResponse,
|
||||
createFavorite,
|
||||
createPlaylist,
|
||||
genre,
|
||||
getAlbum,
|
||||
getAlbumList2,
|
||||
getArtist,
|
||||
getArtists,
|
||||
getGenres,
|
||||
getPlaylist,
|
||||
getPlaylists,
|
||||
getSong,
|
||||
getSongsByGenre,
|
||||
getStarred,
|
||||
musicFolderList,
|
||||
ping,
|
||||
playlist,
|
||||
playlistListEntry,
|
||||
randomSongList,
|
||||
removeFavorite,
|
||||
scrobble,
|
||||
search3,
|
||||
serverInfo,
|
||||
setRating,
|
||||
similarSongs,
|
||||
song,
|
||||
structuredLyrics,
|
||||
topSongsList,
|
||||
},
|
||||
};
|
||||
113
src/shared/api/utils.ts
Normal file
113
src/shared/api/utils.ts
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
import { AxiosHeaders } from 'axios';
|
||||
import isElectron from 'is-electron';
|
||||
import semverCoerce from 'semver/functions/coerce';
|
||||
import semverGte from 'semver/functions/gte';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { ServerListItem } from '/@/shared/types/domain-types';
|
||||
import { ServerFeature } from '/@/shared/types/features-types';
|
||||
|
||||
// Since ts-rest client returns a strict response type, we need to add the headers to the body object
|
||||
export const resultWithHeaders = <ItemType extends z.ZodTypeAny>(itemSchema: ItemType) => {
|
||||
return z.object({
|
||||
data: itemSchema,
|
||||
headers: z.instanceof(AxiosHeaders),
|
||||
});
|
||||
};
|
||||
|
||||
export const resultSubsonicBaseResponse = <ItemType extends z.ZodRawShape>(
|
||||
itemSchema: ItemType,
|
||||
) => {
|
||||
return z.object({
|
||||
'subsonic-response': z
|
||||
.object({
|
||||
status: z.string(),
|
||||
version: z.string(),
|
||||
})
|
||||
.extend(itemSchema),
|
||||
});
|
||||
};
|
||||
|
||||
export const hasFeature = (server: null | ServerListItem, feature: ServerFeature): boolean => {
|
||||
if (!server || !server.features) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (server.features[feature]?.length || 0) > 0;
|
||||
};
|
||||
|
||||
export type VersionInfo = ReadonlyArray<[string, Record<string, readonly number[]>]>;
|
||||
|
||||
/**
|
||||
* Returns the available server features given the version string.
|
||||
* @param versionInfo a list, in DECREASING VERSION order, of the features supported by the server.
|
||||
* The first version match will automatically consider the rest matched.
|
||||
* @example
|
||||
* ```
|
||||
* // The CORRECT way to order
|
||||
* const VERSION_INFO: VersionInfo = [
|
||||
* ['0.49.3', { [ServerFeature.SHARING_ALBUM_SONG]: [1] }],
|
||||
* ['0.48.0', { [ServerFeature.PLAYLISTS_SMART]: [1] }],
|
||||
* ];
|
||||
* // INCORRECT way to order
|
||||
* const VERSION_INFO: VersionInfo = [
|
||||
* ['0.48.0', { [ServerFeature.PLAYLISTS_SMART]: [1] }],
|
||||
* ['0.49.3', { [ServerFeature.SHARING_ALBUM_SONG]: [1] }],
|
||||
* ];
|
||||
* ```
|
||||
* @param version the version string (SemVer)
|
||||
* @returns a Record containing the matched features (if any) and their versions
|
||||
*/
|
||||
export const getFeatures = (
|
||||
versionInfo: VersionInfo,
|
||||
version: string,
|
||||
): Record<string, number[]> => {
|
||||
const cleanVersion = semverCoerce(version);
|
||||
const features: Record<string, number[]> = {};
|
||||
let matched = cleanVersion === null;
|
||||
|
||||
for (const [version, supportedFeatures] of versionInfo) {
|
||||
if (!matched) {
|
||||
matched = semverGte(cleanVersion!, version);
|
||||
}
|
||||
|
||||
if (matched) {
|
||||
for (const [feature, feat] of Object.entries(supportedFeatures)) {
|
||||
if (feature in features) {
|
||||
features[feature].push(...feat);
|
||||
} else {
|
||||
features[feature] = [...feat];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return features;
|
||||
};
|
||||
|
||||
export const getClientType = (): string => {
|
||||
if (isElectron()) {
|
||||
return 'Desktop Client';
|
||||
}
|
||||
const agent = navigator.userAgent;
|
||||
switch (true) {
|
||||
case agent.toLowerCase().indexOf('edge') > -1:
|
||||
return 'Microsoft Edge';
|
||||
case agent.toLowerCase().indexOf('edg/') > -1:
|
||||
return 'Edge Chromium'; // Match also / to avoid matching for the older Edge
|
||||
case agent.toLowerCase().indexOf('opr') > -1:
|
||||
return 'Opera';
|
||||
case agent.toLowerCase().indexOf('chrome') > -1:
|
||||
return 'Chrome';
|
||||
case agent.toLowerCase().indexOf('trident') > -1:
|
||||
return 'Internet Explorer';
|
||||
case agent.toLowerCase().indexOf('firefox') > -1:
|
||||
return 'Firefox';
|
||||
case agent.toLowerCase().indexOf('safari') > -1:
|
||||
return 'Safari';
|
||||
default:
|
||||
return 'PC';
|
||||
}
|
||||
};
|
||||
|
||||
export const SEPARATOR_STRING = ' · ';
|
||||
1484
src/shared/types/domain-types.ts
Normal file
1484
src/shared/types/domain-types.ts
Normal file
File diff suppressed because it is too large
Load diff
13
src/shared/types/features-types.ts
Normal file
13
src/shared/types/features-types.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
// Should follow a strict naming convention: "<FEATURE GROUP>_<FEATURE NAME>"
|
||||
// For example: <FEATURE GROUP>: "Playlists", <FEATURE NAME>: "Smart" = "PLAYLISTS_SMART"
|
||||
export enum ServerFeature {
|
||||
BFR = 'bfr',
|
||||
LYRICS_MULTIPLE_STRUCTURED = 'lyricsMultipleStructured',
|
||||
LYRICS_SINGLE_STRUCTURED = 'lyricsSingleStructured',
|
||||
PLAYLISTS_SMART = 'playlistsSmart',
|
||||
PUBLIC_PLAYLIST = 'publicPlaylist',
|
||||
SHARING_ALBUM_SONG = 'sharingAlbumSong',
|
||||
TAGS = 'tags',
|
||||
}
|
||||
|
||||
export type ServerFeatures = Partial<Record<ServerFeature, number[]>>;
|
||||
245
src/shared/types/types.ts
Normal file
245
src/shared/types/types.ts
Normal file
|
|
@ -0,0 +1,245 @@
|
|||
import { AppRoute } from '@ts-rest/core';
|
||||
import { ReactNode } from 'react';
|
||||
import { Song } from 'src/main/features/core/lyrics/netease';
|
||||
|
||||
import {
|
||||
Album,
|
||||
AlbumArtist,
|
||||
Artist,
|
||||
LibraryItem,
|
||||
Playlist,
|
||||
QueueSong,
|
||||
} from '/@/shared/types/domain-types';
|
||||
import { ServerFeatures } from '/@/shared/types/features-types';
|
||||
|
||||
export enum ListDisplayType {
|
||||
CARD = 'card',
|
||||
POSTER = 'poster',
|
||||
TABLE = 'table',
|
||||
TABLE_PAGINATED = 'paginatedTable',
|
||||
}
|
||||
|
||||
export enum Platform {
|
||||
LINUX = 'linux',
|
||||
MACOS = 'macos',
|
||||
WEB = 'web',
|
||||
WINDOWS = 'windows',
|
||||
}
|
||||
|
||||
export enum ServerType {
|
||||
JELLYFIN = 'jellyfin',
|
||||
NAVIDROME = 'navidrome',
|
||||
SUBSONIC = 'subsonic',
|
||||
}
|
||||
|
||||
export type CardRoute = {
|
||||
route: AppRoute | string;
|
||||
slugs?: RouteSlug[];
|
||||
};
|
||||
|
||||
export type CardRow<T> = {
|
||||
arrayProperty?: string;
|
||||
format?: (value: T) => ReactNode;
|
||||
property: keyof T;
|
||||
route?: CardRoute;
|
||||
};
|
||||
|
||||
export type RouteSlug = {
|
||||
idProperty: string;
|
||||
slugProperty: string;
|
||||
};
|
||||
|
||||
export type TablePagination = {
|
||||
currentPage: number;
|
||||
itemsPerPage: number;
|
||||
totalItems: number;
|
||||
totalPages: number;
|
||||
};
|
||||
|
||||
export type TableType =
|
||||
| 'albumDetail'
|
||||
| 'fullScreen'
|
||||
| 'nowPlaying'
|
||||
| 'sideDrawerQueue'
|
||||
| 'sideQueue'
|
||||
| 'songs';
|
||||
|
||||
export const toServerType = (value?: string): null | ServerType => {
|
||||
switch (value?.toLowerCase()) {
|
||||
case ServerType.JELLYFIN:
|
||||
return ServerType.JELLYFIN;
|
||||
case ServerType.NAVIDROME:
|
||||
return ServerType.NAVIDROME;
|
||||
case ServerType.SUBSONIC:
|
||||
return ServerType.SUBSONIC;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export enum AuthState {
|
||||
INVALID = 'invalid',
|
||||
LOADING = 'loading',
|
||||
VALID = 'valid',
|
||||
}
|
||||
|
||||
export enum CrossfadeStyle {
|
||||
CONSTANT_POWER = 'constantPower',
|
||||
CONSTANT_POWER_SLOW_CUT = 'constantPowerSlowCut',
|
||||
CONSTANT_POWER_SLOW_FADE = 'constantPowerSlowFade',
|
||||
DIPPED = 'dipped',
|
||||
EQUALPOWER = 'equalPower',
|
||||
LINEAR = 'linear',
|
||||
}
|
||||
|
||||
export enum FontType {
|
||||
BUILT_IN = 'builtIn',
|
||||
CUSTOM = 'custom',
|
||||
SYSTEM = 'system',
|
||||
}
|
||||
|
||||
export enum Play {
|
||||
LAST = 'last',
|
||||
NEXT = 'next',
|
||||
NOW = 'now',
|
||||
SHUFFLE = 'shuffle',
|
||||
}
|
||||
|
||||
export enum PlaybackStyle {
|
||||
CROSSFADE = 'crossfade',
|
||||
GAPLESS = 'gapless',
|
||||
}
|
||||
|
||||
export enum PlaybackType {
|
||||
LOCAL = 'local',
|
||||
WEB = 'web',
|
||||
}
|
||||
|
||||
export enum PlayerRepeat {
|
||||
ALL = 'all',
|
||||
NONE = 'none',
|
||||
ONE = 'one',
|
||||
}
|
||||
|
||||
export enum PlayerShuffle {
|
||||
ALBUM = 'album',
|
||||
NONE = 'none',
|
||||
TRACK = 'track',
|
||||
}
|
||||
|
||||
export enum PlayerStatus {
|
||||
PAUSED = 'paused',
|
||||
PLAYING = 'playing',
|
||||
}
|
||||
|
||||
export enum TableColumn {
|
||||
ACTIONS = 'actions',
|
||||
ALBUM = 'album',
|
||||
ALBUM_ARTIST = 'albumArtist',
|
||||
ALBUM_COUNT = 'albumCount',
|
||||
ARTIST = 'artist',
|
||||
BIOGRAPHY = 'biography',
|
||||
BIT_RATE = 'bitRate',
|
||||
BPM = 'bpm',
|
||||
CHANNELS = 'channels',
|
||||
CODEC = 'codec',
|
||||
COMMENT = 'comment',
|
||||
DATE_ADDED = 'dateAdded',
|
||||
DISC_NUMBER = 'discNumber',
|
||||
DURATION = 'duration',
|
||||
GENRE = 'genre',
|
||||
LAST_PLAYED = 'lastPlayedAt',
|
||||
OWNER = 'username',
|
||||
PATH = 'path',
|
||||
PLAY_COUNT = 'playCount',
|
||||
RELEASE_DATE = 'releaseDate',
|
||||
ROW_INDEX = 'rowIndex',
|
||||
SIZE = 'size',
|
||||
SKIP = 'skip',
|
||||
SONG_COUNT = 'songCount',
|
||||
TITLE = 'title',
|
||||
TITLE_COMBINED = 'titleCombined',
|
||||
TRACK_NUMBER = 'trackNumber',
|
||||
USER_FAVORITE = 'userFavorite',
|
||||
USER_RATING = 'userRating',
|
||||
YEAR = 'releaseYear',
|
||||
}
|
||||
|
||||
export type GridCardData = {
|
||||
cardControls: any;
|
||||
cardRows: CardRow<Album | AlbumArtist | Artist | Playlist | Song>[];
|
||||
columnCount: number;
|
||||
display: ListDisplayType;
|
||||
handleFavorite: (options: { id: string[]; isFavorite: boolean; itemType: LibraryItem }) => void;
|
||||
handlePlayQueueAdd: (options: PlayQueueAddOptions) => void;
|
||||
itemCount: number;
|
||||
itemData: any[];
|
||||
itemGap: number;
|
||||
itemHeight: number;
|
||||
itemType: LibraryItem;
|
||||
itemWidth: number;
|
||||
playButtonBehavior: Play;
|
||||
resetInfiniteLoaderCache: () => void;
|
||||
route: CardRoute;
|
||||
};
|
||||
|
||||
export type PlayQueueAddOptions = {
|
||||
byData?: QueueSong[];
|
||||
byItemType?: {
|
||||
id: string[];
|
||||
type: LibraryItem;
|
||||
};
|
||||
initialIndex?: number;
|
||||
initialSongId?: string;
|
||||
playType: Play;
|
||||
query?: Record<string, any>;
|
||||
};
|
||||
|
||||
export type QueryBuilderGroup = {
|
||||
group: QueryBuilderGroup[];
|
||||
rules: QueryBuilderRule[];
|
||||
type: 'all' | 'any';
|
||||
uniqueId: string;
|
||||
};
|
||||
|
||||
export type QueryBuilderRule = {
|
||||
field?: null | string;
|
||||
operator?: null | string;
|
||||
uniqueId: string;
|
||||
value?: any | Date | null | number | string | undefined;
|
||||
};
|
||||
|
||||
export type ServerListItem = {
|
||||
credential: string;
|
||||
features?: ServerFeatures;
|
||||
id: string;
|
||||
name: string;
|
||||
ndCredential?: string;
|
||||
savePassword?: boolean;
|
||||
type: ServerType;
|
||||
url: string;
|
||||
userId: null | string;
|
||||
username: string;
|
||||
version?: string;
|
||||
};
|
||||
|
||||
export type SongState = {
|
||||
position?: number;
|
||||
repeat?: PlayerRepeat;
|
||||
shuffle?: boolean;
|
||||
song?: QueueSong;
|
||||
status?: PlayerStatus;
|
||||
/** This volume is in range 0-100 */
|
||||
volume?: number;
|
||||
};
|
||||
|
||||
export type TitleTheme = 'dark' | 'light' | 'system';
|
||||
|
||||
export interface UniqueId {
|
||||
uniqueId: string;
|
||||
}
|
||||
|
||||
export type WebAudio = {
|
||||
context: AudioContext;
|
||||
gain: GainNode;
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue