From 9634d7c50d1c3f561bc4d01cef2b83b04253d9dc Mon Sep 17 00:00:00 2001 From: Kendall Garner <17521368+kgarner7@users.noreply.github.com> Date: Sun, 7 Dec 2025 03:28:49 -0800 Subject: [PATCH] feat(subsonic): support form post (#1333) * feat(subsonic): support form post --------- Co-authored-by: jeffvli --- src/renderer/api/subsonic/subsonic-api.ts | 53 ++++++++++++------- .../api/subsonic/subsonic-controller.ts | 4 ++ src/shared/types/features-types.ts | 1 + 3 files changed, 38 insertions(+), 20 deletions(-) diff --git a/src/renderer/api/subsonic/subsonic-api.ts b/src/renderer/api/subsonic/subsonic-api.ts index e23f759ab..587bdbea5 100644 --- a/src/renderer/api/subsonic/subsonic-api.ts +++ b/src/renderer/api/subsonic/subsonic-api.ts @@ -1,13 +1,15 @@ import { initClient, initContract } from '@ts-rest/core'; -import axios, { AxiosError, AxiosResponse, isAxiosError, Method } from 'axios'; +import axios, { AxiosError, AxiosRequestConfig, AxiosResponse, isAxiosError } from 'axios'; import omitBy from 'lodash/omitBy'; import qs from 'qs'; import { z } from 'zod'; import i18n from '/@/i18n/i18n'; import { ssType } from '/@/shared/api/subsonic/subsonic-types'; +import { hasFeature } from '/@/shared/api/utils'; import { toast } from '/@/shared/components/toast/toast'; import { ServerListItemWithCredential } from '/@/shared/types/domain-types'; +import { ServerFeature } from '/@/shared/types/features-types'; const c = initContract(); @@ -312,7 +314,7 @@ export const ssApiClient = (args: { const { server, signal, silent, url } = args; return initClient(contract, { - api: async ({ body, headers, method, path }) => { + api: async ({ headers, method, path }) => { let baseUrl: string | undefined; const authParams: Record = {}; @@ -334,25 +336,36 @@ export const ssApiClient = (args: { baseUrl = url; } + const request: AxiosRequestConfig = { + headers, + signal, + // In cases where we have a fallback, don't notify the error + transformResponse: silent ? silentlyTransformResponse : undefined, + url: `${baseUrl}/${api}`, + }; + + const data = { + c: 'Feishin', + f: 'json', + v: '1.13.0', + ...authParams, + ...params, + }; + + if (hasFeature(server, ServerFeature.OS_FORM_POST)) { + headers['Content-Type'] = 'application/x-www-form-urlencoded'; + request.method = 'POST'; + request.data = qs.stringify(data, { arrayFormat: 'repeat' }); + } else { + request.method = method; + request.params = data; + } + try { - const result = await axiosClient.request< - z.infer - >({ - 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}`, - }); + const result = + await axiosClient.request>( + request, + ); return { body: result.data['subsonic-response'], diff --git a/src/renderer/api/subsonic/subsonic-controller.ts b/src/renderer/api/subsonic/subsonic-controller.ts index f9bf203cc..fec76811a 100644 --- a/src/renderer/api/subsonic/subsonic-controller.ts +++ b/src/renderer/api/subsonic/subsonic-controller.ts @@ -997,6 +997,10 @@ export const SubsonicController: InternalControllerEndpoint = { features.lyricsMultipleStructured = [1]; } + if (subsonicFeatures[SubsonicExtensions.FORM_POST]) { + features.formPost = [1]; + } + return { features, id: apiClientProps.server?.id, version: ping.body.serverVersion }; }, getSimilarSongs: async (args) => { diff --git a/src/shared/types/features-types.ts b/src/shared/types/features-types.ts index 8d82cff37..0b1d8469c 100644 --- a/src/shared/types/features-types.ts +++ b/src/shared/types/features-types.ts @@ -5,6 +5,7 @@ export enum ServerFeature { LYRICS_MULTIPLE_STRUCTURED = 'lyricsMultipleStructured', LYRICS_SINGLE_STRUCTURED = 'lyricsSingleStructured', MUSIC_FOLDER_MULTISELECT = 'musicFolderMultiselect', + OS_FORM_POST = 'osFormPost', PLAYLISTS_SMART = 'playlistsSmart', PUBLIC_PLAYLIST = 'publicPlaylist', SHARING_ALBUM_SONG = 'sharingAlbumSong',