move all domain types to separate files

This commit is contained in:
jeffvli
2025-07-07 21:08:52 -07:00
parent 7785874605
commit 1c22461ee4
19 changed files with 706 additions and 584 deletions
-517
View File
@@ -1,517 +0,0 @@
import orderBy from 'lodash/orderBy';
import reverse from 'lodash/reverse';
import shuffle from 'lodash/shuffle';
import {
AlbumArtistDetailArgs,
AlbumArtistDetailResponse,
AlbumArtistListArgs,
AlbumArtistListResponse,
AlbumArtistListSort,
ArtistListArgs,
ArtistListResponse,
ArtistListSort,
} from './domain/artist-domain-types';
import { JFSortOrder } from '/@/shared/api/jellyfin.types';
import { NDSortOrder } from '/@/shared/api/navidrome.types';
import {
Album,
AlbumDetailArgs,
AlbumDetailResponse,
AlbumInfo,
AlbumListArgs,
AlbumListResponse,
AlbumListSort,
} from '/@/shared/types/domain/album-domain-types';
import { AlbumArtist, Artist } from '/@/shared/types/domain/artist-domain-types';
import { GenreListArgs, GenreListResponse } from '/@/shared/types/domain/genre-domain-types';
import {
LyricsArgs,
LyricsResponse,
StructuredLyric,
StructuredLyricsArgs,
} from '/@/shared/types/domain/lyric-domain-types';
import {
AddToPlaylistArgs,
AddToPlaylistResponse,
CreatePlaylistArgs,
CreatePlaylistResponse,
DeletePlaylistArgs,
DeletePlaylistResponse,
Playlist,
PlaylistDetailArgs,
PlaylistDetailResponse,
PlaylistListArgs,
PlaylistListResponse,
PlaylistSongListArgs,
RemoveFromPlaylistArgs,
RemoveFromPlaylistResponse,
UpdatePlaylistArgs,
UpdatePlaylistResponse,
} from '/@/shared/types/domain/playlist-domain-types';
import { SearchArgs, SearchResponse } from '/@/shared/types/domain/search-domain-types';
import {
ServerInfo,
ServerInfoArgs,
ServerListItem,
ServerMusicFolderListArgs,
ServerMusicFolderListResponse,
} from '/@/shared/types/domain/server-domain-types';
import { ListSortOrder } from '/@/shared/types/domain/shared-domain-types';
import {
RandomSongListArgs,
SimilarSongsArgs,
Song,
SongDetailArgs,
SongDetailResponse,
SongListArgs,
SongListResponse,
SongListSort,
TopSongListArgs,
TopSongListResponse,
} from '/@/shared/types/domain/song-domain-types';
import {
FavoriteArgs,
FavoriteResponse,
RatingResponse,
ScrobbleArgs,
ScrobbleResponse,
SetRatingArgs,
UserListArgs,
UserListResponse,
} from '/@/shared/types/domain/user-domain-types';
import { PlayerStatus } from '/@/shared/types/types';
export enum LibraryItem {
ALBUM = 'album',
ALBUM_ARTIST = 'albumArtist',
ARTIST = 'artist',
GENRE = 'genre',
PLAYLIST = 'playlist',
SONG = 'song',
}
export type AnyLibraryItem = Album | AlbumArtist | Artist | Playlist | QueueSong | Song;
export type AnyLibraryItems =
| Album[]
| AlbumArtist[]
| Artist[]
| Playlist[]
| QueueSong[]
| Song[];
export interface PlayerData {
current: {
index: number;
nextIndex?: number;
player: 1 | 2;
previousIndex?: number;
shuffledIndex: number;
song?: QueueSong;
status: PlayerStatus;
};
player1?: QueueSong;
player2?: QueueSong;
queue: QueueData;
}
export interface QueueData {
current?: QueueSong;
length: number;
next?: QueueSong;
previous?: QueueSong;
}
export type QueueSong = Song & {
uniqueId: string;
};
type SortOrderMap = {
jellyfin: Record<ListSortOrder, JFSortOrder>;
navidrome: Record<ListSortOrder, NDSortOrder>;
subsonic: Record<ListSortOrder, undefined>;
};
export const sortOrderMap: SortOrderMap = {
jellyfin: {
ASC: JFSortOrder.ASC,
DESC: JFSortOrder.DESC,
},
navidrome: {
ASC: NDSortOrder.ASC,
DESC: NDSortOrder.DESC,
},
subsonic: {
ASC: undefined,
DESC: undefined,
},
};
export enum ExternalSource {
LASTFM = 'LASTFM',
MUSICBRAINZ = 'MUSICBRAINZ',
SPOTIFY = 'SPOTIFY',
THEAUDIODB = 'THEAUDIODB',
}
export enum ExternalType {
ID = 'ID',
LINK = 'LINK',
}
export enum ImageType {
BACKDROP = 'BACKDROP',
LOGO = 'LOGO',
PRIMARY = 'PRIMARY',
SCREENSHOT = 'SCREENSHOT',
}
export enum Played {
All = 'all',
Never = 'never',
Played = 'played',
}
export type AuthenticationResponse = {
credential: string;
ndCredential?: string;
userId: null | string;
username: string;
};
export type BaseEndpointArgs = {
apiClientProps: {
server: null | ServerListItem;
signal?: AbortSignal;
};
};
export interface BasePaginatedResponse<T> {
error?: any | string;
items: T;
startIndex: number;
totalRecordCount: null | number;
}
export interface BaseQuery<T> {
sortBy: T;
sortOrder: ListSortOrder;
}
export type EndpointDetails = {
server: ServerListItem;
};
export type GainInfo = {
album?: number;
track?: number;
};
export type ShareItemArgs = BaseEndpointArgs & { body: ShareItemBody; serverId?: string };
export type ShareItemBody = {
description: string;
downloadable: boolean;
expires: number;
resourceIds: string;
resourceType: string;
};
export type ShareItemResponse = undefined | { id: string };
export const instanceOfCancellationError = (error: any) => {
return 'revert' in error;
};
export enum LyricSource {
GENIUS = 'Genius',
LRCLIB = 'lrclib.net',
NETEASE = 'NetEase',
}
export type ControllerEndpoint = {
addToPlaylist: (args: AddToPlaylistArgs) => Promise<AddToPlaylistResponse>;
authenticate: (
url: string,
body: { legacy?: boolean; password: string; username: string },
) => Promise<AuthenticationResponse>;
createFavorite: (args: FavoriteArgs) => Promise<FavoriteResponse>;
createPlaylist: (args: CreatePlaylistArgs) => Promise<CreatePlaylistResponse>;
deleteFavorite: (args: FavoriteArgs) => Promise<FavoriteResponse>;
deletePlaylist: (args: DeletePlaylistArgs) => Promise<DeletePlaylistResponse>;
getAlbumArtistDetail: (args: AlbumArtistDetailArgs) => Promise<AlbumArtistDetailResponse>;
getAlbumArtistList: (args: AlbumArtistListArgs) => Promise<AlbumArtistListResponse>;
getAlbumArtistListCount: (args: AlbumArtistListArgs) => Promise<number>;
getAlbumDetail: (args: AlbumDetailArgs) => Promise<AlbumDetailResponse>;
getAlbumInfo?: (args: AlbumDetailArgs) => Promise<AlbumInfo>;
getAlbumList: (args: AlbumListArgs) => Promise<AlbumListResponse>;
getAlbumListCount: (args: AlbumListArgs) => Promise<number>;
// getArtistInfo?: (args: any) => void;
getArtistList: (args: ArtistListArgs) => Promise<ArtistListResponse>;
getArtistListCount: (args: ArtistListArgs) => Promise<number>;
getDownloadUrl: (args: DownloadArgs) => string;
getGenreList: (args: GenreListArgs) => Promise<GenreListResponse>;
getLyrics?: (args: LyricsArgs) => Promise<LyricsResponse>;
getMusicFolderList: (args: ServerMusicFolderListArgs) => Promise<ServerMusicFolderListResponse>;
getPlaylistDetail: (args: PlaylistDetailArgs) => Promise<PlaylistDetailResponse>;
getPlaylistList: (args: PlaylistListArgs) => Promise<PlaylistListResponse>;
getPlaylistListCount: (args: PlaylistListArgs) => Promise<number>;
getPlaylistSongList: (args: PlaylistSongListArgs) => Promise<SongListResponse>;
getRandomSongList: (args: RandomSongListArgs) => Promise<SongListResponse>;
getRoles: (args: BaseEndpointArgs) => Promise<Array<string | { label: string; value: string }>>;
getServerInfo: (args: ServerInfoArgs) => Promise<ServerInfo>;
getSimilarSongs: (args: SimilarSongsArgs) => Promise<Song[]>;
getSongDetail: (args: SongDetailArgs) => Promise<SongDetailResponse>;
getSongList: (args: SongListArgs) => Promise<SongListResponse>;
getSongListCount: (args: SongListArgs) => Promise<number>;
getStructuredLyrics?: (args: StructuredLyricsArgs) => Promise<StructuredLyric[]>;
getTags?: (args: TagArgs) => Promise<TagResponses>;
getTopSongs: (args: TopSongListArgs) => Promise<TopSongListResponse>;
getTranscodingUrl: (args: TranscodingArgs) => string;
getUserList?: (args: UserListArgs) => Promise<UserListResponse>;
movePlaylistItem?: (args: MoveItemArgs) => Promise<void>;
removeFromPlaylist: (args: RemoveFromPlaylistArgs) => Promise<RemoveFromPlaylistResponse>;
scrobble: (args: ScrobbleArgs) => Promise<ScrobbleResponse>;
search: (args: SearchArgs) => Promise<SearchResponse>;
setRating?: (args: SetRatingArgs) => Promise<RatingResponse>;
shareItem?: (args: ShareItemArgs) => Promise<ShareItemResponse>;
updatePlaylist: (args: UpdatePlaylistArgs) => Promise<UpdatePlaylistResponse>;
};
export type DownloadArgs = BaseEndpointArgs & {
query: DownloadQuery;
};
export type DownloadQuery = {
id: string;
};
// This type from https://wicg.github.io/local-font-access/#fontdata
// NOTE: it is still experimental, so this should be updates as appropriate
export type FontData = {
family: string;
fullName: string;
postscriptName: string;
style: string;
};
export type MoveItemArgs = BaseEndpointArgs & {
query: MoveItemQuery;
};
export type MoveItemQuery = {
endingIndex: number;
playlistId: string;
startingIndex: number;
trackId: string;
};
export type Tag = {
name: string;
options: string[];
};
export type TagArgs = BaseEndpointArgs & {
query: TagQuery;
};
export type TagQuery = {
folder?: string;
type: LibraryItem.ALBUM | LibraryItem.SONG;
};
export type TagResponses = {
boolTags?: string[];
enumTags?: Tag[];
};
export type TranscodingArgs = BaseEndpointArgs & {
query: TranscodingQuery;
};
export type TranscodingQuery = {
base: string;
bitrate?: number;
format?: string;
};
export const sortAlbumList = (albums: Album[], sortBy: AlbumListSort, sortOrder: ListSortOrder) => {
let results = albums;
const order = sortOrder === ListSortOrder.ASC ? 'asc' : 'desc';
switch (sortBy) {
case AlbumListSort.ALBUM_ARTIST:
results = orderBy(
results,
['albumArtist', (v) => v.name.toLowerCase()],
[order, 'asc'],
);
break;
case AlbumListSort.DURATION:
results = orderBy(results, ['duration'], [order]);
break;
case AlbumListSort.FAVORITED:
results = orderBy(results, ['starred'], [order]);
break;
case AlbumListSort.NAME:
results = orderBy(results, [(v) => v.name.toLowerCase()], [order]);
break;
case AlbumListSort.PLAY_COUNT:
results = orderBy(results, ['playCount'], [order]);
break;
case AlbumListSort.RANDOM:
results = shuffle(results);
break;
case AlbumListSort.RATING:
results = orderBy(results, ['userRating'], [order]);
break;
case AlbumListSort.RECENTLY_ADDED:
results = orderBy(results, ['createdAt'], [order]);
break;
case AlbumListSort.RECENTLY_PLAYED:
results = orderBy(results, ['lastPlayedAt'], [order]);
break;
case AlbumListSort.SONG_COUNT:
results = orderBy(results, ['songCount'], [order]);
break;
case AlbumListSort.YEAR:
results = orderBy(results, ['releaseYear'], [order]);
break;
default:
break;
}
return results;
};
export const sortSongList = (
songs: QueueSong[],
sortBy: SongListSort,
sortOrder: ListSortOrder,
) => {
let results = songs;
const order = sortOrder === ListSortOrder.ASC ? 'asc' : 'desc';
switch (sortBy) {
case SongListSort.ALBUM:
results = orderBy(
results,
[(v) => v.album?.toLowerCase(), 'discNumber', 'trackNumber'],
[order, 'asc', 'asc'],
);
break;
case SongListSort.ALBUM_ARTIST:
results = orderBy(
results,
['albumArtist', (v) => v.album?.toLowerCase(), 'discNumber', 'trackNumber'],
[order, order, 'asc', 'asc'],
);
break;
case SongListSort.ARTIST:
results = orderBy(
results,
['artist', (v) => v.album?.toLowerCase(), 'discNumber', 'trackNumber'],
[order, order, 'asc', 'asc'],
);
break;
case SongListSort.DURATION:
results = orderBy(results, ['duration'], [order]);
break;
case SongListSort.FAVORITED:
results = orderBy(results, ['userFavorite', (v) => v.name.toLowerCase()], [order]);
break;
case SongListSort.GENRE:
results = orderBy(
results,
[
(v) => v.genres?.[0].name.toLowerCase(),
(v) => v.album?.toLowerCase(),
'discNumber',
'trackNumber',
],
[order, order, 'asc', 'asc'],
);
break;
case SongListSort.ID:
if (order === 'desc') {
results = reverse(results as any);
}
break;
case SongListSort.NAME:
results = orderBy(results, [(v) => v.name.toLowerCase()], [order]);
break;
case SongListSort.PLAY_COUNT:
results = orderBy(results, ['playCount'], [order]);
break;
case SongListSort.RANDOM:
results = shuffle(results);
break;
case SongListSort.RATING:
results = orderBy(results, ['userRating', (v) => v.name.toLowerCase()], [order]);
break;
case SongListSort.RECENTLY_ADDED:
results = orderBy(results, ['created'], [order]);
break;
case SongListSort.YEAR:
results = orderBy(
results,
['year', (v) => v.album?.toLowerCase(), 'discNumber', 'track'],
[order, 'asc', 'asc', 'asc'],
);
break;
default:
break;
}
return results;
};
export const sortAlbumArtistList = (
artists: AlbumArtist[],
sortBy: AlbumArtistListSort | ArtistListSort,
sortOrder: ListSortOrder,
) => {
const order = sortOrder === ListSortOrder.ASC ? 'asc' : 'desc';
let results = artists;
switch (sortBy) {
case AlbumArtistListSort.ALBUM_COUNT:
results = orderBy(artists, ['albumCount', (v) => v.name.toLowerCase()], [order, 'asc']);
break;
case AlbumArtistListSort.FAVORITED:
results = orderBy(artists, ['starred'], [order]);
break;
case AlbumArtistListSort.NAME:
results = orderBy(artists, [(v) => v.name.toLowerCase()], [order]);
break;
case AlbumArtistListSort.RATING:
results = orderBy(artists, ['userRating'], [order]);
break;
default:
break;
}
return results;
};
+78 -12
View File
@@ -1,6 +1,7 @@
import i18n from 'src/i18n/i18n';
import { orderBy, shuffle } from 'lodash';
import { z } from 'zod';
import i18n from '/@/i18n/i18n';
import { JFAlbumListSort } from '/@/shared/api/jellyfin.types';
import { jfType } from '/@/shared/api/jellyfin/jellyfin-types';
import { NDAlbumListSort } from '/@/shared/api/navidrome.types';
@@ -9,11 +10,11 @@ import {
BaseEndpointArgs,
BasePaginatedResponse,
BaseQuery,
LibraryItem,
} from '/@/shared/types/domain-types';
} from '/@/shared/types/domain/api-domain-types';
import { RelatedArtist } from '/@/shared/types/domain/artist-domain-types';
import { Genre } from '/@/shared/types/domain/genre-domain-types';
import { ServerType } from '/@/shared/types/domain/server-domain-types';
import { LibraryItem, ListSortOrder } from '/@/shared/types/domain/shared-domain-types';
import { Song } from '/@/shared/types/domain/song-domain-types';
export enum AlbumListSortOptions {
@@ -51,6 +52,7 @@ export const AlbumListSortOptionsLabels = {
[AlbumListSortOptions.TRACK_COUNT]: i18n.t('filter.trackCount'),
[AlbumListSortOptions.YEAR]: i18n.t('filter.year'),
};
export enum AlbumListSort {
ALBUM_ARTIST = 'albumArtist',
ARTIST = 'artist',
@@ -87,7 +89,6 @@ export interface AlbumListQuery extends BaseQuery<AlbumListSort> {
searchTerm?: string;
startIndex: number;
}
// Album List
export type AlbumListResponse = BasePaginatedResponse<Album[]> | null | undefined;
type AlbumListSortMap = {
@@ -150,13 +151,20 @@ export const albumListSortMap: AlbumListSortMap = {
year: undefined,
},
};
export type Album = {
albumArtist: string;
albumArtists: RelatedArtist[];
artists: RelatedArtist[];
backdropImageUrl: null | string;
comment: null | string;
createdAt: string;
createdDate: string;
description: null | string;
discTitles: {
disc: number;
title: string;
}[];
displayArtist: null | string;
duration: null | number;
genres: Genre[];
id: string;
@@ -164,33 +172,91 @@ export type Album = {
imageUrl: null | string;
isCompilation: boolean | null;
itemType: LibraryItem.ALBUM;
lastPlayedAt: null | string;
mbzId: null | string;
mbzAlbumId: null | string;
mbzReleaseGroupId: null | string;
missing: boolean;
name: string;
originalDate: null | string;
originalReleaseDate: null | string;
participants: null | Record<string, RelatedArtist[]>;
playCount: null | number;
releaseDate: null | string;
releaseTypes: {
id: string;
name: string;
}[];
releaseYear: null | number;
serverId: string;
serverType: ServerType;
size: null | number;
songCount: null | number;
songs?: Song[];
tags: null | Record<string, string[]>;
sortName: string;
tags: Record<string, string[]>;
uniqueId: string;
updatedAt: string;
updatedDate: string;
userFavorite: boolean;
userFavoriteDate: null | string;
userLastPlayedDate: null | string;
userPlayCount: null | number;
userRating: null | number;
} & { songs?: Song[] };
export type AlbumDetailArgs = BaseEndpointArgs & { query: AlbumDetailQuery };
// Album Detail
export type AlbumDetailQuery = { id: string };
export type AlbumDetailResponse = Album | null | undefined;
export type AlbumInfo = {
imageUrl: null | string;
notes: null | string;
};
export const sortAlbumList = (albums: Album[], sortBy: AlbumListSort, sortOrder: ListSortOrder) => {
let results = albums;
const order = sortOrder === ListSortOrder.ASC ? 'asc' : 'desc';
switch (sortBy) {
case AlbumListSort.ALBUM_ARTIST:
results = orderBy(
results,
['albumArtist', (v) => v.name.toLowerCase()],
[order, 'asc'],
);
break;
case AlbumListSort.DURATION:
results = orderBy(results, ['duration'], [order]);
break;
case AlbumListSort.FAVORITED:
results = orderBy(results, ['starred'], [order]);
break;
case AlbumListSort.NAME:
results = orderBy(results, [(v) => v.name.toLowerCase()], [order]);
break;
case AlbumListSort.PLAY_COUNT:
results = orderBy(results, ['playCount'], [order]);
break;
case AlbumListSort.RANDOM:
results = shuffle(results);
break;
case AlbumListSort.RATING:
results = orderBy(results, ['userRating'], [order]);
break;
case AlbumListSort.RECENTLY_ADDED:
results = orderBy(results, ['createdAt'], [order]);
break;
case AlbumListSort.RECENTLY_PLAYED:
results = orderBy(results, ['lastPlayedAt'], [order]);
break;
case AlbumListSort.SONG_COUNT:
results = orderBy(results, ['songCount'], [order]);
break;
case AlbumListSort.YEAR:
results = orderBy(results, ['releaseYear'], [order]);
break;
default:
break;
}
return results;
};
+151
View File
@@ -0,0 +1,151 @@
import {
AlbumDetailArgs,
AlbumDetailResponse,
AlbumInfo,
AlbumListArgs,
AlbumListResponse,
} from '/@/shared/types/domain/album-domain-types';
import {
AlbumArtistDetailArgs,
AlbumArtistDetailResponse,
AlbumArtistListArgs,
AlbumArtistListResponse,
ArtistListArgs,
ArtistListResponse,
} from '/@/shared/types/domain/artist-domain-types';
import { AuthenticationResponse } from '/@/shared/types/domain/auth-domain-types';
import { GenreListArgs, GenreListResponse } from '/@/shared/types/domain/genre-domain-types';
import {
LyricsArgs,
LyricsResponse,
StructuredLyric,
StructuredLyricsArgs,
} from '/@/shared/types/domain/lyric-domain-types';
import { TranscodingArgs } from '/@/shared/types/domain/player-domain-types';
import {
AddToPlaylistArgs,
AddToPlaylistResponse,
CreatePlaylistArgs,
CreatePlaylistResponse,
DeletePlaylistArgs,
DeletePlaylistResponse,
MoveItemArgs,
PlaylistDetailArgs,
PlaylistDetailResponse,
PlaylistListArgs,
PlaylistListResponse,
PlaylistSongListArgs,
RemoveFromPlaylistArgs,
RemoveFromPlaylistResponse,
UpdatePlaylistArgs,
UpdatePlaylistResponse,
} from '/@/shared/types/domain/playlist-domain-types';
import { SearchArgs, SearchResponse } from '/@/shared/types/domain/search-domain-types';
import {
ServerInfo,
ServerInfoArgs,
ServerListItem,
ServerMusicFolderListArgs,
ServerMusicFolderListResponse,
} from '/@/shared/types/domain/server-domain-types';
import { ListSortOrder } from '/@/shared/types/domain/shared-domain-types';
import {
RandomSongListArgs,
SimilarSongsArgs,
Song,
SongDetailArgs,
SongDetailResponse,
SongListArgs,
SongListResponse,
TopSongListArgs,
TopSongListResponse,
} from '/@/shared/types/domain/song-domain-types';
import { TagArgs, TagsResponse } from '/@/shared/types/domain/tag-domain-types';
import {
DownloadArgs,
FavoriteArgs,
FavoriteResponse,
RatingResponse,
ScrobbleArgs,
ScrobbleResponse,
SetRatingArgs,
ShareItemArgs,
ShareItemResponse,
UserListArgs,
UserListResponse,
} from '/@/shared/types/domain/user-domain-types';
export type BaseEndpointArgs = {
apiClientProps: {
server: null | ServerListItem;
signal?: AbortSignal;
};
};
export interface BasePaginatedResponse<T> {
error?: any | string;
items: T;
startIndex: number;
totalRecordCount: null | number;
}
export interface BaseQuery<T> {
sortBy: T;
sortOrder: ListSortOrder;
}
export type EndpointDetails = {
server: ServerListItem;
};
export const instanceOfCancellationError = (error: any) => {
return 'revert' in error;
};
export type ControllerEndpoint = {
addToPlaylist: (args: AddToPlaylistArgs) => Promise<AddToPlaylistResponse>;
authenticate: (
url: string,
body: { legacy?: boolean; password: string; username: string },
) => Promise<AuthenticationResponse>;
createFavorite: (args: FavoriteArgs) => Promise<FavoriteResponse>;
createPlaylist: (args: CreatePlaylistArgs) => Promise<CreatePlaylistResponse>;
deleteFavorite: (args: FavoriteArgs) => Promise<FavoriteResponse>;
deletePlaylist: (args: DeletePlaylistArgs) => Promise<DeletePlaylistResponse>;
getAlbumArtistDetail: (args: AlbumArtistDetailArgs) => Promise<AlbumArtistDetailResponse>;
getAlbumArtistList: (args: AlbumArtistListArgs) => Promise<AlbumArtistListResponse>;
getAlbumArtistListCount: (args: AlbumArtistListArgs) => Promise<number>;
getAlbumDetail: (args: AlbumDetailArgs) => Promise<AlbumDetailResponse>;
getAlbumInfo?: (args: AlbumDetailArgs) => Promise<AlbumInfo>;
getAlbumList: (args: AlbumListArgs) => Promise<AlbumListResponse>;
getAlbumListCount: (args: AlbumListArgs) => Promise<number>;
getArtistList: (args: ArtistListArgs) => Promise<ArtistListResponse>;
getArtistListCount: (args: ArtistListArgs) => Promise<number>;
getDownloadUrl: (args: DownloadArgs) => string;
getGenreList: (args: GenreListArgs) => Promise<GenreListResponse>;
getLyrics?: (args: LyricsArgs) => Promise<LyricsResponse>;
getMusicFolderList: (args: ServerMusicFolderListArgs) => Promise<ServerMusicFolderListResponse>;
getPlaylistDetail: (args: PlaylistDetailArgs) => Promise<PlaylistDetailResponse>;
getPlaylistList: (args: PlaylistListArgs) => Promise<PlaylistListResponse>;
getPlaylistListCount: (args: PlaylistListArgs) => Promise<number>;
getPlaylistSongList: (args: PlaylistSongListArgs) => Promise<SongListResponse>;
getRandomSongList: (args: RandomSongListArgs) => Promise<SongListResponse>;
getRoles: (args: BaseEndpointArgs) => Promise<Array<string | { label: string; value: string }>>;
getServerInfo: (args: ServerInfoArgs) => Promise<ServerInfo>;
getSimilarSongs: (args: SimilarSongsArgs) => Promise<Song[]>;
getSongDetail: (args: SongDetailArgs) => Promise<SongDetailResponse>;
getSongList: (args: SongListArgs) => Promise<SongListResponse>;
getSongListCount: (args: SongListArgs) => Promise<number>;
getStructuredLyrics?: (args: StructuredLyricsArgs) => Promise<StructuredLyric[]>;
getTags?: (args: TagArgs) => Promise<TagsResponse>;
getTopSongs: (args: TopSongListArgs) => Promise<TopSongListResponse>;
getTranscodingUrl: (args: TranscodingArgs) => string;
getUserList?: (args: UserListArgs) => Promise<UserListResponse>;
movePlaylistItem?: (args: MoveItemArgs) => Promise<void>;
removeFromPlaylist: (args: RemoveFromPlaylistArgs) => Promise<RemoveFromPlaylistResponse>;
scrobble: (args: ScrobbleArgs) => Promise<ScrobbleResponse>;
search: (args: SearchArgs) => Promise<SearchResponse>;
setRating?: (args: SetRatingArgs) => Promise<RatingResponse>;
shareItem?: (args: ShareItemArgs) => Promise<ShareItemResponse>;
updatePlaylist: (args: UpdatePlaylistArgs) => Promise<UpdatePlaylistResponse>;
};
+36 -3
View File
@@ -1,6 +1,7 @@
import i18n from 'src/i18n/i18n';
import { orderBy } from 'lodash';
import { z } from 'zod';
import i18n from '/@/i18n/i18n';
import { JFAlbumArtistListSort, JFArtistListSort } from '/@/shared/api/jellyfin.types';
import { jfType } from '/@/shared/api/jellyfin/jellyfin-types';
import { NDAlbumArtistListSort } from '/@/shared/api/navidrome.types';
@@ -9,10 +10,10 @@ import {
BaseEndpointArgs,
BasePaginatedResponse,
BaseQuery,
LibraryItem,
} from '/@/shared/types/domain-types';
} from '/@/shared/types/domain/api-domain-types';
import { Genre } from '/@/shared/types/domain/genre-domain-types';
import { ServerType } from '/@/shared/types/domain/server-domain-types';
import { LibraryItem, ListSortOrder } from '/@/shared/types/domain/shared-domain-types';
export enum ArtistListSortOptions {
ALBUM_COUNT = 'albumCount',
@@ -243,3 +244,35 @@ export type ArtistInfoQuery = {
limit: number;
musicFolderId?: string;
};
export const sortAlbumArtistList = (
artists: AlbumArtist[],
sortBy: AlbumArtistListSort | ArtistListSort,
sortOrder: ListSortOrder,
) => {
const order = sortOrder === ListSortOrder.ASC ? 'asc' : 'desc';
let results = artists;
switch (sortBy) {
case AlbumArtistListSort.ALBUM_COUNT:
results = orderBy(artists, ['albumCount', (v) => v.name.toLowerCase()], [order, 'asc']);
break;
case AlbumArtistListSort.FAVORITED:
results = orderBy(artists, ['starred'], [order]);
break;
case AlbumArtistListSort.NAME:
results = orderBy(artists, [(v) => v.name.toLowerCase()], [order]);
break;
case AlbumArtistListSort.RATING:
results = orderBy(artists, ['userRating'], [order]);
break;
default:
break;
}
return results;
};
@@ -0,0 +1,6 @@
export type AuthenticationResponse = {
credential: string;
ndCredential?: string;
userId: null | string;
username: string;
};
@@ -0,0 +1,11 @@
export enum ExternalSource {
LASTFM = 'LASTFM',
MUSICBRAINZ = 'MUSICBRAINZ',
SPOTIFY = 'SPOTIFY',
THEAUDIODB = 'THEAUDIODB',
}
export enum ExternalType {
ID = 'ID',
LINK = 'LINK',
}
@@ -1,15 +1,13 @@
import i18n from 'src/i18n/i18n';
import { UserListSort } from './user-domain-types';
import i18n from '/@/i18n/i18n';
import { JFGenreListSort } from '/@/shared/api/jellyfin.types';
import { NDGenreListSort } from '/@/shared/api/navidrome.types';
import {
BaseEndpointArgs,
BasePaginatedResponse,
BaseQuery,
LibraryItem,
} from '/@/shared/types/domain-types';
} from '/@/shared/types/domain/api-domain-types';
import { LibraryItem } from '/@/shared/types/domain/shared-domain-types';
import { UserListSort } from '/@/shared/types/domain/user-domain-types';
export enum GenreListSortOptions {
ALBUM_COUNT = 'albumCount',
@@ -22,6 +20,7 @@ export const GenreListSortOptionsLabels = {
[GenreListSortOptions.NAME]: i18n.t('filter.name'),
[GenreListSortOptions.TRACK_COUNT]: i18n.t('filter.trackCount'),
};
export type Genre = {
albumCount?: number;
id: string;
@@ -43,7 +42,6 @@ export interface GenreListQuery extends BaseQuery<GenreListSort> {
searchTerm?: string;
startIndex: number;
}
// Genre List
export type GenreListResponse = BasePaginatedResponse<Genre[]> | null | undefined;
@@ -0,0 +1,6 @@
export enum ImageType {
BACKDROP = 'BACKDROP',
LOGO = 'LOGO',
PRIMARY = 'PRIMARY',
SCREENSHOT = 'SCREENSHOT',
}
@@ -1,12 +1,17 @@
import { BaseEndpointArgs, LyricSource } from '/@/shared/types/domain-types';
import { BaseEndpointArgs } from './api-domain-types';
import { Song } from '/@/shared/types/domain/song-domain-types';
export enum LyricSource {
GENIUS = 'Genius',
LRCLIB = 'lrclib.net',
NETEASE = 'NetEase',
}
export type FullLyricsMetadata = Omit<InternetProviderLyricResponse, 'id' | 'lyrics' | 'source'> & {
lyrics: LyricsResponse;
remote: boolean;
source: string;
};
export type InternetProviderLyricResponse = {
artist: string;
id: string;
@@ -21,6 +26,7 @@ export type InternetProviderLyricSearchResponse = {
score?: number;
source: LyricSource;
};
export type LyricGetQuery = {
remoteSongId: string;
remoteSource: LyricSource;
@@ -32,7 +38,6 @@ export type LyricOverride = Omit<InternetProviderLyricResponse, 'lyrics'>;
export type LyricsArgs = BaseEndpointArgs & {
query: LyricsQuery;
};
export type LyricSearchQuery = {
album?: string;
artist?: string;
@@ -40,6 +45,7 @@ export type LyricSearchQuery = {
name?: string;
};
export type LyricsOverride = Omit<FullLyricsMetadata, 'lyrics'> & { id: string };
export type LyricsQuery = {
songId: string;
};
@@ -58,7 +64,6 @@ export type StructuredSyncedLyric = Omit<FullLyricsMetadata, 'lyrics'> & {
lyrics: SynchronizedLyricsArray;
synced: true;
};
export type StructuredUnsyncedLyric = Omit<FullLyricsMetadata, 'lyrics'> & {
lyrics: string;
synced: false;
@@ -0,0 +1,43 @@
import { BaseEndpointArgs } from '/@/shared/types/domain/api-domain-types';
import { Song } from '/@/shared/types/domain/song-domain-types';
import { PlayerStatus } from '/@/shared/types/types';
export enum Played {
All = 'all',
Never = 'never',
Played = 'played',
}
export interface PlayerData {
current: {
index: number;
nextIndex?: number;
player: 1 | 2;
previousIndex?: number;
shuffledIndex: number;
song?: QueueSong;
status: PlayerStatus;
};
player1?: QueueSong;
player2?: QueueSong;
queue: QueueData;
}
export interface QueueData {
current?: QueueSong;
length: number;
next?: QueueSong;
previous?: QueueSong;
}
export type QueueSong = Song & {
uniqueId: string;
};
export type TranscodingArgs = BaseEndpointArgs & {
query: TranscodingQuery;
};
export type TranscodingQuery = {
base: string;
bitrate?: number;
format?: string;
};
@@ -1,8 +1,6 @@
import i18n from 'src/i18n/i18n';
import { z } from 'zod';
import { Genre } from './genre-domain-types';
import i18n from '/@/i18n/i18n';
import { JFPlaylistListSort } from '/@/shared/api/jellyfin.types';
import { jfType } from '/@/shared/api/jellyfin/jellyfin-types';
import { NDPlaylistListSort } from '/@/shared/api/navidrome.types';
@@ -11,10 +9,10 @@ import {
BaseEndpointArgs,
BasePaginatedResponse,
BaseQuery,
LibraryItem,
} from '/@/shared/types/domain-types';
} from '/@/shared/types/domain/api-domain-types';
import { Genre } from '/@/shared/types/domain/genre-domain-types';
import { ServerType } from '/@/shared/types/domain/server-domain-types';
import { ListSortOrder } from '/@/shared/types/domain/shared-domain-types';
import { LibraryItem, ListSortOrder } from '/@/shared/types/domain/shared-domain-types';
import { Song, SongListSort } from '/@/shared/types/domain/song-domain-types';
export enum PlaylistListSortOptions {
@@ -191,6 +189,17 @@ export const playlistListSortMap: PlaylistListSortMap = {
updatedAt: undefined,
},
};
export type MoveItemArgs = BaseEndpointArgs & {
query: MoveItemQuery;
};
export type MoveItemQuery = {
endingIndex: number;
playlistId: string;
startingIndex: number;
trackId: string;
};
export type PlaylistDetailArgs = BaseEndpointArgs & { query: PlaylistDetailQuery };
export type PlaylistDetailQuery = {
@@ -200,7 +209,6 @@ export type PlaylistDetailQuery = {
export type PlaylistDetailResponse = Playlist;
export type PlaylistSongListArgs = BaseEndpointArgs & { query: PlaylistSongListQuery };
export type PlaylistSongListQuery = {
id: string;
limit?: number;
+113
View File
@@ -0,0 +1,113 @@
import { QueueSong } from '/@/shared/types/domain/player-domain-types';
import { PlayerRepeat, PlayerStatus, SongState } from '/@/shared/types/types';
export interface ClientAuth {
event: 'authenticate';
header: string;
}
export type ClientEvent =
| ClientAuth
| ClientFavorite
| ClientPosition
| ClientRating
| ClientSimpleEvent
| ClientVolume;
export interface ClientFavorite {
event: 'favorite';
favorite: boolean;
id: string;
}
export interface ClientPosition {
event: 'position';
position: number;
}
export interface ClientRating {
event: 'rating';
id: string;
rating: number;
}
export interface ClientSimpleEvent {
event: 'next' | 'pause' | 'play' | 'previous' | 'proxy' | 'repeat' | 'shuffle';
}
export interface ClientVolume {
event: 'volume';
volume: number;
}
export interface ServerError {
data: string;
event: 'error';
}
export type ServerEvent =
| ServerError
| ServerFavorite
| ServerPlayStatus
| ServerPosition
| ServerProxy
| ServerRating
| ServerRepeat
| ServerShuffle
| ServerSong
| ServerState
| ServerVolume;
export interface ServerFavorite {
data: { favorite: boolean; id: string };
event: 'favorite';
}
export interface ServerPlayStatus {
data: PlayerStatus;
event: 'playback';
}
export interface ServerPosition {
data: number;
event: 'position';
}
export interface ServerProxy {
data: string;
event: 'proxy';
}
export interface ServerRating {
data: { id: string; rating: number };
event: 'rating';
}
export interface ServerRepeat {
data: PlayerRepeat;
event: 'repeat';
}
export interface ServerShuffle {
data: boolean;
event: 'shuffle';
}
export interface ServerSong {
data: null | QueueSong;
event: 'song';
}
export interface ServerState {
data: SongState;
event: 'state';
}
export interface ServerVolume {
data: number;
event: 'volume';
}
export interface SongUpdateSocket extends Omit<SongState, 'song'> {
position?: number;
song?: null | QueueSong;
}
@@ -1,5 +1,5 @@
import { BaseEndpointArgs } from '/@/shared/types/domain-types';
import { Album } from '/@/shared/types/domain/album-domain-types';
import { BaseEndpointArgs } from '/@/shared/types/domain/api-domain-types';
import { AlbumArtist } from '/@/shared/types/domain/artist-domain-types';
import { Song } from '/@/shared/types/domain/song-domain-types';
+15 -2
View File
@@ -1,7 +1,6 @@
import i18n from 'src/i18n/i18n';
import { BaseEndpointArgs, BasePaginatedResponse } from '/@/shared/types/domain-types';
import { ServerFeatures } from '/@/shared/types/features-types';
import { BaseEndpointArgs, BasePaginatedResponse } from '/@/shared/types/domain/api-domain-types';
export enum ServerListSortOptions {
CREATED_AT = 'createdAt',
@@ -17,12 +16,24 @@ export const ServerListSortOptionsLabels = {
[ServerListSortOptions.UPDATED_AT]: i18n.t('filter.updatedAt'),
};
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 enum ServerType {
JELLYFIN = 'jellyfin',
NAVIDROME = 'navidrome',
SUBSONIC = 'subsonic',
}
export type ServerFeatures = Partial<Record<ServerFeature, number[]>>;
export type ServerInfo = {
features: ServerFeatures;
id?: string;
@@ -53,8 +64,10 @@ export type ServerMusicFolder = {
export type ServerMusicFolderListArgs = BaseEndpointArgs;
export type ServerMusicFolderListQuery = null;
export type ServerMusicFolderListResponse =
| BasePaginatedResponse<ServerMusicFolder[]>
| null
| undefined;
export type ServerMusicFoldersResponse = ServerMusicFolder[];
@@ -1,4 +1,58 @@
import { JFSortOrder } from '/@/shared/api/jellyfin.types';
import { NDSortOrder } from '/@/shared/api/navidrome.types';
import { Album } from '/@/shared/types/domain/album-domain-types';
import { AlbumArtist, Artist } from '/@/shared/types/domain/artist-domain-types';
import { QueueSong } from '/@/shared/types/domain/player-domain-types';
import { Playlist } from '/@/shared/types/domain/playlist-domain-types';
import { Song } from '/@/shared/types/domain/song-domain-types';
export enum LibraryItem {
ALBUM = 'album',
ALBUM_ARTIST = 'albumArtist',
ARTIST = 'artist',
GENRE = 'genre',
PLAYLIST = 'playlist',
SONG = 'song',
}
export enum ListSortOrder {
ASC = 'ASC',
DESC = 'DESC',
}
export type AnyLibraryItem = Album | AlbumArtist | Artist | Playlist | QueueSong | Song;
export type AnyLibraryItems =
| Album[]
| AlbumArtist[]
| Artist[]
| Playlist[]
| QueueSong[]
| Song[];
type SortOrderMap = {
jellyfin: Record<ListSortOrder, JFSortOrder>;
navidrome: Record<ListSortOrder, NDSortOrder>;
subsonic: Record<ListSortOrder, undefined>;
};
export const sortOrderMap: SortOrderMap = {
jellyfin: {
ASC: JFSortOrder.ASC,
DESC: JFSortOrder.DESC,
},
navidrome: {
ASC: NDSortOrder.ASC,
DESC: NDSortOrder.DESC,
},
subsonic: {
ASC: undefined,
DESC: undefined,
},
}; // This type from https://wicg.github.io/local-font-access/#fontdata
// NOTE: it is still experimental, so this should be updates as appropriate
export type FontData = {
family: string;
fullName: string;
postscriptName: string;
style: string;
};
+108 -9
View File
@@ -1,6 +1,7 @@
import i18n from 'src/i18n/i18n';
import { orderBy, reverse, shuffle } from 'lodash';
import { z } from 'zod';
import i18n from '/@/i18n/i18n';
import { JFSongListSort } from '/@/shared/api/jellyfin.types';
import { jfType } from '/@/shared/api/jellyfin/jellyfin-types';
import { NDSongListSort } from '/@/shared/api/navidrome.types';
@@ -9,13 +10,12 @@ import {
BaseEndpointArgs,
BasePaginatedResponse,
BaseQuery,
GainInfo,
LibraryItem,
Played,
} from '/@/shared/types/domain-types';
} from '/@/shared/types/domain/api-domain-types';
import { RelatedArtist } from '/@/shared/types/domain/artist-domain-types';
import { Genre } from '/@/shared/types/domain/genre-domain-types';
import { Played, QueueSong } from '/@/shared/types/domain/player-domain-types';
import { ServerType } from '/@/shared/types/domain/server-domain-types';
import { LibraryItem, ListSortOrder } from '/@/shared/types/domain/shared-domain-types';
export enum SongListSortOptions {
ALBUM = 'album',
@@ -212,6 +212,11 @@ export const songListSortMap: SongListSortMap = {
year: undefined,
},
};
export type GainInfo = {
album?: number;
track?: number;
};
export type RandomSongListArgs = BaseEndpointArgs & {
query: RandomSongListQuery;
};
@@ -224,8 +229,8 @@ export type RandomSongListQuery = {
musicFolderId?: string;
played: Played;
};
export type RandomSongListResponse = SongListResponse;
export type SimilarSongsArgs = BaseEndpointArgs & {
query: SimilarSongsQuery;
};
@@ -235,11 +240,11 @@ export type SimilarSongsQuery = {
count?: number;
songId: string;
};
export type SongDetailArgs = BaseEndpointArgs & { query: SongDetailQuery };
export type SongDetailQuery = { id: string };
export type SongDetailQuery = { id: string };
export type SongDetailResponse = null | Song | undefined;
export type TopSongListArgs = BaseEndpointArgs & { query: TopSongListQuery };
export type TopSongListQuery = {
@@ -247,5 +252,99 @@ export type TopSongListQuery = {
artistId: string;
limit?: number;
};
export type TopSongListResponse = BasePaginatedResponse<Song[]> | null | undefined;
export const sortSongList = (
songs: QueueSong[],
sortBy: SongListSort,
sortOrder: ListSortOrder,
) => {
let results = songs;
const order = sortOrder === ListSortOrder.ASC ? 'asc' : 'desc';
switch (sortBy) {
case SongListSort.ALBUM:
results = orderBy(
results,
[(v) => v.album?.toLowerCase(), 'discNumber', 'trackNumber'],
[order, 'asc', 'asc'],
);
break;
case SongListSort.ALBUM_ARTIST:
results = orderBy(
results,
['albumArtist', (v) => v.album?.toLowerCase(), 'discNumber', 'trackNumber'],
[order, order, 'asc', 'asc'],
);
break;
case SongListSort.ARTIST:
results = orderBy(
results,
['artist', (v) => v.album?.toLowerCase(), 'discNumber', 'trackNumber'],
[order, order, 'asc', 'asc'],
);
break;
case SongListSort.DURATION:
results = orderBy(results, ['duration'], [order]);
break;
case SongListSort.FAVORITED:
results = orderBy(results, ['userFavorite', (v) => v.name.toLowerCase()], [order]);
break;
case SongListSort.GENRE:
results = orderBy(
results,
[
(v) => v.genres?.[0].name.toLowerCase(),
(v) => v.album?.toLowerCase(),
'discNumber',
'trackNumber',
],
[order, order, 'asc', 'asc'],
);
break;
case SongListSort.ID:
if (order === 'desc') {
results = reverse(results as any);
}
break;
case SongListSort.NAME:
results = orderBy(results, [(v) => v.name.toLowerCase()], [order]);
break;
case SongListSort.PLAY_COUNT:
results = orderBy(results, ['playCount'], [order]);
break;
case SongListSort.RANDOM:
results = shuffle(results);
break;
case SongListSort.RATING:
results = orderBy(results, ['userRating', (v) => v.name.toLowerCase()], [order]);
break;
case SongListSort.RECENTLY_ADDED:
results = orderBy(results, ['created'], [order]);
break;
case SongListSort.YEAR:
results = orderBy(
results,
['year', (v) => v.album?.toLowerCase(), 'discNumber', 'track'],
[order, 'asc', 'asc', 'asc'],
);
break;
default:
break;
}
return results;
};
@@ -0,0 +1,21 @@
import { BaseEndpointArgs } from '/@/shared/types/domain/api-domain-types';
import { LibraryItem } from '/@/shared/types/domain/shared-domain-types';
export type Tag = {
name: string;
options: string[];
};
export type TagArgs = BaseEndpointArgs & {
query: TagQuery;
};
export type TagQuery = {
folder?: string;
type: LibraryItem.ALBUM | LibraryItem.SONG;
};
export type TagsResponse = {
boolTags?: string[];
enumTags?: Tag[];
};
+34 -9
View File
@@ -1,13 +1,11 @@
import i18n from 'src/i18n/i18n';
import i18n from '/@/i18n/i18n';
import { NDUserListSort } from '/@/shared/api/navidrome.types';
import {
AnyLibraryItems,
BaseEndpointArgs,
BasePaginatedResponse,
BaseQuery,
LibraryItem,
} from '/@/shared/types/domain-types';
import { RatingQuery } from '/@/shared/types/domain/user-domain-types';
import { NDUserListSort } from '/@/shared/api/navidrome.types';
} from '/@/shared/types/domain/api-domain-types';
import { AnyLibraryItems, LibraryItem } from '/@/shared/types/domain/shared-domain-types';
export enum UserListSortOptions {
CREATED_AT = 'createdAt',
@@ -22,6 +20,7 @@ export const UserListSortOptionsLabels = {
[UserListSortOptions.NAME]: i18n.t('filter.name'),
[UserListSortOptions.UPDATED_AT]: i18n.t('filter.updatedAt'),
};
export type FavoriteArgs = BaseEndpointArgs & { query: FavoriteQuery; serverId?: string };
export type FavoriteQuery = {
@@ -36,7 +35,9 @@ export type RatingQuery = {
};
export type RatingResponse = null | undefined;
export type SetRatingArgs = BaseEndpointArgs & { query: RatingQuery; serverId?: string };
export type UserListArgs = BaseEndpointArgs & { query: UserListQuery };
export interface UserListQuery extends BaseQuery<UserListSort> {
@@ -51,6 +52,7 @@ export interface UserListQuery extends BaseQuery<UserListSort> {
}
export type UserListResponse = BasePaginatedResponse<User[]> | null | undefined;
type UserListSortMap = {
jellyfin: Record<UserListSort, undefined>;
navidrome: Record<UserListSort, NDUserListSort | undefined>;
@@ -68,6 +70,19 @@ export const userListSortMap: UserListSortMap = {
name: undefined,
},
};
export enum UserListSort {
NAME = 'name',
}
export type DownloadArgs = BaseEndpointArgs & {
query: DownloadQuery;
};
export type DownloadQuery = {
id: string;
};
export type ScrobbleArgs = BaseEndpointArgs & {
query: ScrobbleQuery;
serverId?: string;
@@ -81,9 +96,19 @@ export type ScrobbleQuery = {
};
export type ScrobbleResponse = null | undefined;
export enum UserListSort {
NAME = 'name',
}
export type ShareItemArgs = BaseEndpointArgs & { body: ShareItemBody; serverId?: string };
export type ShareItemBody = {
description: string;
downloadable: boolean;
expires: number;
resourceIds: string;
resourceType: string;
};
export type ShareItemResponse = undefined | { id: string };
export type User = {
createdAt: null | string;
email: null | string;
-13
View File
@@ -1,13 +0,0 @@
// 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[]>>;