2021-03-20 13:31:53 +01:00

471 lines
13 KiB
TypeScript

import axios, { AxiosRequestConfig, AxiosInstance } from 'axios'
import { AuthService } from '@/auth/service'
export type AlbumSort =
'a-z' |
'recently-added'|
'recently-played' |
'most-played' |
'random'
export interface Track {
id: string
title: string
duration: number
starred: boolean
image?: string
url?: string
track?: number
album?: string
albumId?: string
artist?: string
artistId?: string
}
export interface Album {
id: string
name: string
artist: string
artistId: string
year: number
starred: boolean
genreId?: string
image?: string
tracks?: Track[]
}
export interface Artist {
id: string
name: string
albumCount: number
description?: string
starred: boolean
lastFmUrl?: string
musicBrainzUrl?: string
similarArtist?: Artist[]
albums?: Album[]
}
export interface SearchResult {
artists: Artist[]
albums: Album[]
tracks: Track[]
}
export interface RadioStation {
id: string
title: string
description: string
url: string
}
export class API {
readonly http: AxiosInstance;
readonly get: (path: string, params?: any) => Promise<any>;
readonly post: (path: string, params?: any) => Promise<any>;
readonly clientName = window.origin || 'web';
constructor(private auth: AuthService) {
this.http = axios.create({})
this.http.interceptors.request.use((config: AxiosRequestConfig) => {
config.params = config.params || {}
config.baseURL = this.auth.server
config.params.u = this.auth.username
config.params.s = this.auth.salt
config.params.t = this.auth.hash
config.params.c = this.clientName
config.params.f = 'json'
config.params.v = '1.15.0'
return config
})
this.get = (path: string, params: any = {}) => {
return this.http.get(path, { params }).then(response => {
const subsonicResponse = response.data['subsonic-response']
if (subsonicResponse.status !== 'ok') {
const message = subsonicResponse.error?.message || subsonicResponse.status
const err = new Error(message)
return Promise.reject(err)
}
return Promise.resolve(subsonicResponse)
})
}
this.post = (path: string, params: any = {}) => {
return this.http.post(path, params).then(response => {
const subsonicResponse = response.data['subsonic-response']
if (subsonicResponse.status !== 'ok') {
const err = new Error(subsonicResponse.status)
return Promise.reject(err)
}
return Promise.resolve(subsonicResponse)
})
}
}
async getGenres() {
const response = await this.get('rest/getGenres', {})
return response.genres.genre
.map((item: any) => ({
id: item.value,
name: item.value,
albumCount: item.albumCount,
trackCount: item.songCount,
}))
.sort((a: any, b:any) => b.albumCount - a.albumCount)
}
async getAlbumsByGenre(id: string, size: number, offset = 0) {
const params = {
type: 'byGenre',
genre: id,
size,
offset,
}
const response = await this.get('rest/getAlbumList2', params)
return (response.albumList2?.album || []).map(this.normalizeAlbum, this)
}
async getTracksByGenre(id: string, size: number, offset = 0) {
const params = {
genre: id,
count: size,
offset,
}
const response = await this.get('rest/getSongsByGenre', params)
return (response.songsByGenre?.song || []).map(this.normalizeTrack, this)
}
async getArtists(): Promise<Artist[]> {
const response = await this.get('rest/getArtists')
return (response.artists?.index || [])
.flatMap((index: any) => index.artist)
.map(this.normalizeArtist, this)
}
async getAlbums(sort: AlbumSort, size: number, offset = 0): Promise<Album[]> {
const type = {
'a-z': 'alphabeticalByName',
'recently-added': 'newest',
'recently-played': 'recent',
'most-played': 'frequent',
random: 'random',
}[sort]
const params = { type, offset, size }
const response = await this.get('rest/getAlbumList2', params)
const albums = response.albumList2?.album || []
return albums.map(this.normalizeAlbum, this)
}
async getArtistDetails(id: string): Promise<Artist> {
const params = { id }
const [info1, info2] = await Promise.all([
this.get('rest/getArtist', params).then(r => r.artist),
this.get('rest/getArtistInfo2', params).then(r => r.artistInfo2),
])
return this.normalizeArtist({ ...info1, ...info2 })
}
async getAlbumDetails(id: string): Promise<Album> {
const params = { id }
const data = await this.get('rest/getAlbum', params)
return this.normalizeAlbum(data.album)
}
async getPlaylists() {
const response = await this.get('rest/getPlaylists')
return (response.playlists?.playlist || []).map((playlist: any) => ({
...playlist,
name: playlist.name || '(Unnamed)',
image: playlist.songCount > 0 ? this.getCoverArtUrl(playlist) : undefined,
}))
}
async getPlaylist(id: string) {
if (id === 'random') {
return {
id,
name: 'Random',
tracks: await this.getRandomSongs(),
}
}
const response = await this.get('rest/getPlaylist', { id })
return {
...response.playlist,
name: response.playlist.name || '(Unnamed)',
tracks: (response.playlist.entry || []).map(this.normalizeTrack, this),
}
}
async createPlaylist(name: string) {
await this.get('rest/createPlaylist', { name })
return this.getPlaylists()
}
async editPlaylist(playlistId: string, name: string, comment: string) {
const params = {
playlistId,
name,
comment,
}
await this.get('rest/updatePlaylist', params)
}
async deletePlaylist(id: string) {
await this.get('rest/deletePlaylist', { id })
}
async addToPlaylist(playlistId: string, trackId: string) {
const params = {
playlistId,
songIdToAdd: trackId,
}
await this.get('rest/updatePlaylist', params)
}
async removeFromPlaylist(playlistId: string, index: string) {
const params = {
playlistId,
songIndexToRemove: index,
}
await this.get('rest/updatePlaylist', params)
}
async getRandomSongs(): Promise<Track[]> {
const params = {
size: 200,
}
const response = await this.get('rest/getRandomSongs', params)
return (response.randomSongs?.song || []).map(this.normalizeTrack, this)
}
async getStarred() {
const response = await this.get('rest/getStarred2')
return {
albums: (response.starred2?.album || []).map(this.normalizeAlbum, this),
artists: (response.starred2?.artist || []).map(this.normalizeArtist, this),
tracks: (response.starred2?.song || []).map(this.normalizeTrack, this)
}
}
starAlbum(id: string) {
return this.star('album', id)
}
unstarAlbum(id: string) {
return this.unstar('album', id)
}
async star(type: 'track' | 'album' | 'artist', id: string) {
const params = {
id: type === 'track' ? id : undefined,
albumId: type === 'album' ? id : undefined,
artistId: type === 'artist' ? id : undefined,
}
await this.get('rest/star', params)
}
async unstar(type: 'track' | 'album' | 'artist', id: string) {
const params = {
id: type === 'track' ? id : undefined,
albumId: type === 'album' ? id : undefined,
artistId: type === 'artist' ? id : undefined,
}
await this.get('rest/unstar', params)
}
async search(query: string): Promise<SearchResult> {
const params = {
query,
}
const data = await this.get('rest/search3', params)
return {
tracks: (data.searchResult3.song || []).map(this.normalizeTrack, this),
albums: (data.searchResult3.album || []).map(this.normalizeAlbum, this),
artists: (data.searchResult3.artist || []).map(this.normalizeArtist, this),
}
}
async getRadioStations(): Promise<RadioStation[]> {
const response = await this.get('rest/getInternetRadioStations')
return (response?.internetRadioStations?.internetRadioStation || [])
.map((item: any, idx: number) => ({ ...item, track: idx + 1 }))
.map(this.normalizeRadioStation, this)
}
async addRadioStation(title: string, url: string): Promise<RadioStation> {
const params = {
name: title,
streamUrl: url,
}
return this
.get('rest/createInternetRadioStation', params)
.then(this.normalizeRadioStation)
}
async updateRadioStation(item: RadioStation): Promise<RadioStation> {
const params = {
id: item.id,
name: item.title,
streamUrl: item.url,
}
return this
.get('rest/updateInternetRadioStation', params)
.then(this.normalizeRadioStation)
}
async deleteRadioStation(id: string): Promise<void> {
return this.get('rest/deleteInternetRadioStation', { id })
}
async getPodcasts(): Promise<any[]> {
const response = await this.get('rest/getPodcasts')
return (response?.podcasts?.channel || []).map(this.normalizePodcast, this)
}
async getPodcast(id: string): Promise<any> {
const response = await this.get('rest/getPodcasts', { id })
return this.normalizePodcast(response?.podcasts?.channel[0])
}
async refreshPodcasts(): Promise<void> {
return this.get('rest/refreshPodcasts')
}
async scan(): Promise<void> {
return this.get('rest/startScan')
}
async scrobble(id: string): Promise<void> {
return this.get('rest/scrobble', { id })
}
private normalizeRadioStation(item: any): Track & RadioStation {
return {
id: `radio-${item.id}`,
title: item.name,
description: item.homePageUrl,
track: item.track,
url: item.streamUrl,
duration: 0,
starred: false,
}
}
private normalizeTrack(item: any): Track {
return {
id: item.id,
title: item.title,
duration: item.duration,
starred: !!item.starred,
track: item.track,
album: item.album,
albumId: item.albumId,
artist: item.artist,
artistId: item.artistId,
url: this.getStreamUrl(item.id),
image: this.getCoverArtUrl(item),
}
}
private normalizeAlbum(item: any): Album {
return {
id: item.id,
name: item.name,
artist: item.artist,
artistId: item.artistId,
image: this.getCoverArtUrl(item),
year: item.year || 0,
starred: !!item.starred,
genreId: item.genre,
tracks: (item.song || []).map(this.normalizeTrack, this)
}
}
private normalizeArtist(item: any): Artist {
const albums = item.album
?.map(this.normalizeAlbum, this)
.sort((a: any, b: any) => b.year - a.year)
return {
id: item.id,
name: item.name,
description: (item.biography || '').replace(/<a[^>]*>.*?<\/a>/gm, ''),
starred: !!item.starred,
albumCount: item.albumCount,
lastFmUrl: item.lastFmUrl,
musicBrainzUrl: item.musicBrainzId
? `https://musicbrainz.org/artist/${item.musicBrainzId}`
: undefined,
albums,
similarArtist: (item.similarArtist || []).map(this.normalizeArtist, this)
}
}
private normalizePodcast(podcast: any): any {
const image = podcast.originalImageUrl
return {
id: podcast.id,
name: podcast.title,
description: podcast.description,
image: image,
url: podcast.url,
trackCount: podcast.episode.length,
tracks: podcast.episode.map((episode: any, index: number) => ({
id: episode.id,
title: episode.title,
duration: episode.duration,
starred: false,
track: podcast.episode.length - index,
album: podcast.title,
albumId: null,
artist: '',
artistId: null,
image,
url: episode.streamId ? this.getStreamUrl(episode.streamId) : null,
description: episode.description,
playable: episode.status === 'completed',
})),
}
}
getDownloadUrl(id: any) {
const { server, username, salt, hash } = this.auth
return `${server}/rest/download` +
`?id=${id}` +
'&v=1.15.0' +
`&u=${username}` +
`&s=${salt}` +
`&t=${hash}` +
`&c=${this.clientName}`
}
private getCoverArtUrl(item: any) {
if (!item.coverArt) {
return undefined
}
const { server, username, salt, hash } = this.auth
return `${server}/rest/getCoverArt` +
`?id=${item.coverArt}` +
'&v=1.15.0' +
`&u=${username}` +
`&s=${salt}` +
`&t=${hash}` +
`&c=${this.clientName}` +
'&size=300'
}
private getStreamUrl(id: any) {
const { server, username, salt, hash } = this.auth
return `${server}/rest/stream` +
`?id=${id}` +
'&format=raw' +
'&v=1.15.0' +
`&u=${username}` +
`&s=${salt}` +
`&t=${hash}` +
`&c=${this.clientName}`
}
}