export type Credentials = { username: string; password: string }; export function isSuccess( authResult: AuthSuccess | AuthFailure ): authResult is AuthSuccess { return (authResult as AuthSuccess).authToken !== undefined; } export function isFailure( authResult: any | AuthFailure ): authResult is AuthFailure { return (authResult as AuthFailure).message !== undefined; } export type AuthSuccess = { authToken: string; userId: string; nickname: string; }; export type AuthFailure = { message: string; }; export type ArtistSummary = { id: string; name: string; }; export type Images = { small: string | undefined; medium: string | undefined; large: string | undefined; }; export const NO_IMAGES: Images = { small: undefined, medium: undefined, large: undefined, }; export type Artist = ArtistSummary & { image: Images albums: AlbumSummary[]; similarArtists: ArtistSummary[] }; export type AlbumSummary = { id: string; name: string; year: string | undefined; genre: Genre | undefined; }; export type Album = AlbumSummary & {}; export type Genre = { name: string; id: string; } export type Track = { id: string; name: string; mimeType: string; duration: number; number: number | undefined; genre: Genre | undefined; album: AlbumSummary; artist: ArtistSummary; }; export type Paging = { _index: number; _count: number; }; export type Result = { results: T[]; total: number; }; export function slice2({ _index, _count }: Paging) { return (things: T[]): [T[], number] => [ things.slice(_index, _index + _count), things.length, ]; } export const asResult = ([results, total]: [T[], number]) => ({ results, total, }); export type ArtistQuery = Paging; export type AlbumQueryType = 'alphabeticalByArtist' | 'byGenre' | 'random' | 'recent' | 'frequent' | 'newest' | 'starred'; export type AlbumQuery = Paging & { type: AlbumQueryType; genre?: string; }; export const artistToArtistSummary = (it: Artist): ArtistSummary => ({ id: it.id, name: it.name, }); export const albumToAlbumSummary = (it: Album): AlbumSummary => ({ id: it.id, name: it.name, year: it.year, genre: it.genre, }); export type StreamingHeader = "content-type" | "content-length" | "content-range" | "accept-ranges"; export type TrackStream = { status: number; headers: Record; stream: any; }; export type CoverArt = { contentType: string; data: Buffer; } export const range = (size: number) => [...Array(size).keys()]; export const asArtistAlbumPairs = (artists: Artist[]): [Artist, Album][] => artists.flatMap((artist) => artist.albums.map((album) => [artist, album] as [Artist, Album]) ); export interface MusicService { generateToken(credentials: Credentials): Promise; login(authToken: string): Promise; } export interface MusicLibrary { artists(q: ArtistQuery): Promise>; artist(id: string): Promise; albums(q: AlbumQuery): Promise>; album(id: string): Promise; tracks(albumId: string): Promise; track(trackId: string): Promise; genres(): Promise; stream({ trackId, range, }: { trackId: string; range: string | undefined; }): Promise; coverArt(id: string, type: "album" | "artist", size?: number): Promise; scrobble(id: string): Promise searchArtists(query: string): Promise; searchAlbums(query: string): Promise; searchTracks(query: string): Promise; }