257 lines
7.4 KiB
TypeScript
257 lines
7.4 KiB
TypeScript
import { initClient, initContract } from '@ts-rest/core';
|
|
import axios, { Method, AxiosError, isAxiosError, AxiosResponse } from 'axios';
|
|
import omitBy from 'lodash/omitBy';
|
|
import qs from 'qs';
|
|
import { z } from 'zod';
|
|
import { ssType } from '/@/renderer/api/subsonic/subsonic-types';
|
|
import { ServerListItem } from '/@/renderer/api/types';
|
|
import { toast } from '/@/renderer/components/toast/index';
|
|
import i18n from '/@/i18n/i18n';
|
|
|
|
const c = initContract();
|
|
|
|
export const contract = c.router({
|
|
authenticate: {
|
|
method: 'GET',
|
|
path: 'ping.view',
|
|
query: ssType._parameters.authenticate,
|
|
responses: {
|
|
200: ssType._response.authenticate,
|
|
},
|
|
},
|
|
createFavorite: {
|
|
method: 'GET',
|
|
path: 'star.view',
|
|
query: ssType._parameters.createFavorite,
|
|
responses: {
|
|
200: ssType._response.createFavorite,
|
|
},
|
|
},
|
|
getArtistInfo: {
|
|
method: 'GET',
|
|
path: 'getArtistInfo.view',
|
|
query: ssType._parameters.artistInfo,
|
|
responses: {
|
|
200: ssType._response.artistInfo,
|
|
},
|
|
},
|
|
getMusicFolderList: {
|
|
method: 'GET',
|
|
path: 'getMusicFolders.view',
|
|
responses: {
|
|
200: ssType._response.musicFolderList,
|
|
},
|
|
},
|
|
getRandomSongList: {
|
|
method: 'GET',
|
|
path: 'getRandomSongs.view',
|
|
query: ssType._parameters.randomSongList,
|
|
responses: {
|
|
200: ssType._response.randomSongList,
|
|
},
|
|
},
|
|
getServerInfo: {
|
|
method: 'GET',
|
|
path: 'getOpenSubsonicExtensions.view',
|
|
responses: {
|
|
200: ssType._response.serverInfo,
|
|
},
|
|
},
|
|
getSimilarSongs: {
|
|
method: 'GET',
|
|
path: 'getSimilarSongs',
|
|
query: ssType._parameters.similarSongs,
|
|
responses: {
|
|
200: ssType._response.similarSongs,
|
|
},
|
|
},
|
|
getStructuredLyrics: {
|
|
method: 'GET',
|
|
path: 'getLyricsBySongId.view',
|
|
query: ssType._parameters.structuredLyrics,
|
|
responses: {
|
|
200: ssType._response.structuredLyrics,
|
|
},
|
|
},
|
|
getTopSongsList: {
|
|
method: 'GET',
|
|
path: 'getTopSongs.view',
|
|
query: ssType._parameters.topSongsList,
|
|
responses: {
|
|
200: ssType._response.topSongsList,
|
|
},
|
|
},
|
|
ping: {
|
|
method: 'GET',
|
|
path: 'ping.view',
|
|
responses: {
|
|
200: ssType._response.ping,
|
|
},
|
|
},
|
|
removeFavorite: {
|
|
method: 'GET',
|
|
path: 'unstar.view',
|
|
query: ssType._parameters.removeFavorite,
|
|
responses: {
|
|
200: ssType._response.removeFavorite,
|
|
},
|
|
},
|
|
scrobble: {
|
|
method: 'GET',
|
|
path: 'scrobble.view',
|
|
query: ssType._parameters.scrobble,
|
|
responses: {
|
|
200: ssType._response.scrobble,
|
|
},
|
|
},
|
|
search3: {
|
|
method: 'GET',
|
|
path: 'search3.view',
|
|
query: ssType._parameters.search3,
|
|
responses: {
|
|
200: ssType._response.search3,
|
|
},
|
|
},
|
|
setRating: {
|
|
method: 'GET',
|
|
path: 'setRating.view',
|
|
query: ssType._parameters.setRating,
|
|
responses: {
|
|
200: ssType._response.setRating,
|
|
},
|
|
},
|
|
});
|
|
|
|
const axiosClient = axios.create({});
|
|
|
|
axiosClient.defaults.paramsSerializer = (params) => {
|
|
return qs.stringify(params, { arrayFormat: 'repeat' });
|
|
};
|
|
|
|
axiosClient.interceptors.response.use(
|
|
(response) => {
|
|
const data = response.data;
|
|
if (data['subsonic-response'].status !== 'ok') {
|
|
// Suppress code related to non-linked lastfm or spotify from Navidrome
|
|
if (data['subsonic-response'].error.code !== 0) {
|
|
toast.error({
|
|
message: data['subsonic-response'].error.message,
|
|
title: i18n.t('error.genericError', { postProcess: 'sentenceCase' }) as string,
|
|
});
|
|
}
|
|
}
|
|
|
|
return response;
|
|
},
|
|
(error) => {
|
|
return Promise.reject(error);
|
|
},
|
|
);
|
|
|
|
const parsePath = (fullPath: string) => {
|
|
const [path, params] = fullPath.split('?');
|
|
|
|
const parsedParams = qs.parse(params);
|
|
const notNilParams = omitBy(parsedParams, (value) => value === 'undefined' || value === 'null');
|
|
|
|
return {
|
|
params: notNilParams,
|
|
path,
|
|
};
|
|
};
|
|
|
|
const silentlyTransformResponse = (data: any) => {
|
|
const jsonBody = JSON.parse(data);
|
|
const status = jsonBody ? jsonBody['subsonic-response']?.status : undefined;
|
|
|
|
if (status && status !== 'ok') {
|
|
jsonBody['subsonic-response'].error.code = 0;
|
|
}
|
|
|
|
return jsonBody;
|
|
};
|
|
|
|
export const ssApiClient = (args: {
|
|
server: ServerListItem | null;
|
|
signal?: AbortSignal;
|
|
silent?: boolean;
|
|
url?: string;
|
|
}) => {
|
|
const { server, url, signal, silent } = args;
|
|
|
|
return initClient(contract, {
|
|
api: async ({ path, method, headers, body }) => {
|
|
let baseUrl: string | undefined;
|
|
const authParams: Record<string, any> = {};
|
|
|
|
const { params, path: api } = parsePath(path);
|
|
|
|
if (server) {
|
|
baseUrl = `${server.url}/rest`;
|
|
const token = server.credential;
|
|
const params = token.split(/&?\w=/gm);
|
|
|
|
authParams.u = server.username;
|
|
if (params?.length === 4) {
|
|
authParams.s = params[2];
|
|
authParams.t = params[3];
|
|
} else if (params?.length === 3) {
|
|
authParams.p = params[2];
|
|
}
|
|
} else {
|
|
baseUrl = url;
|
|
}
|
|
|
|
try {
|
|
const result = await axiosClient.request<
|
|
z.infer<typeof ssType._response.baseResponse>
|
|
>({
|
|
data: body,
|
|
headers,
|
|
method: method as Method,
|
|
params: {
|
|
c: 'Feishin',
|
|
f: 'json',
|
|
v: '1.13.0',
|
|
...authParams,
|
|
...params,
|
|
},
|
|
signal,
|
|
// In cases where we have a fallback, don't notify the error
|
|
transformResponse: silent ? silentlyTransformResponse : undefined,
|
|
url: `${baseUrl}/${api}`,
|
|
});
|
|
|
|
return {
|
|
body: result.data['subsonic-response'],
|
|
headers: result.headers as any,
|
|
status: result.status,
|
|
};
|
|
} catch (e: Error | AxiosError | any) {
|
|
if (isAxiosError(e)) {
|
|
if (e.code === 'ERR_NETWORK') {
|
|
throw new Error(
|
|
i18n.t('error.networkError', {
|
|
postProcess: 'sentenceCase',
|
|
}) as string,
|
|
);
|
|
}
|
|
|
|
const error = e as AxiosError;
|
|
const response = error.response as AxiosResponse;
|
|
|
|
return {
|
|
body: response?.data,
|
|
headers: response.headers as any,
|
|
status: response?.status,
|
|
};
|
|
}
|
|
throw e;
|
|
}
|
|
},
|
|
baseHeaders: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
baseUrl: '',
|
|
});
|
|
};
|