mirror of
https://github.com/wkulhanek/bonob.git
synced 2025-12-21 17:33:29 +01:00
Move subsonic generic library into proper class
This commit is contained in:
600
src/subsonic.ts
600
src/subsonic.ts
@@ -431,6 +431,312 @@ interface SubsonicMusicLibrary extends MusicLibrary {
|
|||||||
): TE.TaskEither<Error, string | undefined>;
|
): TE.TaskEither<Error, string | undefined>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class SubsonicGenericMusicLibrary implements SubsonicMusicLibrary {
|
||||||
|
subsonic: Subsonic;
|
||||||
|
credentials: SubsonicCredentials;
|
||||||
|
|
||||||
|
constructor(subsonic: Subsonic, credentials: SubsonicCredentials) {
|
||||||
|
this.subsonic = subsonic;
|
||||||
|
this.credentials = credentials;
|
||||||
|
}
|
||||||
|
|
||||||
|
flavour = () => "subsonic";
|
||||||
|
|
||||||
|
bearerToken = (_: Credentials) => TE.right(undefined);
|
||||||
|
|
||||||
|
artists = (q: ArtistQuery): Promise<Result<ArtistSummary & Sortable>> =>
|
||||||
|
this.subsonic
|
||||||
|
.getArtists(this.credentials)
|
||||||
|
.then(slice2(q))
|
||||||
|
.then(([page, total]) => ({
|
||||||
|
total,
|
||||||
|
results: page.map((it) => ({
|
||||||
|
id: it.id,
|
||||||
|
name: it.name,
|
||||||
|
sortName: it.name,
|
||||||
|
image: it.image,
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
|
||||||
|
artist = async (id: string): Promise<Artist> =>
|
||||||
|
this.subsonic.getArtistWithInfo(this.credentials, id);
|
||||||
|
|
||||||
|
albums = async (q: AlbumQuery): Promise<Result<AlbumSummary>> =>
|
||||||
|
this.subsonic.getAlbumList2(this.credentials, q);
|
||||||
|
|
||||||
|
album = (id: string): Promise<Album> =>
|
||||||
|
this.subsonic.getAlbum(this.credentials, id);
|
||||||
|
|
||||||
|
genres = () =>
|
||||||
|
this.subsonic
|
||||||
|
.getJSON<GetGenresResponse>(this.credentials, "/rest/getGenres")
|
||||||
|
.then((it) =>
|
||||||
|
pipe(
|
||||||
|
it.genres.genre || [],
|
||||||
|
A.filter((it) => it.albumCount > 0),
|
||||||
|
A.map((it) => it.value),
|
||||||
|
A.sort(ordString),
|
||||||
|
A.map((it) => ({ id: b64Encode(it), name: it }))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
tracks = (albumId: string) =>
|
||||||
|
this.subsonic
|
||||||
|
.getJSON<GetAlbumResponse>(this.credentials, "/rest/getAlbum", {
|
||||||
|
id: albumId,
|
||||||
|
})
|
||||||
|
.then((it) => it.album)
|
||||||
|
.then((album) =>
|
||||||
|
(album.song || []).map((song) => asTrack(asAlbum(album), song))
|
||||||
|
);
|
||||||
|
|
||||||
|
track = (trackId: string) =>
|
||||||
|
this.subsonic.getTrack(this.credentials, trackId);
|
||||||
|
|
||||||
|
rate = (trackId: string, rating: Rating) =>
|
||||||
|
Promise.resolve(true)
|
||||||
|
.then(() => {
|
||||||
|
if (rating.stars >= 0 && rating.stars <= 5) {
|
||||||
|
return this.subsonic.getTrack(this.credentials, trackId);
|
||||||
|
} else {
|
||||||
|
throw `Invalid rating.stars value of ${rating.stars}`;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then((track) => {
|
||||||
|
const thingsToUpdate = [];
|
||||||
|
if (track.rating.love != rating.love) {
|
||||||
|
thingsToUpdate.push(
|
||||||
|
this.subsonic.getJSON(
|
||||||
|
this.credentials,
|
||||||
|
`/rest/${rating.love ? "star" : "unstar"}`,
|
||||||
|
{
|
||||||
|
id: trackId,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (track.rating.stars != rating.stars) {
|
||||||
|
thingsToUpdate.push(
|
||||||
|
this.subsonic.getJSON(this.credentials, `/rest/setRating`, {
|
||||||
|
id: trackId,
|
||||||
|
rating: rating.stars,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Promise.all(thingsToUpdate);
|
||||||
|
})
|
||||||
|
.then(() => true)
|
||||||
|
.catch(() => false);
|
||||||
|
|
||||||
|
stream = async ({
|
||||||
|
trackId,
|
||||||
|
range,
|
||||||
|
}: {
|
||||||
|
trackId: string;
|
||||||
|
range: string | undefined;
|
||||||
|
}) =>
|
||||||
|
this.subsonic.getTrack(this.credentials, trackId).then((track) =>
|
||||||
|
this.subsonic
|
||||||
|
.get(
|
||||||
|
this.credentials,
|
||||||
|
`/rest/stream`,
|
||||||
|
{
|
||||||
|
id: trackId,
|
||||||
|
c: this.subsonic.streamClientApplication(track),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: pipe(
|
||||||
|
range,
|
||||||
|
O.fromNullable,
|
||||||
|
O.map((range) => ({
|
||||||
|
"User-Agent": USER_AGENT,
|
||||||
|
Range: range,
|
||||||
|
})),
|
||||||
|
O.getOrElse(() => ({
|
||||||
|
"User-Agent": USER_AGENT,
|
||||||
|
}))
|
||||||
|
),
|
||||||
|
responseType: "stream",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.then((res) => ({
|
||||||
|
status: res.status,
|
||||||
|
headers: {
|
||||||
|
"content-type": res.headers["content-type"],
|
||||||
|
"content-length": res.headers["content-length"],
|
||||||
|
"content-range": res.headers["content-range"],
|
||||||
|
"accept-ranges": res.headers["accept-ranges"],
|
||||||
|
},
|
||||||
|
stream: res.data,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
coverArt = async (coverArtURN: BUrn, size?: number) =>
|
||||||
|
Promise.resolve(coverArtURN)
|
||||||
|
.then((it) => assertSystem(it, "subsonic"))
|
||||||
|
.then((it) => it.resource.split(":")[1]!)
|
||||||
|
.then((it) => this.subsonic.getCoverArt(this.credentials, it, size))
|
||||||
|
.then((res) => ({
|
||||||
|
contentType: res.headers["content-type"],
|
||||||
|
data: Buffer.from(res.data, "binary"),
|
||||||
|
}))
|
||||||
|
.catch((e) => {
|
||||||
|
logger.error(`Failed getting coverArt for urn:'${coverArtURN}': ${e}`);
|
||||||
|
return undefined;
|
||||||
|
});
|
||||||
|
|
||||||
|
scrobble = async (id: string) =>
|
||||||
|
this.subsonic
|
||||||
|
.getJSON(this.credentials, `/rest/scrobble`, {
|
||||||
|
id,
|
||||||
|
submission: true,
|
||||||
|
})
|
||||||
|
.then((_) => true)
|
||||||
|
.catch(() => false);
|
||||||
|
|
||||||
|
nowPlaying = async (id: string) =>
|
||||||
|
this.subsonic
|
||||||
|
.getJSON(this.credentials, `/rest/scrobble`, {
|
||||||
|
id,
|
||||||
|
submission: false,
|
||||||
|
})
|
||||||
|
.then((_) => true)
|
||||||
|
.catch(() => false);
|
||||||
|
|
||||||
|
searchArtists = async (query: string) =>
|
||||||
|
this.subsonic
|
||||||
|
.search3(this.credentials, { query, artistCount: 20 })
|
||||||
|
.then(({ artists }) =>
|
||||||
|
artists.map((artist) => ({
|
||||||
|
id: artist.id,
|
||||||
|
name: artist.name,
|
||||||
|
image: artistImageURN({
|
||||||
|
artistId: artist.id,
|
||||||
|
artistImageURL: artist.artistImageUrl,
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
searchAlbums = async (query: string) =>
|
||||||
|
this.subsonic
|
||||||
|
.search3(this.credentials, { query, albumCount: 20 })
|
||||||
|
.then(({ albums }) => this.subsonic.toAlbumSummary(albums));
|
||||||
|
|
||||||
|
searchTracks = async (query: string) =>
|
||||||
|
this.subsonic
|
||||||
|
.search3(this.credentials, { query, songCount: 20 })
|
||||||
|
.then(({ songs }) =>
|
||||||
|
Promise.all(
|
||||||
|
songs.map((it) => this.subsonic.getTrack(this.credentials, it.id))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
playlists = async () =>
|
||||||
|
this.subsonic
|
||||||
|
.getJSON<GetPlaylistsResponse>(this.credentials, "/rest/getPlaylists")
|
||||||
|
.then((it) => it.playlists.playlist || [])
|
||||||
|
.then((playlists) =>
|
||||||
|
playlists.map((it) => ({ id: it.id, name: it.name }))
|
||||||
|
);
|
||||||
|
|
||||||
|
playlist = async (id: string) =>
|
||||||
|
this.subsonic
|
||||||
|
.getJSON<GetPlaylistResponse>(this.credentials, "/rest/getPlaylist", {
|
||||||
|
id,
|
||||||
|
})
|
||||||
|
.then((it) => it.playlist)
|
||||||
|
.then((playlist) => {
|
||||||
|
let trackNumber = 1;
|
||||||
|
return {
|
||||||
|
id: playlist.id,
|
||||||
|
name: playlist.name,
|
||||||
|
entries: (playlist.entry || []).map((entry) => ({
|
||||||
|
...asTrack(
|
||||||
|
{
|
||||||
|
id: entry.albumId!,
|
||||||
|
name: entry.album!,
|
||||||
|
year: entry.year,
|
||||||
|
genre: maybeAsGenre(entry.genre),
|
||||||
|
artistName: entry.artist,
|
||||||
|
artistId: entry.artistId,
|
||||||
|
coverArt: coverArtURN(entry.coverArt),
|
||||||
|
},
|
||||||
|
entry
|
||||||
|
),
|
||||||
|
number: trackNumber++,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
createPlaylist = async (name: string) =>
|
||||||
|
this.subsonic
|
||||||
|
.getJSON<GetPlaylistResponse>(this.credentials, "/rest/createPlaylist", {
|
||||||
|
name,
|
||||||
|
})
|
||||||
|
.then((it) => it.playlist)
|
||||||
|
.then((it) => ({ id: it.id, name: it.name }));
|
||||||
|
|
||||||
|
deletePlaylist = async (id: string) =>
|
||||||
|
this.subsonic
|
||||||
|
.getJSON<GetPlaylistResponse>(this.credentials, "/rest/deletePlaylist", {
|
||||||
|
id,
|
||||||
|
})
|
||||||
|
.then((_) => true);
|
||||||
|
|
||||||
|
addToPlaylist = async (playlistId: string, trackId: string) =>
|
||||||
|
this.subsonic
|
||||||
|
.getJSON<GetPlaylistResponse>(this.credentials, "/rest/updatePlaylist", {
|
||||||
|
playlistId,
|
||||||
|
songIdToAdd: trackId,
|
||||||
|
})
|
||||||
|
.then((_) => true);
|
||||||
|
|
||||||
|
removeFromPlaylist = async (playlistId: string, indicies: number[]) =>
|
||||||
|
this.subsonic
|
||||||
|
.getJSON<GetPlaylistResponse>(this.credentials, "/rest/updatePlaylist", {
|
||||||
|
playlistId,
|
||||||
|
songIndexToRemove: indicies,
|
||||||
|
})
|
||||||
|
.then((_) => true);
|
||||||
|
|
||||||
|
similarSongs = async (id: string) =>
|
||||||
|
this.subsonic
|
||||||
|
.getJSON<GetSimilarSongsResponse>(
|
||||||
|
this.credentials,
|
||||||
|
"/rest/getSimilarSongs2",
|
||||||
|
{ id, count: 50 }
|
||||||
|
)
|
||||||
|
.then((it) => it.similarSongs2.song || [])
|
||||||
|
.then((songs) =>
|
||||||
|
Promise.all(
|
||||||
|
songs.map((song) =>
|
||||||
|
this.subsonic
|
||||||
|
.getAlbum(this.credentials, song.albumId!)
|
||||||
|
.then((album) => asTrack(album, song))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
topSongs = async (artistId: string) =>
|
||||||
|
this.subsonic.getArtist(this.credentials, artistId).then(({ name }) =>
|
||||||
|
this.subsonic
|
||||||
|
.getJSON<GetTopSongsResponse>(this.credentials, "/rest/getTopSongs", {
|
||||||
|
artist: name,
|
||||||
|
count: 50,
|
||||||
|
})
|
||||||
|
.then((it) => it.topSongs.song || [])
|
||||||
|
.then((songs) =>
|
||||||
|
Promise.all(
|
||||||
|
songs.map((song) =>
|
||||||
|
this.subsonic
|
||||||
|
.getAlbum(this.credentials, song.albumId!)
|
||||||
|
.then((album) => asTrack(album, song))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export class Subsonic implements MusicService {
|
export class Subsonic implements MusicService {
|
||||||
url: string;
|
url: string;
|
||||||
streamClientApplication: StreamClientApplication;
|
streamClientApplication: StreamClientApplication;
|
||||||
@@ -699,281 +1005,8 @@ export class Subsonic implements MusicService {
|
|||||||
private libraryFor = (
|
private libraryFor = (
|
||||||
credentials: SubsonicCredentials
|
credentials: SubsonicCredentials
|
||||||
): Promise<SubsonicMusicLibrary> => {
|
): Promise<SubsonicMusicLibrary> => {
|
||||||
const subsonic = this;
|
const genericSubsonic: SubsonicMusicLibrary =
|
||||||
|
new SubsonicGenericMusicLibrary(this, credentials);
|
||||||
const genericSubsonic: SubsonicMusicLibrary = {
|
|
||||||
flavour: () => "subsonic",
|
|
||||||
bearerToken: (_: Credentials) => TE.right(undefined),
|
|
||||||
artists: (q: ArtistQuery): Promise<Result<ArtistSummary & Sortable>> =>
|
|
||||||
subsonic
|
|
||||||
.getArtists(credentials)
|
|
||||||
.then(slice2(q))
|
|
||||||
.then(([page, total]) => ({
|
|
||||||
total,
|
|
||||||
results: page.map((it) => ({
|
|
||||||
id: it.id,
|
|
||||||
name: it.name,
|
|
||||||
sortName: it.name,
|
|
||||||
image: it.image,
|
|
||||||
})),
|
|
||||||
})),
|
|
||||||
artist: async (id: string): Promise<Artist> =>
|
|
||||||
subsonic.getArtistWithInfo(credentials, id),
|
|
||||||
albums: async (q: AlbumQuery): Promise<Result<AlbumSummary>> =>
|
|
||||||
subsonic.getAlbumList2(credentials, q),
|
|
||||||
album: (id: string): Promise<Album> => subsonic.getAlbum(credentials, id),
|
|
||||||
genres: () =>
|
|
||||||
subsonic
|
|
||||||
.getJSON<GetGenresResponse>(credentials, "/rest/getGenres")
|
|
||||||
.then((it) =>
|
|
||||||
pipe(
|
|
||||||
it.genres.genre || [],
|
|
||||||
A.filter((it) => it.albumCount > 0),
|
|
||||||
A.map((it) => it.value),
|
|
||||||
A.sort(ordString),
|
|
||||||
A.map((it) => ({ id: b64Encode(it), name: it }))
|
|
||||||
)
|
|
||||||
),
|
|
||||||
tracks: (albumId: string) =>
|
|
||||||
subsonic
|
|
||||||
.getJSON<GetAlbumResponse>(credentials, "/rest/getAlbum", {
|
|
||||||
id: albumId,
|
|
||||||
})
|
|
||||||
.then((it) => it.album)
|
|
||||||
.then((album) =>
|
|
||||||
(album.song || []).map((song) => asTrack(asAlbum(album), song))
|
|
||||||
),
|
|
||||||
track: (trackId: string) => subsonic.getTrack(credentials, trackId),
|
|
||||||
rate: (trackId: string, rating: Rating) =>
|
|
||||||
Promise.resolve(true)
|
|
||||||
.then(() => {
|
|
||||||
if (rating.stars >= 0 && rating.stars <= 5) {
|
|
||||||
return subsonic.getTrack(credentials, trackId);
|
|
||||||
} else {
|
|
||||||
throw `Invalid rating.stars value of ${rating.stars}`;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.then((track) => {
|
|
||||||
const thingsToUpdate = [];
|
|
||||||
if (track.rating.love != rating.love) {
|
|
||||||
thingsToUpdate.push(
|
|
||||||
subsonic.getJSON(
|
|
||||||
credentials,
|
|
||||||
`/rest/${rating.love ? "star" : "unstar"}`,
|
|
||||||
{
|
|
||||||
id: trackId,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (track.rating.stars != rating.stars) {
|
|
||||||
thingsToUpdate.push(
|
|
||||||
subsonic.getJSON(credentials, `/rest/setRating`, {
|
|
||||||
id: trackId,
|
|
||||||
rating: rating.stars,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return Promise.all(thingsToUpdate);
|
|
||||||
})
|
|
||||||
.then(() => true)
|
|
||||||
.catch(() => false),
|
|
||||||
stream: async ({
|
|
||||||
trackId,
|
|
||||||
range,
|
|
||||||
}: {
|
|
||||||
trackId: string;
|
|
||||||
range: string | undefined;
|
|
||||||
}) =>
|
|
||||||
subsonic.getTrack(credentials, trackId).then((track) =>
|
|
||||||
subsonic
|
|
||||||
.get(
|
|
||||||
credentials,
|
|
||||||
`/rest/stream`,
|
|
||||||
{
|
|
||||||
id: trackId,
|
|
||||||
c: this.streamClientApplication(track),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: pipe(
|
|
||||||
range,
|
|
||||||
O.fromNullable,
|
|
||||||
O.map((range) => ({
|
|
||||||
"User-Agent": USER_AGENT,
|
|
||||||
Range: range,
|
|
||||||
})),
|
|
||||||
O.getOrElse(() => ({
|
|
||||||
"User-Agent": USER_AGENT,
|
|
||||||
}))
|
|
||||||
),
|
|
||||||
responseType: "stream",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.then((res) => ({
|
|
||||||
status: res.status,
|
|
||||||
headers: {
|
|
||||||
"content-type": res.headers["content-type"],
|
|
||||||
"content-length": res.headers["content-length"],
|
|
||||||
"content-range": res.headers["content-range"],
|
|
||||||
"accept-ranges": res.headers["accept-ranges"],
|
|
||||||
},
|
|
||||||
stream: res.data,
|
|
||||||
}))
|
|
||||||
),
|
|
||||||
coverArt: async (coverArtURN: BUrn, size?: number) =>
|
|
||||||
Promise.resolve(coverArtURN)
|
|
||||||
.then((it) => assertSystem(it, "subsonic"))
|
|
||||||
.then((it) => it.resource.split(":")[1]!)
|
|
||||||
.then((it) => subsonic.getCoverArt(credentials, it, size))
|
|
||||||
.then((res) => ({
|
|
||||||
contentType: res.headers["content-type"],
|
|
||||||
data: Buffer.from(res.data, "binary"),
|
|
||||||
}))
|
|
||||||
.catch((e) => {
|
|
||||||
logger.error(
|
|
||||||
`Failed getting coverArt for urn:'${coverArtURN}': ${e}`
|
|
||||||
);
|
|
||||||
return undefined;
|
|
||||||
}),
|
|
||||||
scrobble: async (id: string) =>
|
|
||||||
subsonic
|
|
||||||
.getJSON(credentials, `/rest/scrobble`, {
|
|
||||||
id,
|
|
||||||
submission: true,
|
|
||||||
})
|
|
||||||
.then((_) => true)
|
|
||||||
.catch(() => false),
|
|
||||||
nowPlaying: async (id: string) =>
|
|
||||||
subsonic
|
|
||||||
.getJSON(credentials, `/rest/scrobble`, {
|
|
||||||
id,
|
|
||||||
submission: false,
|
|
||||||
})
|
|
||||||
.then((_) => true)
|
|
||||||
.catch(() => false),
|
|
||||||
searchArtists: async (query: string) =>
|
|
||||||
subsonic
|
|
||||||
.search3(credentials, { query, artistCount: 20 })
|
|
||||||
.then(({ artists }) =>
|
|
||||||
artists.map((artist) => ({
|
|
||||||
id: artist.id,
|
|
||||||
name: artist.name,
|
|
||||||
image: artistImageURN({
|
|
||||||
artistId: artist.id,
|
|
||||||
artistImageURL: artist.artistImageUrl,
|
|
||||||
}),
|
|
||||||
}))
|
|
||||||
),
|
|
||||||
searchAlbums: async (query: string) =>
|
|
||||||
subsonic
|
|
||||||
.search3(credentials, { query, albumCount: 20 })
|
|
||||||
.then(({ albums }) => subsonic.toAlbumSummary(albums)),
|
|
||||||
searchTracks: async (query: string) =>
|
|
||||||
subsonic
|
|
||||||
.search3(credentials, { query, songCount: 20 })
|
|
||||||
.then(({ songs }) =>
|
|
||||||
Promise.all(
|
|
||||||
songs.map((it) => subsonic.getTrack(credentials, it.id))
|
|
||||||
)
|
|
||||||
),
|
|
||||||
playlists: async () =>
|
|
||||||
subsonic
|
|
||||||
.getJSON<GetPlaylistsResponse>(credentials, "/rest/getPlaylists")
|
|
||||||
.then((it) => it.playlists.playlist || [])
|
|
||||||
.then((playlists) =>
|
|
||||||
playlists.map((it) => ({ id: it.id, name: it.name }))
|
|
||||||
),
|
|
||||||
playlist: async (id: string) =>
|
|
||||||
subsonic
|
|
||||||
.getJSON<GetPlaylistResponse>(credentials, "/rest/getPlaylist", {
|
|
||||||
id,
|
|
||||||
})
|
|
||||||
.then((it) => it.playlist)
|
|
||||||
.then((playlist) => {
|
|
||||||
let trackNumber = 1;
|
|
||||||
return {
|
|
||||||
id: playlist.id,
|
|
||||||
name: playlist.name,
|
|
||||||
entries: (playlist.entry || []).map((entry) => ({
|
|
||||||
...asTrack(
|
|
||||||
{
|
|
||||||
id: entry.albumId!,
|
|
||||||
name: entry.album!,
|
|
||||||
year: entry.year,
|
|
||||||
genre: maybeAsGenre(entry.genre),
|
|
||||||
artistName: entry.artist,
|
|
||||||
artistId: entry.artistId,
|
|
||||||
coverArt: coverArtURN(entry.coverArt),
|
|
||||||
},
|
|
||||||
entry
|
|
||||||
),
|
|
||||||
number: trackNumber++,
|
|
||||||
})),
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
createPlaylist: async (name: string) =>
|
|
||||||
subsonic
|
|
||||||
.getJSON<GetPlaylistResponse>(credentials, "/rest/createPlaylist", {
|
|
||||||
name,
|
|
||||||
})
|
|
||||||
.then((it) => it.playlist)
|
|
||||||
.then((it) => ({ id: it.id, name: it.name })),
|
|
||||||
deletePlaylist: async (id: string) =>
|
|
||||||
subsonic
|
|
||||||
.getJSON<GetPlaylistResponse>(credentials, "/rest/deletePlaylist", {
|
|
||||||
id,
|
|
||||||
})
|
|
||||||
.then((_) => true),
|
|
||||||
addToPlaylist: async (playlistId: string, trackId: string) =>
|
|
||||||
subsonic
|
|
||||||
.getJSON<GetPlaylistResponse>(credentials, "/rest/updatePlaylist", {
|
|
||||||
playlistId,
|
|
||||||
songIdToAdd: trackId,
|
|
||||||
})
|
|
||||||
.then((_) => true),
|
|
||||||
removeFromPlaylist: async (playlistId: string, indicies: number[]) =>
|
|
||||||
subsonic
|
|
||||||
.getJSON<GetPlaylistResponse>(credentials, "/rest/updatePlaylist", {
|
|
||||||
playlistId,
|
|
||||||
songIndexToRemove: indicies,
|
|
||||||
})
|
|
||||||
.then((_) => true),
|
|
||||||
similarSongs: async (id: string) =>
|
|
||||||
subsonic
|
|
||||||
.getJSON<GetSimilarSongsResponse>(
|
|
||||||
credentials,
|
|
||||||
"/rest/getSimilarSongs2",
|
|
||||||
{ id, count: 50 }
|
|
||||||
)
|
|
||||||
.then((it) => it.similarSongs2.song || [])
|
|
||||||
.then((songs) =>
|
|
||||||
Promise.all(
|
|
||||||
songs.map((song) =>
|
|
||||||
subsonic
|
|
||||||
.getAlbum(credentials, song.albumId!)
|
|
||||||
.then((album) => asTrack(album, song))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
),
|
|
||||||
topSongs: async (artistId: string) =>
|
|
||||||
subsonic.getArtist(credentials, artistId).then(({ name }) =>
|
|
||||||
subsonic
|
|
||||||
.getJSON<GetTopSongsResponse>(credentials, "/rest/getTopSongs", {
|
|
||||||
artist: name,
|
|
||||||
count: 50,
|
|
||||||
})
|
|
||||||
.then((it) => it.topSongs.song || [])
|
|
||||||
.then((songs) =>
|
|
||||||
Promise.all(
|
|
||||||
songs.map((song) =>
|
|
||||||
subsonic
|
|
||||||
.getAlbum(credentials, song.albumId!)
|
|
||||||
.then((album) => asTrack(album, song))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
if (credentials.type == "navidrome") {
|
if (credentials.type == "navidrome") {
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
@@ -991,8 +1024,9 @@ export class Subsonic implements MusicService {
|
|||||||
),
|
),
|
||||||
TE.map((it) => it.data.token as string | undefined)
|
TE.map((it) => it.data.token as string | undefined)
|
||||||
),
|
),
|
||||||
artists: async (q: ArtistQuery): Promise<Result<ArtistSummary & Sortable>> =>
|
artists: async (
|
||||||
{
|
q: ArtistQuery
|
||||||
|
): Promise<Result<ArtistSummary & Sortable>> => {
|
||||||
let params: any = {
|
let params: any = {
|
||||||
_sort: "name",
|
_sort: "name",
|
||||||
_order: "ASC",
|
_order: "ASC",
|
||||||
@@ -1001,8 +1035,8 @@ export class Subsonic implements MusicService {
|
|||||||
if (q._count) {
|
if (q._count) {
|
||||||
params = {
|
params = {
|
||||||
...params,
|
...params,
|
||||||
_end: (q._index || 0) + q._count
|
_end: (q._index || 0) + q._count,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return axios
|
return axios
|
||||||
@@ -1025,12 +1059,14 @@ export class Subsonic implements MusicService {
|
|||||||
})
|
})
|
||||||
.then((it) => ({
|
.then((it) => ({
|
||||||
results: (it.data as NDArtist[]).map(artistSummaryFromNDArtist),
|
results: (it.data as NDArtist[]).map(artistSummaryFromNDArtist),
|
||||||
total: Number.parseInt(it.headers["x-total-count"] || "0")
|
total: Number.parseInt(it.headers["x-total-count"] || "0"),
|
||||||
}))
|
}));
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
return Promise.resolve(genericSubsonic);
|
return Promise.resolve(genericSubsonic);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default Subsonic;
|
||||||
|
|||||||
Reference in New Issue
Block a user