no more subsonic in the library

This commit is contained in:
simojenki
2022-04-23 16:46:52 +10:00
parent 1b14b88fb4
commit 730524d7a1
4 changed files with 165 additions and 155 deletions

View File

@@ -17,7 +17,7 @@ import {
import { b64Encode, b64Decode } from "../b64"; import { b64Encode, b64Decode } from "../b64";
import { axiosImageFetcher, ImageFetcher } from "../images"; import { axiosImageFetcher, ImageFetcher } from "../images";
import { navidromeMusicLibrary, SubsonicGenericMusicLibrary } from "./library"; import { navidromeMusicLibrary, SubsonicGenericMusicLibrary } from "./library";
import { http, getJSON as getJSON2 } from "./http"; import { getJSON as getJSON2 } from "./subsonic_http";
export const t = (password: string, s: string) => export const t = (password: string, s: string) =>
Md5.hashStr(`${password}${s}`); Md5.hashStr(`${password}${s}`);
@@ -61,6 +61,7 @@ export function isError(
return (subsonicResponse as SubsonicError).error !== undefined; return (subsonicResponse as SubsonicError).error !== undefined;
} }
// todo: is this a good name?
export type StreamClientApplication = (track: Track) => string; export type StreamClientApplication = (track: Track) => string;
export const DEFAULT_CLIENT_APPLICATION = "bonob"; export const DEFAULT_CLIENT_APPLICATION = "bonob";
@@ -94,9 +95,12 @@ export interface SubsonicMusicLibrary extends MusicLibrary {
export class Subsonic implements MusicService { export class Subsonic implements MusicService {
url: string; url: string;
// todo: does this need to be in here now?
streamClientApplication: StreamClientApplication; streamClientApplication: StreamClientApplication;
// todo: why is this in here? // todo: why is this in here?
externalImageFetcher: ImageFetcher; externalImageFetcher: ImageFetcher;
base: Http; base: Http;
constructor( constructor(
@@ -114,9 +118,6 @@ export class Subsonic implements MusicService {
}); });
} }
// todo: delete
http = (credentials: Credentials) => http(this.url, credentials);
authenticated = (credentials: Credentials, wrap: Http = this.base) => authenticated = (credentials: Credentials, wrap: Http = this.base) =>
http2(wrap, { http2(wrap, {
params: { params: {
@@ -168,8 +169,7 @@ export class Subsonic implements MusicService {
credentials: SubsonicCredentials credentials: SubsonicCredentials
): Promise<SubsonicMusicLibrary> => { ): Promise<SubsonicMusicLibrary> => {
const subsonicGenericLibrary = new SubsonicGenericMusicLibrary( const subsonicGenericLibrary = new SubsonicGenericMusicLibrary(
this, this.streamClientApplication,
credentials,
this.authenticated(credentials, this.base) this.authenticated(credentials, this.base)
); );
if (credentials.type == "navidrome") { if (credentials.type == "navidrome") {

View File

@@ -27,8 +27,9 @@ import {
Sortable, Sortable,
Track, Track,
} from "../music_service"; } from "../music_service";
import Subsonic, { import {
DODGY_IMAGE_NAME, DODGY_IMAGE_NAME,
StreamClientApplication,
SubsonicCredentials, SubsonicCredentials,
SubsonicMusicLibrary, SubsonicMusicLibrary,
SubsonicResponse, SubsonicResponse,
@@ -38,8 +39,8 @@ import axios from "axios";
import { asURLSearchParams } from "../utils"; import { asURLSearchParams } from "../utils";
import { artistSummaryFromNDArtist, NDArtist } from "./navidrome"; import { artistSummaryFromNDArtist, NDArtist } from "./navidrome";
//todo: rename http2 -> http //todo: rename http2 -> http
import { Http, http as http2 } from "../http"; import { Http, http as http2, RequestParams } from "../http";
import { getRaw2 } from "./http"; import { getRaw2, getJSON as getJSON2 } from "./subsonic_http";
type album = { type album = {
id: string; id: string;
@@ -275,20 +276,22 @@ const maybeAsGenre = (genreName: string | undefined): Genre | undefined =>
); );
export class SubsonicGenericMusicLibrary implements SubsonicMusicLibrary { export class SubsonicGenericMusicLibrary implements SubsonicMusicLibrary {
subsonic: Subsonic; streamClientApplication: StreamClientApplication;
credentials: SubsonicCredentials;
http: Http; http: Http;
constructor( constructor(
subsonic: Subsonic, streamClientApplication: StreamClientApplication,
credentials: SubsonicCredentials,
http: Http http: Http
) { ) {
this.subsonic = subsonic; this.streamClientApplication = streamClientApplication;
this.credentials = credentials;
this.http = http; this.http = http;
} }
GET = (query: Partial<RequestParams>) => ({
asRAW: () => getRaw2(http2(this.http, query)),
asJSON: <T>() => getJSON2<T>(http2(this.http, query)),
});
flavour = () => "subsonic"; flavour = () => "subsonic";
bearerToken = (_: Credentials): TE.TaskEither<Error, string | undefined> => bearerToken = (_: Credentials): TE.TaskEither<Error, string | undefined> =>
@@ -315,8 +318,10 @@ export class SubsonicGenericMusicLibrary implements SubsonicMusicLibrary {
album = (id: string): Promise<Album> => this.getAlbum(id); album = (id: string): Promise<Album> => this.getAlbum(id);
genres = () => genres = () =>
this.subsonic this.GET({
.getJSON<GetGenresResponse>(this.credentials, "/rest/getGenres") url: "/rest/getGenres",
})
.asJSON<GetGenresResponse>()
.then((it) => .then((it) =>
pipe( pipe(
it.genres.genre || [], it.genres.genre || [],
@@ -328,10 +333,13 @@ export class SubsonicGenericMusicLibrary implements SubsonicMusicLibrary {
); );
tracks = (albumId: string) => tracks = (albumId: string) =>
this.subsonic this.GET({
.getJSON<GetAlbumResponse>(this.credentials, "/rest/getAlbum", { url: "/rest/getAlbum",
params: {
id: albumId, id: albumId,
},
}) })
.asJSON<GetAlbumResponse>()
.then((it) => it.album) .then((it) => it.album)
.then((album) => .then((album) =>
(album.song || []).map((song) => asTrack(asAlbum(album), song)) (album.song || []).map((song) => asTrack(asAlbum(album), song))
@@ -352,21 +360,23 @@ export class SubsonicGenericMusicLibrary implements SubsonicMusicLibrary {
const thingsToUpdate = []; const thingsToUpdate = [];
if (track.rating.love != rating.love) { if (track.rating.love != rating.love) {
thingsToUpdate.push( thingsToUpdate.push(
this.subsonic.getJSON( this.GET({
this.credentials, url: `/rest/${rating.love ? "star" : "unstar"}`,
`/rest/${rating.love ? "star" : "unstar"}`, params: {
{
id: trackId, id: trackId,
} },
) }).asJSON()
); );
} }
if (track.rating.stars != rating.stars) { if (track.rating.stars != rating.stars) {
thingsToUpdate.push( thingsToUpdate.push(
this.subsonic.getJSON(this.credentials, `/rest/setRating`, { this.GET({
url: `/rest/setRating`,
params: {
id: trackId, id: trackId,
rating: rating.stars, rating: rating.stars,
}) },
}).asJSON()
); );
} }
return Promise.all(thingsToUpdate); return Promise.all(thingsToUpdate);
@@ -383,12 +393,11 @@ export class SubsonicGenericMusicLibrary implements SubsonicMusicLibrary {
}) => }) =>
// todo: all these headers and stuff can be rolled into httpeee // todo: all these headers and stuff can be rolled into httpeee
this.getTrack(trackId).then((track) => this.getTrack(trackId).then((track) =>
getRaw2( this.GET({
http2(this.http, { url: "/rest/stream",
url: `/rest/stream`,
params: { params: {
id: trackId, id: trackId,
c: this.subsonic.streamClientApplication(track), c: this.streamClientApplication(track),
}, },
headers: pipe( headers: pipe(
range, range,
@@ -403,7 +412,7 @@ export class SubsonicGenericMusicLibrary implements SubsonicMusicLibrary {
), ),
responseType: "stream", responseType: "stream",
}) })
) .asRAW()
.then((res) => ({ .then((res) => ({
status: res.status, status: res.status,
headers: { headers: {
@@ -420,7 +429,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.getCoverArt(this.credentials, it, size)) .then((it) => this.getCoverArt(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"),
@@ -433,20 +442,26 @@ export class SubsonicGenericMusicLibrary implements SubsonicMusicLibrary {
}); });
scrobble = async (id: string) => scrobble = async (id: string) =>
this.subsonic this.GET({
.getJSON(this.credentials, `/rest/scrobble`, { url: `/rest/scrobble`,
params: {
id, id,
submission: true, submission: true,
},
}) })
.asJSON()
.then((_) => true) .then((_) => true)
.catch(() => false); .catch(() => false);
nowPlaying = async (id: string) => nowPlaying = async (id: string) =>
this.subsonic this.GET({
.getJSON(this.credentials, `/rest/scrobble`, { url: `/rest/scrobble`,
params: {
id, id,
submission: false, submission: false,
},
}) })
.asJSON()
.then((_) => true) .then((_) => true)
.catch(() => false); .catch(() => false);
@@ -473,18 +488,21 @@ export class SubsonicGenericMusicLibrary implements SubsonicMusicLibrary {
); );
playlists = async () => playlists = async () =>
this.subsonic this.GET({ url: "/rest/getPlaylists" })
.getJSON<GetPlaylistsResponse>(this.credentials, "/rest/getPlaylists") .asJSON<GetPlaylistsResponse>()
.then((it) => it.playlists.playlist || []) .then((it) => it.playlists.playlist || [])
.then((playlists) => .then((playlists) =>
playlists.map((it) => ({ id: it.id, name: it.name })) playlists.map((it) => ({ id: it.id, name: it.name }))
); );
playlist = async (id: string) => playlist = async (id: string) =>
this.subsonic this.GET({
.getJSON<GetPlaylistResponse>(this.credentials, "/rest/getPlaylist", { url: "/rest/getPlaylist",
params: {
id, id,
},
}) })
.asJSON<GetPlaylistResponse>()
.then((it) => it.playlist) .then((it) => it.playlist)
.then((playlist) => { .then((playlist) => {
let trackNumber = 1; let trackNumber = 1;
@@ -510,43 +528,54 @@ export class SubsonicGenericMusicLibrary implements SubsonicMusicLibrary {
}); });
createPlaylist = async (name: string) => createPlaylist = async (name: string) =>
this.subsonic this.GET({
.getJSON<GetPlaylistResponse>(this.credentials, "/rest/createPlaylist", { url: "/rest/createPlaylist",
params: {
name, name,
},
}) })
.asJSON<GetPlaylistResponse>()
.then((it) => it.playlist) .then((it) => it.playlist)
.then((it) => ({ id: it.id, name: it.name })); .then((it) => ({ id: it.id, name: it.name }));
deletePlaylist = async (id: string) => deletePlaylist = async (id: string) =>
this.subsonic this.GET({
.getJSON<GetPlaylistResponse>(this.credentials, "/rest/deletePlaylist", { url: "/rest/deletePlaylist",
params: {
id, id,
},
}) })
.asJSON<GetPlaylistResponse>()
.then((_) => true); .then((_) => true);
addToPlaylist = async (playlistId: string, trackId: string) => addToPlaylist = async (playlistId: string, trackId: string) =>
this.subsonic this.GET({
.getJSON<GetPlaylistResponse>(this.credentials, "/rest/updatePlaylist", { url: "/rest/updatePlaylist",
params: {
playlistId, playlistId,
songIdToAdd: trackId, songIdToAdd: trackId,
},
}) })
.asJSON<GetPlaylistResponse>()
.then((_) => true); .then((_) => true);
removeFromPlaylist = async (playlistId: string, indicies: number[]) => removeFromPlaylist = async (playlistId: string, indicies: number[]) =>
this.subsonic this.GET({
.getJSON<GetPlaylistResponse>(this.credentials, "/rest/updatePlaylist", { url: "/rest/updatePlaylist",
params: {
playlistId, playlistId,
songIndexToRemove: indicies, songIndexToRemove: indicies,
},
}) })
.asJSON<GetPlaylistResponse>()
.then((_) => true); .then((_) => true);
similarSongs = async (id: string) => similarSongs = async (id: string) =>
this.subsonic this.GET({
.getJSON<GetSimilarSongsResponse>( url: "/rest/getSimilarSongs2",
this.credentials, params: { id, count: 50 },
"/rest/getSimilarSongs2", })
{ id, count: 50 } .asJSON<GetSimilarSongsResponse>()
)
.then((it) => it.similarSongs2.song || []) .then((it) => it.similarSongs2.song || [])
.then((songs) => .then((songs) =>
Promise.all( Promise.all(
@@ -558,11 +587,14 @@ export class SubsonicGenericMusicLibrary implements SubsonicMusicLibrary {
topSongs = async (artistId: string) => topSongs = async (artistId: string) =>
this.getArtist(artistId).then(({ name }) => this.getArtist(artistId).then(({ name }) =>
this.subsonic this.GET({
.getJSON<GetTopSongsResponse>(this.credentials, "/rest/getTopSongs", { url: "/rest/getTopSongs",
params: {
artist: name, artist: name,
count: 50, count: 50,
},
}) })
.asJSON<GetTopSongsResponse>()
.then((it) => it.topSongs.song || []) .then((it) => it.topSongs.song || [])
.then((songs) => .then((songs) =>
Promise.all( Promise.all(
@@ -576,8 +608,8 @@ export class SubsonicGenericMusicLibrary implements SubsonicMusicLibrary {
private getArtists = (): Promise< private getArtists = (): Promise<
(IdName & { albumCount: number; image: BUrn | undefined })[] (IdName & { albumCount: number; image: BUrn | undefined })[]
> => > =>
this.subsonic this.GET({ url: "/rest/getArtists" })
.getJSON<GetArtistsResponse>(this.credentials, "/rest/getArtists") .asJSON<GetArtistsResponse>()
.then((it) => (it.artists.index || []).flatMap((it) => it.artist || [])) .then((it) => (it.artists.index || []).flatMap((it) => it.artist || []))
.then((artists) => .then((artists) =>
artists.map((artist) => ({ artists.map((artist) => ({
@@ -601,16 +633,15 @@ export class SubsonicGenericMusicLibrary implements SubsonicMusicLibrary {
l: string | undefined; l: string | undefined;
}; };
}> => }> =>
this.subsonic this.GET({
.getJSON<GetArtistInfoResponse>( url: "/rest/getArtistInfo2",
this.credentials, params: {
"/rest/getArtistInfo2",
{
id, id,
count: 50, count: 50,
includeNotPresent: true, includeNotPresent: true,
} },
) })
.asJSON<GetArtistInfoResponse>()
.then((it) => it.artistInfo2) .then((it) => it.artistInfo2)
.then((it) => ({ .then((it) => ({
images: { images: {
@@ -630,8 +661,8 @@ export class SubsonicGenericMusicLibrary implements SubsonicMusicLibrary {
})); }));
private getAlbum = (id: string): Promise<Album> => private getAlbum = (id: string): Promise<Album> =>
this.subsonic this.GET({ url: "/rest/getAlbum", params: { id } })
.getJSON<GetAlbumResponse>(this.credentials, "/rest/getAlbum", { id }) .asJSON<GetAlbumResponse>()
.then((it) => it.album) .then((it) => it.album)
.then((album) => ({ .then((album) => ({
id: album.id, id: album.id,
@@ -648,10 +679,13 @@ export class SubsonicGenericMusicLibrary implements SubsonicMusicLibrary {
): Promise< ): Promise<
IdName & { artistImageUrl: string | undefined; albums: AlbumSummary[] } IdName & { artistImageUrl: string | undefined; albums: AlbumSummary[] }
> => > =>
this.subsonic this.GET({
.getJSON<GetArtistResponse>(this.credentials, "/rest/getArtist", { url: "/rest/getArtist",
params: {
id, id,
},
}) })
.asJSON<GetArtistResponse>()
.then((it) => it.artist) .then((it) => it.artist)
.then((it) => ({ .then((it) => ({
id: it.id, id: it.id,
@@ -679,18 +713,21 @@ export class SubsonicGenericMusicLibrary implements SubsonicMusicLibrary {
}) })
); );
private getCoverArt = (credentials: Credentials, id: string, size?: number) => private getCoverArt = (id: string, size?: number) =>
getRaw2(http2(this.subsonic.authenticated(credentials), { this.GET({
url: "/rest/getCoverArt", url: "/rest/getCoverArt",
params: { id, size }, params: { id, size },
responseType: "arraybuffer", responseType: "arraybuffer",
})); }).asRAW();
private getTrack = (id: string) => private getTrack = (id: string) =>
this.subsonic this.GET({
.getJSON<GetSongResponse>(this.credentials, "/rest/getSong", { url: "/rest/getSong",
params: {
id, id,
},
}) })
.asJSON<GetSongResponse>()
.then((it) => it.song) .then((it) => it.song)
.then((song) => .then((song) =>
this.getAlbum(song.albumId!).then((album) => asTrack(album, song)) this.getAlbum(song.albumId!).then((album) => asTrack(album, song))
@@ -708,13 +745,16 @@ export class SubsonicGenericMusicLibrary implements SubsonicMusicLibrary {
})); }));
private search3 = (q: any) => private search3 = (q: any) =>
this.subsonic this.GET({
.getJSON<Search3Response>(this.credentials, "/rest/search3", { url: "/rest/search3",
params: {
artistCount: 0, artistCount: 0,
albumCount: 0, albumCount: 0,
songCount: 0, songCount: 0,
...q, ...q,
},
}) })
.asJSON<Search3Response>()
.then((it) => ({ .then((it) => ({
artists: it.searchResult3.artist || [], artists: it.searchResult3.artist || [],
albums: it.searchResult3.album || [], albums: it.searchResult3.album || [],
@@ -726,17 +766,16 @@ export class SubsonicGenericMusicLibrary implements SubsonicMusicLibrary {
this.getArtists().then((it) => this.getArtists().then((it) =>
inject(it, (total, artist) => total + artist.albumCount, 0) inject(it, (total, artist) => total + artist.albumCount, 0)
), ),
this.subsonic this.GET({
.getJSON<GetAlbumListResponse>( url: "/rest/getAlbumList2",
this.credentials, params: {
"/rest/getAlbumList2",
{
type: AlbumQueryTypeToSubsonicType[q.type], type: AlbumQueryTypeToSubsonicType[q.type],
...(q.genre ? { genre: b64Decode(q.genre) } : {}), ...(q.genre ? { genre: b64Decode(q.genre) } : {}),
size: 500, size: 500,
offset: q._index, offset: q._index,
} },
) })
.asJSON<GetAlbumListResponse>()
.then((response) => response.albumList2.album || []) .then((response) => response.albumList2.album || [])
.then(this.toAlbumSummary), .then(this.toAlbumSummary),
]).then(([total, albums]) => ({ ]).then(([total, albums]) => ({
@@ -758,11 +797,12 @@ export const navidromeMusicLibrary = (
pipe( pipe(
TE.tryCatch( TE.tryCatch(
() => () =>
// todo: not hardcode axios in here
axios({ axios({
method: 'post', method: "post",
baseURL: url, baseURL: url,
url: `/auth/login`, url: `/auth/login`,
data: _.pick(credentials, "username", "password") data: _.pick(credentials, "username", "password"),
}), }),
() => new AuthFailure("Failed to get bearerToken") () => new AuthFailure("Failed to get bearerToken")
), ),

View File

@@ -54,28 +54,6 @@ export interface HTTP {
): Promise<HttpResponse>; ): Promise<HttpResponse>;
} }
// export const basic = (opts : AxiosRequestConfig) => axios(opts);
// function whatDoesItLookLike() {
// const basic = axios;
// const authenticatedClient = httpee(axios, chain(
// baseUrl("http://foobar"),
// subsonicAuth({username: 'bob', password: 'foo'})
// ));
// const jsonClient = httpee(authenticatedClient, formatJson())
// jsonClient({ })
// }
// .then((response) => response.data as SubsonicEnvelope)
// .then((json) => json["subsonic-response"])
// .then((json) => {
// if (isError(json)) throw `Subsonic error:${json.error.message}`;
// else return json as unknown as T;
// });
export const raw = (response: AxiosPromise<any>) => export const raw = (response: AxiosPromise<any>) =>
response response
.catch((e) => { .catch((e) => {
@@ -87,7 +65,6 @@ export const raw = (response: AxiosPromise<any>) =>
} else return response; } else return response;
}); });
// todo: delete
export const getRaw2 = (http: Http) => export const getRaw2 = (http: Http) =>
http({ method: "get" }) http({ method: "get" })
.catch((e) => { .catch((e) => {

View File

@@ -44,7 +44,7 @@ import {
SubsonicGenericMusicLibrary, SubsonicGenericMusicLibrary,
} from "../../src/subsonic/library"; } from "../../src/subsonic/library";
import { EMPTY, error, FAILURE, subsonicOK, ok } from "../subsonic.test"; import { EMPTY, error, FAILURE, subsonicOK, ok } from "../subsonic.test";
import Subsonic, { DODGY_IMAGE_NAME, t } from "../../src/subsonic"; import { DODGY_IMAGE_NAME, t } from "../../src/subsonic";
import { b64Encode } from "../../src/b64"; import { b64Encode } from "../../src/b64";
import { http as http2 } from "../../src/http"; import { http as http2 } from "../../src/http";
@@ -490,7 +490,6 @@ describe("SubsonicGenericMusicLibrary", () => {
const salt = "saltysalty"; const salt = "saltysalty";
const streamClientApplication = jest.fn(); const streamClientApplication = jest.fn();
const subsonic = new Subsonic(url, streamClientApplication)
const authParams = { const authParams = {
u: username, u: username,
@@ -510,13 +509,7 @@ describe("SubsonicGenericMusicLibrary", () => {
}; };
const generic = new SubsonicGenericMusicLibrary( const generic = new SubsonicGenericMusicLibrary(
subsonic, streamClientApplication,
{
username,
password,
type: 'subsonic',
bearer: undefined
},
// todo: all this stuff doesnt need to be defaulted in here. // todo: all this stuff doesnt need to be defaulted in here.
http2(mockAxios, { http2(mockAxios, {
baseURL, baseURL,