This commit is contained in:
simojenki
2022-01-05 12:06:53 +11:00
parent 1c94a6d565
commit 6ad39ce044
2 changed files with 192 additions and 215 deletions

View File

@@ -203,13 +203,6 @@ type GetSongResponse = {
song: song; song: song;
}; };
type GetStarredResponse = {
starred2: {
song: song[];
album: album[];
};
};
export type PingResponse = { export type PingResponse = {
status: string; status: string;
version: string; version: string;
@@ -445,8 +438,7 @@ export class SubsonicGenericMusicLibrary implements SubsonicMusicLibrary {
bearerToken = (_: Credentials) => TE.right(undefined); bearerToken = (_: Credentials) => TE.right(undefined);
artists = (q: ArtistQuery): Promise<Result<ArtistSummary & Sortable>> => artists = (q: ArtistQuery): Promise<Result<ArtistSummary & Sortable>> =>
this.subsonic this.getArtists()
.getArtists(this.credentials)
.then(slice2(q)) .then(slice2(q))
.then(([page, total]) => ({ .then(([page, total]) => ({
total, total,
@@ -459,13 +451,12 @@ export class SubsonicGenericMusicLibrary implements SubsonicMusicLibrary {
})); }));
artist = async (id: string): Promise<Artist> => artist = async (id: string): Promise<Artist> =>
this.subsonic.getArtistWithInfo(this.credentials, id); this.getArtistWithInfo(id);
albums = async (q: AlbumQuery): Promise<Result<AlbumSummary>> => albums = async (q: AlbumQuery): Promise<Result<AlbumSummary>> =>
this.subsonic.getAlbumList2(this.credentials, q); this.getAlbumList2(q);
album = (id: string): Promise<Album> => album = (id: string): Promise<Album> => this.getAlbum(id);
this.subsonic.getAlbum(this.credentials, id);
genres = () => genres = () =>
this.subsonic this.subsonic
@@ -490,14 +481,13 @@ export class SubsonicGenericMusicLibrary implements SubsonicMusicLibrary {
(album.song || []).map((song) => asTrack(asAlbum(album), song)) (album.song || []).map((song) => asTrack(asAlbum(album), song))
); );
track = (trackId: string) => track = (trackId: string) => this.getTrack(trackId);
this.subsonic.getTrack(this.credentials, trackId);
rate = (trackId: string, rating: Rating) => rate = (trackId: string, rating: Rating) =>
Promise.resolve(true) Promise.resolve(true)
.then(() => { .then(() => {
if (rating.stars >= 0 && rating.stars <= 5) { if (rating.stars >= 0 && rating.stars <= 5) {
return this.subsonic.getTrack(this.credentials, trackId); return this.getTrack(trackId);
} else { } else {
throw `Invalid rating.stars value of ${rating.stars}`; throw `Invalid rating.stars value of ${rating.stars}`;
} }
@@ -535,7 +525,7 @@ export class SubsonicGenericMusicLibrary implements SubsonicMusicLibrary {
trackId: string; trackId: string;
range: string | undefined; range: string | undefined;
}) => }) =>
this.subsonic.getTrack(this.credentials, trackId).then((track) => this.getTrack(trackId).then((track) =>
this.subsonic this.subsonic
.get( .get(
this.credentials, this.credentials,
@@ -575,7 +565,7 @@ export class SubsonicGenericMusicLibrary implements SubsonicMusicLibrary {
Promise.resolve(coverArtURN) Promise.resolve(coverArtURN)
.then((it) => assertSystem(it, "subsonic")) .then((it) => assertSystem(it, "subsonic"))
.then((it) => it.resource.split(":")[1]!) .then((it) => it.resource.split(":")[1]!)
.then((it) => this.subsonic.getCoverArt(this.credentials, it, size)) .then((it) => this.getCoverArt(this.credentials, it, size))
.then((res) => ({ .then((res) => ({
contentType: res.headers["content-type"], contentType: res.headers["content-type"],
data: Buffer.from(res.data, "binary"), data: Buffer.from(res.data, "binary"),
@@ -604,9 +594,8 @@ export class SubsonicGenericMusicLibrary implements SubsonicMusicLibrary {
.catch(() => false); .catch(() => false);
searchArtists = async (query: string) => searchArtists = async (query: string) =>
this.subsonic this.search3({ query, artistCount: 20 }).then(
.search3(this.credentials, { query, artistCount: 20 }) ({ artists }) =>
.then(({ artists }) =>
artists.map((artist) => ({ artists.map((artist) => ({
id: artist.id, id: artist.id,
name: artist.name, name: artist.name,
@@ -615,21 +604,17 @@ export class SubsonicGenericMusicLibrary implements SubsonicMusicLibrary {
artistImageURL: artist.artistImageUrl, artistImageURL: artist.artistImageUrl,
}), }),
})) }))
); );
searchAlbums = async (query: string) => searchAlbums = async (query: string) =>
this.subsonic this.search3({ query, albumCount: 20 }).then(
.search3(this.credentials, { query, albumCount: 20 }) ({ albums }) => this.toAlbumSummary(albums)
.then(({ albums }) => this.subsonic.toAlbumSummary(albums)); );
searchTracks = async (query: string) => searchTracks = async (query: string) =>
this.subsonic this.search3({ query, songCount: 20 }).then(({ songs }) =>
.search3(this.credentials, { query, songCount: 20 }) Promise.all(songs.map((it) => this.getTrack(it.id)))
.then(({ songs }) => );
Promise.all(
songs.map((it) => this.subsonic.getTrack(this.credentials, it.id))
)
);
playlists = async () => playlists = async () =>
this.subsonic this.subsonic
@@ -710,15 +695,15 @@ export class SubsonicGenericMusicLibrary implements SubsonicMusicLibrary {
.then((songs) => .then((songs) =>
Promise.all( Promise.all(
songs.map((song) => songs.map((song) =>
this.subsonic this.getAlbum(song.albumId!).then((album) =>
.getAlbum(this.credentials, song.albumId!) asTrack(album, song)
.then((album) => asTrack(album, song)) )
) )
) )
); );
topSongs = async (artistId: string) => topSongs = async (artistId: string) =>
this.subsonic.getArtist(this.credentials, artistId).then(({ name }) => this.getArtist(artistId).then(({ name }) =>
this.subsonic this.subsonic
.getJSON<GetTopSongsResponse>(this.credentials, "/rest/getTopSongs", { .getJSON<GetTopSongsResponse>(this.credentials, "/rest/getTopSongs", {
artist: name, artist: name,
@@ -728,13 +713,181 @@ export class SubsonicGenericMusicLibrary implements SubsonicMusicLibrary {
.then((songs) => .then((songs) =>
Promise.all( Promise.all(
songs.map((song) => songs.map((song) =>
this.subsonic this.getAlbum(song.albumId!).then((album) =>
.getAlbum(this.credentials, song.albumId!) asTrack(album, song)
.then((album) => asTrack(album, song)) )
) )
) )
) )
); );
private getArtists = (): Promise<(IdName & { albumCount: number; image: BUrn | undefined })[]> =>
this.subsonic
.getJSON<GetArtistsResponse>(this.credentials, "/rest/getArtists")
.then((it) => (it.artists.index || []).flatMap((it) => it.artist || []))
.then((artists) =>
artists.map((artist) => ({
id: `${artist.id}`,
name: artist.name,
albumCount: artist.albumCount,
image: artistImageURN({
artistId: artist.id,
artistImageURL: artist.artistImageUrl,
}),
}))
);
private getArtistInfo = (
id: string
): Promise<{
similarArtist: (ArtistSummary & { inLibrary: boolean })[];
images: {
s: string | undefined;
m: string | undefined;
l: string | undefined;
};
}> =>
this.subsonic
.getJSON<GetArtistInfoResponse>(this.credentials, "/rest/getArtistInfo2", {
id,
count: 50,
includeNotPresent: true,
})
.then((it) => it.artistInfo2)
.then((it) => ({
images: {
s: it.smallImageUrl,
m: it.mediumImageUrl,
l: it.largeImageUrl,
},
similarArtist: (it.similarArtist || []).map((artist) => ({
id: `${artist.id}`,
name: artist.name,
inLibrary: artistIsInLibrary(artist.id),
image: artistImageURN({
artistId: artist.id,
artistImageURL: artist.artistImageUrl,
}),
})),
}));
private getAlbum = (id: string): Promise<Album> =>
this.subsonic
.getJSON<GetAlbumResponse>(this.credentials, "/rest/getAlbum", { id })
.then((it) => it.album)
.then((album) => ({
id: album.id,
name: album.name,
year: album.year,
genre: maybeAsGenre(album.genre),
artistId: album.artistId,
artistName: album.artist,
coverArt: coverArtURN(album.coverArt),
}));
private getArtist = (
id: string
): Promise<
IdName & { artistImageUrl: string | undefined; albums: AlbumSummary[] }
> =>
this.subsonic
.getJSON<GetArtistResponse>(this.credentials, "/rest/getArtist", {
id,
})
.then((it) => it.artist)
.then((it) => ({
id: it.id,
name: it.name,
artistImageUrl: it.artistImageUrl,
albums: this.toAlbumSummary(it.album || []),
}));
private getArtistWithInfo = (id: string) =>
Promise.all([
this.getArtist(id),
this.getArtistInfo(id),
]).then(([artist, artistInfo]) => ({
id: artist.id,
name: artist.name,
image: artistImageURN({
artistId: artist.id,
artistImageURL: [
artist.artistImageUrl,
artistInfo.images.l,
artistInfo.images.m,
artistInfo.images.s,
].find(isValidImage),
}),
albums: artist.albums,
similarArtists: artistInfo.similarArtist,
}));
private getCoverArt = (credentials: Credentials, id: string, size?: number) =>
this.subsonic.get(
credentials,
"/rest/getCoverArt",
size ? { id, size } : { id },
{
headers: { "User-Agent": "bonob" },
responseType: "arraybuffer",
}
);
private getTrack = (id: string) =>
this.subsonic
.getJSON<GetSongResponse>(this.credentials, "/rest/getSong", {
id,
})
.then((it) => it.song)
.then((song) =>
this.getAlbum(song.albumId!).then((album) =>
asTrack(album, song)
)
);
private toAlbumSummary = (albumList: album[]): AlbumSummary[] =>
albumList.map((album) => ({
id: album.id,
name: album.name,
year: album.year,
genre: maybeAsGenre(album.genre),
artistId: album.artistId,
artistName: album.artist,
coverArt: coverArtURN(album.coverArt),
}));
private search3 = (q: any) =>
this.subsonic
.getJSON<Search3Response>(this.credentials, "/rest/search3", {
artistCount: 0,
albumCount: 0,
songCount: 0,
...q,
})
.then((it) => ({
artists: it.searchResult3.artist || [],
albums: it.searchResult3.album || [],
songs: it.searchResult3.song || [],
}));
private getAlbumList2 = (q: AlbumQuery) =>
Promise.all([
this.getArtists().then((it) =>
_.inject(it, (total, artist) => total + artist.albumCount, 0)
),
this.subsonic
.getJSON<GetAlbumListResponse>(this.credentials, "/rest/getAlbumList2", {
type: AlbumQueryTypeToSubsonicType[q.type],
...(q.genre ? { genre: b64Decode(q.genre) } : {}),
size: 500,
offset: q._index,
})
.then((response) => response.albumList2.album || [])
.then(this.toAlbumSummary),
]).then(([total, albums]) => ({
results: albums.slice(0, q._count),
total: albums.length == 500 ? total : (q._index || 0) + albums.length,
}));
} }
export class Subsonic implements MusicService { export class Subsonic implements MusicService {
@@ -829,177 +982,6 @@ export class Subsonic implements MusicService {
refreshToken = (serviceToken: string) => refreshToken = (serviceToken: string) =>
this.generateToken(parseToken(serviceToken)); this.generateToken(parseToken(serviceToken));
getArtists = (
credentials: Credentials
): Promise<(IdName & { albumCount: number; image: BUrn | undefined })[]> =>
this.getJSON<GetArtistsResponse>(credentials, "/rest/getArtists")
.then((it) => (it.artists.index || []).flatMap((it) => it.artist || []))
.then((artists) =>
artists.map((artist) => ({
id: `${artist.id}`,
name: artist.name,
albumCount: artist.albumCount,
image: artistImageURN({
artistId: artist.id,
artistImageURL: artist.artistImageUrl,
}),
}))
);
getArtistInfo = (
credentials: Credentials,
id: string
): Promise<{
similarArtist: (ArtistSummary & { inLibrary: boolean })[];
images: {
s: string | undefined;
m: string | undefined;
l: string | undefined;
};
}> =>
this.getJSON<GetArtistInfoResponse>(credentials, "/rest/getArtistInfo2", {
id,
count: 50,
includeNotPresent: true,
})
.then((it) => it.artistInfo2)
.then((it) => ({
images: {
s: it.smallImageUrl,
m: it.mediumImageUrl,
l: it.largeImageUrl,
},
similarArtist: (it.similarArtist || []).map((artist) => ({
id: `${artist.id}`,
name: artist.name,
inLibrary: artistIsInLibrary(artist.id),
image: artistImageURN({
artistId: artist.id,
artistImageURL: artist.artistImageUrl,
}),
})),
}));
getAlbum = (credentials: Credentials, id: string): Promise<Album> =>
this.getJSON<GetAlbumResponse>(credentials, "/rest/getAlbum", { id })
.then((it) => it.album)
.then((album) => ({
id: album.id,
name: album.name,
year: album.year,
genre: maybeAsGenre(album.genre),
artistId: album.artistId,
artistName: album.artist,
coverArt: coverArtURN(album.coverArt),
}));
getArtist = (
credentials: Credentials,
id: string
): Promise<
IdName & { artistImageUrl: string | undefined; albums: AlbumSummary[] }
> =>
this.getJSON<GetArtistResponse>(credentials, "/rest/getArtist", {
id,
})
.then((it) => it.artist)
.then((it) => ({
id: it.id,
name: it.name,
artistImageUrl: it.artistImageUrl,
albums: this.toAlbumSummary(it.album || []),
}));
getArtistWithInfo = (credentials: Credentials, id: string) =>
Promise.all([
this.getArtist(credentials, id),
this.getArtistInfo(credentials, id),
]).then(([artist, artistInfo]) => ({
id: artist.id,
name: artist.name,
image: artistImageURN({
artistId: artist.id,
artistImageURL: [
artist.artistImageUrl,
artistInfo.images.l,
artistInfo.images.m,
artistInfo.images.s,
].find(isValidImage),
}),
albums: artist.albums,
similarArtists: artistInfo.similarArtist,
}));
getCoverArt = (credentials: Credentials, id: string, size?: number) =>
this.get(credentials, "/rest/getCoverArt", size ? { id, size } : { id }, {
headers: { "User-Agent": "bonob" },
responseType: "arraybuffer",
});
getTrack = (credentials: Credentials, id: string) =>
this.getJSON<GetSongResponse>(credentials, "/rest/getSong", {
id,
})
.then((it) => it.song)
.then((song) =>
this.getAlbum(credentials, song.albumId!).then((album) =>
asTrack(album, song)
)
);
getStarred = (credentials: Credentials) =>
this.getJSON<GetStarredResponse>(credentials, "/rest/getStarred2").then(
(it) => new Set(it.starred2.song.map((it) => it.id))
);
toAlbumSummary = (albumList: album[]): AlbumSummary[] =>
albumList.map((album) => ({
id: album.id,
name: album.name,
year: album.year,
genre: maybeAsGenre(album.genre),
artistId: album.artistId,
artistName: album.artist,
coverArt: coverArtURN(album.coverArt),
}));
search3 = (credentials: Credentials, q: any) =>
this.getJSON<Search3Response>(credentials, "/rest/search3", {
artistCount: 0,
albumCount: 0,
songCount: 0,
...q,
}).then((it) => ({
artists: it.searchResult3.artist || [],
albums: it.searchResult3.album || [],
songs: it.searchResult3.song || [],
}));
getAlbumList2 = (credentials: Credentials, q: AlbumQuery) =>
Promise.all([
this.getArtists(credentials).then((it) =>
_.inject(it, (total, artist) => total + artist.albumCount, 0)
),
this.getJSON<GetAlbumListResponse>(credentials, "/rest/getAlbumList2", {
type: AlbumQueryTypeToSubsonicType[q.type],
...(q.genre ? { genre: b64Decode(q.genre) } : {}),
size: 500,
offset: q._index,
})
.then((response) => response.albumList2.album || [])
.then(this.toAlbumSummary),
]).then(([total, albums]) => ({
results: albums.slice(0, q._count),
total: albums.length == 500 ? total : (q._index || 0) + albums.length,
}));
// getStarred2 = (credentials: Credentials): Promise<{ albums: Album[] }> =>
// this.getJSON<GetStarredResponse>(credentials, "/rest/getStarred2")
// .then((it) => it.starred2)
// .then((it) => ({
// albums: it.album.map(asAlbum),
// }));
login = async (token: string) => this.libraryFor(parseToken(token)); login = async (token: string) => this.libraryFor(parseToken(token));
private libraryFor = ( private libraryFor = (

View File

@@ -375,11 +375,6 @@ const getAlbumJson = (artist: Artist, album: Album, tracks: Track[]) =>
const getSongJson = (track: Track) => subsonicOK({ song: asSongJson(track) }); const getSongJson = (track: Track) => subsonicOK({ song: asSongJson(track) });
// const getStarredJson = ({ albums }: { albums: Album[] }) => subsonicOK({starred2: {
// album: albums.map(it => asAlbumJson({ id: it.artistId, name: it.artistName }, it, [])),
// song: [],
// }})
const subsonicOK = (body: any = {}) => ({ const subsonicOK = (body: any = {}) => ({
"subsonic-response": { "subsonic-response": {
status: "ok", status: "ok",