From ca695ca155a83711a4f71875cfffa0267464921c Mon Sep 17 00:00:00 2001 From: jeffvli Date: Sun, 3 Dec 2023 16:45:30 -0800 Subject: [PATCH] Add all relevant subsonic endpoints to ts-rest --- src/renderer/api/subsonic/subsonic-api.ts | 407 ++++++- .../api/subsonic/subsonic-normalize.ts | 26 +- src/renderer/api/subsonic/subsonic-types.ts | 1011 ++++++++++++++--- 3 files changed, 1258 insertions(+), 186 deletions(-) diff --git a/src/renderer/api/subsonic/subsonic-api.ts b/src/renderer/api/subsonic/subsonic-api.ts index 5a620f19a..fa5ad2c18 100644 --- a/src/renderer/api/subsonic/subsonic-api.ts +++ b/src/renderer/api/subsonic/subsonic-api.ts @@ -1,93 +1,426 @@ import { initClient, initContract } from '@ts-rest/core'; -import axios, { Method, AxiosError, isAxiosError, AxiosResponse } from 'axios'; +import axios, { AxiosError, AxiosResponse, Method, isAxiosError } from 'axios'; import omitBy from 'lodash/omitBy'; import qs from 'qs'; -import { z } from 'zod'; -import { ssType } from '/@/renderer/api/subsonic/subsonic-types'; +import i18n from '/@/i18n/i18n'; +import { SubsonicApi } 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: { + changePassword: { method: 'GET', - path: 'ping.view', - query: ssType._parameters.authenticate, + path: 'changePassword.view', + query: SubsonicApi.changePassword.parameters, responses: { - 200: ssType._response.authenticate, + 200: SubsonicApi.changePassword.response, }, }, - createFavorite: { + createInternetRadioStation: { method: 'GET', - path: 'star.view', - query: ssType._parameters.createFavorite, + path: 'createInternetRadioStation.view', + query: SubsonicApi.createInternetRadioStation.parameters, responses: { - 200: ssType._response.createFavorite, + 200: SubsonicApi.createInternetRadioStation.response, + }, + }, + createPlaylist: { + method: 'GET', + path: 'createPlaylist.view', + query: SubsonicApi.createPlaylist.parameters, + responses: { + 200: SubsonicApi.createPlaylist.response, + }, + }, + createShare: { + method: 'GET', + path: 'createShare.view', + query: SubsonicApi.createShare.parameters, + responses: { + 200: SubsonicApi.createShare.response, + }, + }, + createUser: { + method: 'GET', + path: 'createUser.view', + query: SubsonicApi.createUser.parameters, + responses: { + 200: SubsonicApi.createUser.response, + }, + }, + deleteInternetRadioStation: { + method: 'GET', + path: 'deleteInternetRadioStation.view', + query: SubsonicApi.deleteInternetRadioStation.parameters, + responses: { + 200: SubsonicApi.deleteInternetRadioStation.response, + }, + }, + deletePlaylist: { + method: 'GET', + path: 'deletePlaylist.view', + query: SubsonicApi.deletePlaylist.parameters, + responses: { + 200: SubsonicApi.deletePlaylist.response, + }, + }, + deleteShare: { + method: 'GET', + path: 'deleteShare.view', + query: SubsonicApi.deleteShare.parameters, + responses: { + 200: SubsonicApi.deleteShare.response, + }, + }, + deleteUser: { + method: 'GET', + path: 'deleteUser.view', + query: SubsonicApi.deleteUser.parameters, + responses: { + 200: SubsonicApi.deleteUser.response, + }, + }, + getAlbum: { + method: 'GET', + path: 'getAlbum.view', + query: SubsonicApi.getAlbum.parameters, + responses: { + 200: SubsonicApi.getAlbum.response, + }, + }, + getAlbumInfo: { + method: 'GET', + path: 'getAlbumInfo.view', + query: SubsonicApi.getAlbumInfo.parameters, + responses: { + 200: SubsonicApi.getAlbumInfo.response, + }, + }, + getAlbumInfo2: { + method: 'GET', + path: 'getAlbumInfo2.view', + query: SubsonicApi.getAlbumInfo2.parameters, + responses: { + 200: SubsonicApi.getAlbumInfo2.response, + }, + }, + getAlbumList: { + method: 'GET', + path: 'getAlbumList.view', + query: SubsonicApi.getAlbumList.parameters, + responses: { + 200: SubsonicApi.getAlbumList.response, + }, + }, + getAlbumList2: { + method: 'GET', + path: 'getAlbumList2.view', + query: SubsonicApi.getAlbumList2.parameters, + responses: { + 200: SubsonicApi.getAlbumList2.response, + }, + }, + getArtist: { + method: 'GET', + path: 'getArtist.view', + query: SubsonicApi.getArtist.parameters, + responses: { + 200: SubsonicApi.getArtist.response, }, }, getArtistInfo: { method: 'GET', path: 'getArtistInfo.view', - query: ssType._parameters.artistInfo, + query: SubsonicApi.getArtistInfo.parameters, responses: { - 200: ssType._response.artistInfo, + 200: SubsonicApi.getArtistInfo.response, }, }, - getMusicFolderList: { + getArtistInfo2: { + method: 'GET', + path: 'getArtistInfo2.view', + query: SubsonicApi.getArtistInfo2.parameters, + responses: { + 200: SubsonicApi.getArtistInfo2.response, + }, + }, + getArtists: { + method: 'GET', + path: 'getArtists.view', + query: SubsonicApi.getArtists.parameters, + responses: { + 200: SubsonicApi.getArtists.response, + }, + }, + getGenres: { + method: 'GET', + path: 'getGenres.view', + query: SubsonicApi.getGenres.parameters, + responses: { + 200: SubsonicApi.getGenres.response, + }, + }, + getIndexes: { + method: 'GET', + path: 'getIndexes.view', + query: SubsonicApi.getIndexes.parameters, + responses: { + 200: SubsonicApi.getIndexes.response, + }, + }, + getInternetRadioStations: { + method: 'GET', + path: 'getInternetRadioStations.view', + query: SubsonicApi.getInternetRadioStations.parameters, + responses: { + 200: SubsonicApi.getInternetRadioStations.response, + }, + }, + getLicense: { + method: 'GET', + path: 'getLicense.view', + query: SubsonicApi.getLicense.parameters, + responses: { + 200: SubsonicApi.getLicense.response, + }, + }, + getLyrics: { + method: 'GET', + path: 'getLyrics.view', + query: SubsonicApi.getLyrics.parameters, + responses: { + 200: SubsonicApi.getLyrics.response, + }, + }, + getMusicDirectory: { + method: 'GET', + path: 'getMusicDirectory.view', + query: SubsonicApi.getMusicDirectory.parameters, + responses: { + 200: SubsonicApi.getMusicDirectory.response, + }, + }, + getMusicFolders: { method: 'GET', path: 'getMusicFolders.view', responses: { - 200: ssType._response.musicFolderList, + 200: SubsonicApi.getMusicFolders.response, }, }, - getRandomSongList: { + getNowPlaying: { + method: 'GET', + path: 'getNowPlaying.view', + query: SubsonicApi.getNowPlaying.parameters, + responses: { + 200: SubsonicApi.getNowPlaying.response, + }, + }, + getOpenSubsonicExtensions: { + method: 'GET', + path: 'getOpenSubsonicExtensions.view', + query: SubsonicApi.getOpenSubsonicExtensions.parameters, + responses: { + 200: SubsonicApi.getOpenSubsonicExtensions.response, + }, + }, + getPlaylist: { + method: 'GET', + path: 'getPlaylist.view', + query: SubsonicApi.getPlaylist.parameters, + responses: { + 200: SubsonicApi.getPlaylist.response, + }, + }, + getPlaylists: { + method: 'GET', + path: 'getPlaylists.view', + query: SubsonicApi.getPlaylists.parameters, + responses: { + 200: SubsonicApi.getPlaylists.response, + }, + }, + getRandomSongs: { method: 'GET', path: 'getRandomSongs.view', - query: ssType._parameters.randomSongList, + query: SubsonicApi.getRandomSongs.parameters, responses: { - 200: ssType._response.randomSongList, + 200: SubsonicApi.getRandomSongs.response, }, }, - getTopSongsList: { + getScanStatus: { + method: 'GET', + path: 'getScanStatus.view', + responses: { + 200: SubsonicApi.getScanStatus.response, + }, + }, + getShares: { + method: 'GET', + path: 'getShares.view', + query: SubsonicApi.getShares.parameters, + responses: { + 200: SubsonicApi.getShares.response, + }, + }, + getSimilarSongs: { + method: 'GET', + path: 'getSimilarSongs.view', + query: SubsonicApi.getSimilarSongs.parameters, + responses: { + 200: SubsonicApi.getSimilarSongs.response, + }, + }, + getSimilarSongs2: { + method: 'GET', + path: 'getSimilarSongs2.view', + query: SubsonicApi.getSimilarSongs2.parameters, + responses: { + 200: SubsonicApi.getSimilarSongs2.response, + }, + }, + getSong: { + method: 'GET', + path: 'getSong.view', + query: SubsonicApi.getSong.parameters, + responses: { + 200: SubsonicApi.getSong.response, + }, + }, + getSongsByGenre: { + method: 'GET', + path: 'getSongsByGenre.view', + query: SubsonicApi.getSongsByGenre.parameters, + responses: { + 200: SubsonicApi.getSongsByGenre.response, + }, + }, + getStarred: { + method: 'GET', + path: 'getStarred.view', + query: SubsonicApi.getStarred.parameters, + responses: { + 200: SubsonicApi.getStarred.response, + }, + }, + getStarred2: { + method: 'GET', + path: 'getStarred2.view', + query: SubsonicApi.getStarred2.parameters, + responses: { + 200: SubsonicApi.getStarred2.response, + }, + }, + getTopSongs: { method: 'GET', path: 'getTopSongs.view', - query: ssType._parameters.topSongsList, + query: SubsonicApi.getTopSongs.parameters, responses: { - 200: ssType._response.topSongsList, + 200: SubsonicApi.getTopSongs.response, }, }, - removeFavorite: { + getUser: { method: 'GET', - path: 'unstar.view', - query: ssType._parameters.removeFavorite, + path: 'getUser.view', + query: SubsonicApi.getUser.parameters, responses: { - 200: ssType._response.removeFavorite, + 200: SubsonicApi.getUser.response, + }, + }, + getUsers: { + method: 'GET', + path: 'getUsers.view', + query: SubsonicApi.getUsers.parameters, + responses: { + 200: SubsonicApi.getUsers.response, + }, + }, + ping: { + method: 'GET', + path: 'ping.view', + query: SubsonicApi.ping.parameters, + responses: { + 200: SubsonicApi.ping.response, }, }, scrobble: { method: 'GET', path: 'scrobble.view', - query: ssType._parameters.scrobble, + query: SubsonicApi.scrobble.parameters, responses: { - 200: ssType._response.scrobble, + 200: SubsonicApi.scrobble.response, }, }, search3: { method: 'GET', path: 'search3.view', - query: ssType._parameters.search3, + query: SubsonicApi.search3.parameters, responses: { - 200: ssType._response.search3, + 200: SubsonicApi.search3.response, }, }, setRating: { method: 'GET', path: 'setRating.view', - query: ssType._parameters.setRating, + query: SubsonicApi.setRating.parameters, responses: { - 200: ssType._response.setRating, + 200: SubsonicApi.setRating.response, + }, + }, + star: { + method: 'GET', + path: 'star.view', + query: SubsonicApi.star.parameters, + responses: { + 200: SubsonicApi.star.response, + }, + }, + startScan: { + method: 'GET', + path: 'startScan.view', + responses: { + 200: SubsonicApi.startScan.response, + }, + }, + unstar: { + method: 'GET', + path: 'unstar.view', + query: SubsonicApi.unstar.parameters, + responses: { + 200: SubsonicApi.unstar.response, + }, + }, + updateInternetRadioStation: { + method: 'GET', + path: 'updateInternetRadioStation.view', + query: SubsonicApi.updateInternetRadioStation.parameters, + responses: { + 200: SubsonicApi.updateInternetRadioStation.response, + }, + }, + updatePlaylist: { + method: 'GET', + path: 'updatePlaylist.view', + query: SubsonicApi.updatePlaylist.parameters, + responses: { + 200: SubsonicApi.updatePlaylist.response, + }, + }, + updateShare: { + method: 'GET', + path: 'updateShare.view', + query: SubsonicApi.updateShare.parameters, + responses: { + 200: SubsonicApi.updateShare.response, + }, + }, + updateUser: { + method: 'GET', + path: 'updateUser.view', + query: SubsonicApi.updateUser.parameters, + responses: { + 200: SubsonicApi.updateUser.response, }, }, }); @@ -110,6 +443,8 @@ axiosClient.interceptors.response.use( title: i18n.t('error.genericError', { postProcess: 'sentenceCase' }) as string, }); } + + return Promise.reject(data['subsonic-response'].error); } return response; @@ -131,7 +466,7 @@ const parsePath = (fullPath: string) => { }; }; -export const ssApiClient = (args: { +export const subsonicApiClient = (args: { server: ServerListItem | null; signal?: AbortSignal; url?: string; @@ -162,9 +497,7 @@ export const ssApiClient = (args: { } try { - const result = await axiosClient.request< - z.infer - >({ + const result = await axiosClient.request({ data: body, headers, method: method as Method, @@ -180,7 +513,7 @@ export const ssApiClient = (args: { }); return { - body: result.data['subsonic-response'], + body: result.data, headers: result.headers as any, status: result.status, }; diff --git a/src/renderer/api/subsonic/subsonic-normalize.ts b/src/renderer/api/subsonic/subsonic-normalize.ts index 881e7fef2..2328515a5 100644 --- a/src/renderer/api/subsonic/subsonic-normalize.ts +++ b/src/renderer/api/subsonic/subsonic-normalize.ts @@ -1,7 +1,7 @@ import { nanoid } from 'nanoid'; import { z } from 'zod'; -import { ssType } from '/@/renderer/api/subsonic/subsonic-types'; -import { QueueSong, LibraryItem, AlbumArtist, Album } from '/@/renderer/api/types'; +import { SubsonicApi } from '/@/renderer/api/subsonic/subsonic-types'; +import { QueueSong, LibraryItem, AlbumArtist, Album, Genre } from '/@/renderer/api/types'; import { ServerListItem, ServerType } from '/@/renderer/types'; const getCoverArtUrl = (args: { @@ -27,7 +27,7 @@ const getCoverArtUrl = (args: { }; const normalizeSong = ( - item: z.infer, + item: z.infer, server: ServerListItem | null, deviceId: string, ): QueueSong => { @@ -105,7 +105,7 @@ const normalizeSong = ( }; const normalizeAlbumArtist = ( - item: z.infer, + item: z.infer, server: ServerListItem | null, ): AlbumArtist => { const imageUrl = @@ -138,7 +138,9 @@ const normalizeAlbumArtist = ( }; const normalizeAlbum = ( - item: z.infer, + item: + | z.infer + | z.infer, server: ServerListItem | null, ): Album => { const imageUrl = @@ -189,8 +191,20 @@ const normalizeAlbum = ( }; }; -export const ssNormalize = { +const normalizeGenre = (item: z.infer): Genre => { + return { + albumCount: item.albumCount, + id: item.value, + imageUrl: null, + itemType: LibraryItem.GENRE, + name: item.value, + songCount: item.songCount, + }; +}; + +export const subsonicNormalize = { album: normalizeAlbum, albumArtist: normalizeAlbumArtist, + genre: normalizeGenre, song: normalizeSong, }; diff --git a/src/renderer/api/subsonic/subsonic-types.ts b/src/renderer/api/subsonic/subsonic-types.ts index 3360081b6..118622be4 100644 --- a/src/renderer/api/subsonic/subsonic-types.ts +++ b/src/renderer/api/subsonic/subsonic-types.ts @@ -1,56 +1,30 @@ import { z } from 'zod'; const baseResponse = z.object({ - 'subsonic-response': z.object({ - status: z.string(), - version: z.string(), - }), + 'subsonic-response': z + .object({ + status: z.string(), + version: z.string(), + }) + // OpenSubsonic v1.0.0 + .extend({ + openSubsonic: z.boolean().optional(), + serverVersion: z.string().optional(), + type: z.string().optional(), + }), }); -const authenticate = z.null(); - -const authenticateParameters = z.object({ - c: z.string(), - f: z.string(), - p: z.string().optional(), - s: z.string().optional(), - t: z.string().optional(), - u: z.string(), - v: z.string(), -}); - -const createFavoriteParameters = z.object({ - albumId: z.array(z.string()).optional(), - artistId: z.array(z.string()).optional(), - id: z.array(z.string()).optional(), -}); - -const createFavorite = z.null(); - -const removeFavoriteParameters = z.object({ - albumId: z.array(z.string()).optional(), - artistId: z.array(z.string()).optional(), - id: z.array(z.string()).optional(), -}); - -const removeFavorite = z.null(); - -const setRatingParameters = z.object({ - id: z.string(), - rating: z.number(), -}); - -const setRating = z.null(); +const baseResponseShape = baseResponse.shape['subsonic-response']; const musicFolder = z.object({ id: z.string(), name: z.string(), }); -const musicFolderList = z.object({ - musicFolders: z.object({ - musicFolder: z.array(musicFolder), - }), +const genre = z.object({ + albumCount: z.number(), + songCount: z.number(), + value: z.string(), }); const song = z.object({ @@ -103,35 +77,82 @@ const album = z.object({ year: z.number().optional(), }); -const albumListParameters = z.object({ - fromYear: z.number().optional(), - genre: z.string().optional(), - musicFolderId: z.string().optional(), - offset: z.number().optional(), - size: z.number().optional(), - toYear: z.number().optional(), - type: z.string().optional(), +const albumListEntry = album.omit({ + song: true, }); -const albumList = z.array(album.omit({ song: true })); +const artist = z + .object({ + album: z.array(album), + albumCount: z.string(), + coverArt: z.string().optional(), + id: z.string(), + name: z.string(), + starred: z.string().optional(), + }) + // Navidrome lastfm extension + .extend({ + artistImageUrl: z.string().optional(), + }); -const albumArtist = z.object({ - albumCount: z.string(), - artistImageUrl: z.string().optional(), +const artistListEntry = artist.pick({ + albumCount: true, + coverArt: true, + id: true, + name: true, + starred: true, +}); + +const playlist = z.object({ + changed: z.string().optional(), + comment: z.string().optional(), coverArt: z.string().optional(), + created: z.string(), + duration: z.number(), + entry: z.array(song), id: z.string(), name: z.string(), + owner: z.string(), + public: z.boolean(), + songCount: z.number(), }); -const albumArtistList = z.object({ - artist: z.array(albumArtist), - name: z.string(), -}); - -const artistInfoParameters = z.object({ - count: z.number().optional(), +const share = z.object({ + created: z.string(), + description: z.string().optional(), + entry: z.array(song), + expires: z.string().optional(), id: z.string(), - includeNotPresent: z.boolean().optional(), + lastVisited: z.string().optional(), + name: z.string(), + public: z.boolean(), + url: z.string(), + username: z.string(), + visitCount: z.number(), +}); + +const user = z.object({ + adminRole: z.boolean(), + commentRole: z.boolean(), + downloadRole: z.boolean(), + email: z.string(), + folder: z.array(z.number()), + jukeboxRole: z.boolean(), + playlistRole: z.boolean(), + podcastRole: z.boolean(), + scrobblingEnabled: z.boolean(), + settingsRole: z.boolean(), + shareRole: z.boolean(), + uploadRole: z.boolean(), + username: z.string(), +}); + +const shareListEntry = share.omit({ + entry: true, +}); + +const playlistListEntry = playlist.omit({ + entry: true, }); const artistInfo = z.object({ @@ -154,87 +175,791 @@ const artistInfo = z.object({ }), }); -const topSongsListParameters = z.object({ - artist: z.string(), // The name of the artist, not the artist ID - count: z.number().optional(), +const albumInfo = z.object({ + largeImageUrl: z.string().optional(), + lastFmUrl: z.string().optional(), + mediumImageUrl: z.string().optional(), + musicBrainzId: z.string().optional(), + notes: z.string().optional(), + smallImageUrl: z.string().optional(), }); -const topSongsList = z.object({ - topSongs: z.object({ - song: z.array(song), - }), -}); - -const scrobbleParameters = z.object({ - id: z.string(), - submission: z.boolean().optional(), - time: z.number().optional(), // The time (in milliseconds since 1 Jan 1970) at which the song was listened to. -}); - -const scrobble = z.null(); - -const search3 = z.object({ - searchResult3: z.object({ - album: z.array(album), - artist: z.array(albumArtist), - song: z.array(song), - }), -}); - -const search3Parameters = z.object({ - albumCount: z.number().optional(), - albumOffset: z.number().optional(), - artistCount: z.number().optional(), - artistOffset: z.number().optional(), - musicFolderId: z.string().optional(), - query: z.string().optional(), - songCount: z.number().optional(), - songOffset: z.number().optional(), -}); - -const randomSongListParameters = z.object({ - fromYear: z.number().optional(), - genre: z.string().optional(), - musicFolderId: z.string().optional(), - size: z.number().optional(), - toYear: z.number().optional(), -}); - -const randomSongList = z.object({ - randomSongs: z.object({ - song: z.array(song), - }), -}); - -export const ssType = { - _parameters: { - albumList: albumListParameters, - artistInfo: artistInfoParameters, - authenticate: authenticateParameters, - createFavorite: createFavoriteParameters, - randomSongList: randomSongListParameters, - removeFavorite: removeFavoriteParameters, - scrobble: scrobbleParameters, - search3: search3Parameters, - setRating: setRatingParameters, - topSongsList: topSongsListParameters, - }, - _response: { - album, - albumArtist, - albumArtistList, - albumList, - artistInfo, - authenticate, - baseResponse, - createFavorite, - musicFolderList, - randomSongList, - removeFavorite, - scrobble, - search3, - setRating, - song, - topSongsList, - }, +const ping = { + parameters: z.object({}), + response: baseResponse, +}; + +const getLicense = { + parameters: z.object({}), + response: z.object({ + 'subsonic-response': baseResponseShape.extend({ + license: z.object({ + email: z.string(), + licenseExpires: z.string(), + trialExpires: z.string(), + valid: z.boolean(), + }), + }), + }), +}; + +const getOpenSubsonicExtensions = { + parameters: z.object({}), + response: z.object({ + 'subsonic-response': baseResponseShape.extend({ + openSubsonicExtensions: z.object({ + name: z.string(), + version: z.array(z.number()), + }), + }), + }), +}; + +const getMusicFolders = { + parameters: z.object({}), + response: z.object({ + 'subsonic-response': baseResponseShape.extend({ + musicFolders: z.object({ + musicFolder: z.array(musicFolder), + }), + }), + }), +}; + +const getIndexes = { + parameters: z.object({ + ifModifiedSince: z.number().optional(), + musicFolderId: z.string().optional(), + }), + response: z.object({ + 'subsonic-response': baseResponseShape.extend({ + indexes: z.object({ + ignoredArticles: z.string(), + index: z.array( + z.object({ + artist: z.array( + z.object({ + albumCount: z.number(), + coverArt: z.string().optional(), + id: z.string(), + name: z.string(), + }), + ), + name: z.string(), + }), + ), + lastModified: z.number(), + }), + }), + }), +}; + +const getMusicDirectory = { + parameters: z.object({ + id: z.string(), + musicFolderId: z.string().optional(), + }), + response: z.object({ + 'subsonic-response': baseResponseShape.extend({ + directory: z.object({ + child: z.array( + z.object({ + artist: z.string().optional(), + coverArt: z.string().optional(), + id: z.string(), + isDir: z.boolean(), + parent: z.string(), + title: z.string(), + }), + ), + childCount: z.number(), + coverArt: z.string().optional(), + id: z.string(), + name: z.string(), + parent: z.string(), + }), + }), + }), +}; + +const getGenres = { + parameters: z.object({}), + response: z.object({ + 'subsonic-response': baseResponseShape.extend({ + genres: z.object({ + genre: z.array(genre), + }), + }), + }), +}; + +const getArtists = { + parameters: z.object({ + musicFolderId: z.string().optional(), + }), + response: z.object({ + 'subsonic-response': baseResponseShape.extend({ + artists: z.object({ + ignoredArticles: z.string(), + index: z.array( + z.object({ + artist: z.array(artistListEntry), + name: z.string(), + }), + ), + }), + }), + }), +}; + +const getArtist = { + parameters: z.object({ + id: z.string(), + }), + response: z.object({ + 'subsonic-response': baseResponseShape.extend({ + artist, + }), + }), +}; + +const getAlbum = { + parameters: z.object({ + id: z.string(), + musicFolderId: z.string().optional(), + }), + response: z.object({ + 'subsonic-response': baseResponseShape.extend({ + album, + }), + }), +}; + +const getSong = { + parameters: z.object({ + id: z.string(), + }), + response: z.object({ + 'subsonic-response': baseResponseShape.extend({ + song, + }), + }), +}; + +const getArtistInfo = { + parameters: z.object({ + count: z.number().optional(), + id: z.string(), + + // Whether to return artists that are not present in the media library. + includeNotPresent: z.boolean().optional(), + }), + response: z.object({ + 'subsonic-response': baseResponseShape.extend({ + artistInfo, + }), + }), +}; + +const getArtistInfo2 = { + parameters: getArtistInfo.parameters, + response: z.object({ + 'subsonic-response': baseResponseShape.extend({ + artistInfo2: artistInfo, + }), + }), +}; + +const getAlbumInfo = { + parameters: z.object({ + id: z.string(), + }), + response: z.object({ + 'subsonic-response': baseResponseShape.extend({ + albumInfo, + }), + }), +}; + +const getAlbumInfo2 = { + parameters: getAlbumInfo.parameters, + response: z.object({ + 'subsonic-response': baseResponseShape.extend({ + albumInfo2: albumInfo, + }), + }), +}; + +const getSimilarSongs = { + parameters: z.object({ + count: z.number().optional(), + id: z.string(), + }), + response: z.object({ + 'subsonic-response': baseResponseShape.extend({ + similarSongs: z.object({ + song: z.array(song), + }), + }), + }), +}; + +const getSimilarSongs2 = { + parameters: getSimilarSongs.parameters, + response: z.object({ + 'subsonic-response': baseResponseShape.extend({ + similarSongs2: z.object({ + song: z.array(song), + }), + }), + }), +}; + +const getTopSongs = { + parameters: z.object({ + artist: z.string(), + count: z.number().optional(), + }), + response: z.object({ + 'subsonic-response': baseResponseShape.extend({ + topSongs: z.object({ + song: z.array(song), + }), + }), + }), +}; + +export enum AlbumListSortType { + ALPHABETICAL_BY_ARTIST = 'alphabeticalByArtist', + ALPHABETICAL_BY_NAME = 'alphabeticalByName', + BY_GENRE = 'byGenre', + BY_YEAR = 'byYear', + FREQUENT = 'frequent', + NEWEST = 'newest', + RANDOM = 'random', + RECENT = 'recent', + STARRED = 'starred', +} + +const getAlbumList = { + enum: { + AlbumListSortType, + }, + parameters: z + .object({ + fromYear: z.number().optional(), + genre: z.string().optional(), + musicFolderId: z.string().optional(), + offset: z.number().optional(), + size: z.number().optional(), + toYear: z.number().optional(), + type: z.nativeEnum(AlbumListSortType), + }) + .refine( + (val) => { + if (val.type === AlbumListSortType.BY_YEAR) { + return val.fromYear !== undefined && val.toYear !== undefined; + } + + return true; + }, + { + message: 'Parameters "fromYear" and "toYear" are required when using sort "byYear"', + }, + ) + .refine( + (val) => { + if (val.type === AlbumListSortType.BY_GENRE) { + return val.genre !== undefined; + } + + return true; + }, + { message: 'Parameter "genre" is required when using sort "byGenre"' }, + ), + response: z.object({ + 'subsonic-response': baseResponseShape.extend({ + albumList: z.object({ + album: z.array(albumListEntry), + }), + }), + }), +}; + +const getAlbumList2 = { + enum: getAlbumList.enum, + parameters: getAlbumList.parameters, + response: z.object({ + 'subsonic-response': baseResponseShape.extend({ + albumList2: z.object({ + album: z.array(albumListEntry), + }), + }), + }), +}; + +const getRandomSongs = { + parameters: z.object({ + fromYear: z.number().optional(), + genre: z.string().optional(), + musicFolderId: z.string().optional(), + size: z.number().optional(), + toYear: z.number().optional(), + }), + response: z.object({ + 'subsonic-response': baseResponseShape.extend({ + randomSongs: z.object({ + song: z.array(song), + }), + }), + }), +}; + +const getSongsByGenre = { + parameters: z.object({ + count: z.number().optional(), + genre: z.string(), + musicFolderId: z.string().optional(), + offset: z.number().optional(), + }), + response: z.object({ + 'subsonic-response': baseResponseShape.extend({ + songsByGenre: z.object({ + song: z.array(song), + }), + }), + }), +}; + +const getNowPlaying = { + parameters: z.object({}), + response: z.object({ + 'subsonic-response': baseResponseShape.extend({ + nowPlaying: z.object({ + entry: z.array( + z.object({ + minutesAgo: z.number(), + playerId: z.number(), + song, + username: z.string(), + }), + ), + }), + }), + }), +}; + +const getStarred = { + parameters: z.object({ + musicFolderId: z.string().optional(), + }), + response: z.object({ + 'subsonic-response': baseResponseShape.extend({ + starred: z.object({ + album: z.array(albumListEntry), + artist: z.array(artistListEntry), + song: z.array(song), + }), + }), + }), +}; + +const getStarred2 = { + parameters: getStarred.parameters, + response: z.object({ + 'subsonic-response': baseResponseShape.extend({ + starred2: z.object({ + album: z.array(albumListEntry), + artist: z.array(artistListEntry), + song: z.array(song), + }), + }), + }), +}; + +const search3 = { + parameters: z.object({ + albumCount: z.number().optional(), + albumOffset: z.number().optional(), + artistCount: z.number().optional(), + artistOffset: z.number().optional(), + musicFolderId: z.string().optional(), + query: z.string(), + songCount: z.number().optional(), + songOffset: z.number().optional(), + }), + response: z.object({ + 'subsonic-response': baseResponseShape.extend({ + searchResult3: z.object({ + album: z.array(albumListEntry), + artist: z.array(artistListEntry), + song: z.array(song), + }), + }), + }), +}; + +const getPlaylists = { + parameters: z.object({ + username: z.string().optional(), + }), + response: z.object({ + 'subsonic-response': baseResponseShape.extend({ + playlists: z.object({ + playlist: z.array(playlistListEntry), + }), + }), + }), +}; + +const getPlaylist = { + parameters: z.object({ + id: z.string(), + }), + response: z.object({ + 'subsonic-response': baseResponseShape.extend({ + playlist, + }), + }), +}; + +const createPlaylist = { + parameters: z.object({ + name: z.string(), + playlistId: z.string().optional(), + songId: z.array(z.string()).optional(), + }), + response: z.object({ + 'subsonic-response': baseResponseShape.extend({ + playlist, + }), + }), +}; + +const updatePlaylist = { + parameters: z.object({ + comment: z.string().optional(), + name: z.string().optional(), + playlistId: z.string(), + public: z.boolean().optional(), + songIdToAdd: z.array(z.string()).optional(), + songIdToRemove: z.array(z.string()).optional(), + }), + response: baseResponse, +}; + +const deletePlaylist = { + parameters: z.object({ + id: z.string(), + }), + response: baseResponse, +}; + +const getLyrics = { + parameters: z.object({ + artist: z.string().optional(), + title: z.string().optional(), + }), + response: z.object({ + 'subsonic-response': baseResponseShape.extend({ + lyrics: z.object({ + artist: z.string(), + title: z.string(), + value: z.string(), + }), + }), + }), +}; + +const star = { + parameters: z.object({ + albumId: z.array(z.string()).optional(), + artistId: z.array(z.string()).optional(), + id: z.array(z.string()).optional(), + }), + response: baseResponse, +}; + +const unstar = { + parameters: z.object({ + albumId: z.array(z.string()).optional(), + artistId: z.array(z.string()).optional(), + id: z.array(z.string()).optional(), + }), + response: baseResponse, +}; + +const setRating = { + parameters: z.object({ + id: z.string(), + rating: z.number(), + }), + response: baseResponse, +}; + +const scrobble = { + parameters: z.object({ + id: z.string(), + submission: z.boolean().optional(), // Whether this is a “submission” or a “now playing” notification. + time: z.number().optional(), // The time (in milliseconds since 1 Jan 1970) at which the song was listened to. + }), + response: baseResponse, +}; + +const getShares = { + parameters: z.object({}), + response: z.object({ + 'subsonic-response': baseResponseShape.extend({ + shares: z.object({ + share: z.array(shareListEntry), + }), + }), + }), +}; + +const createShare = { + parameters: z.object({ + description: z.string().optional(), + expires: z.number().optional(), + id: z.array(z.string()), + }), + response: z.object({ + 'subsonic-response': baseResponseShape.extend({ + shares: z.object({ + share: z.array(shareListEntry), + }), + }), + }), +}; + +const updateShare = { + parameters: z.object({ + description: z.string().optional(), + expires: z.number().optional(), + id: z.string(), + }), + response: baseResponse, +}; + +const deleteShare = { + parameters: z.object({ + id: z.string(), + }), + response: baseResponse, +}; + +const getInternetRadioStations = { + parameters: z.object({}), + response: z.object({ + 'subsonic-response': baseResponseShape.extend({ + internetRadioStations: z.object({ + entry: z.array( + z.object({ + id: z.string(), + name: z.string(), + streamUrl: z.string(), + }), + ), + }), + }), + }), +}; + +const createInternetRadioStation = { + parameters: z.object({ + homePageUrl: z.string().optional(), + name: z.string(), + streamUrl: z.string(), + }), + response: baseResponse, +}; + +const updateInternetRadioStation = { + parameters: z.object({ + homePageUrl: z.string().optional(), + id: z.string(), + name: z.string(), + streamUrl: z.string(), + }), + response: baseResponse, +}; + +const deleteInternetRadioStation = { + parameters: z.object({ + id: z.string(), + }), + response: baseResponse, +}; + +const getUser = { + parameters: z.object({ + username: z.string(), + }), + response: z.object({ + 'subsonic-response': baseResponseShape.extend({ + user, + }), + }), +}; + +const getUsers = { + parameters: z.object({}), + response: z.object({ + 'subsonic-response': baseResponseShape.extend({ + users: z.object({ + user: z.array(user), + }), + }), + }), +}; + +const createUser = { + parameters: z.object({ + adminRole: z.boolean().optional(), + commentRole: z.boolean().optional(), + coverArtRole: z.boolean().optional(), + downloadRole: z.boolean().optional(), + email: z.string(), + folder: z.array(z.number()).optional(), + jukeboxRole: z.boolean().optional(), + ldapAuthenticated: z.boolean().optional(), + musicFolderId: z.array(z.string()).optional(), + password: z.string(), + playlistRole: z.boolean().optional(), + podcastRole: z.boolean().optional(), + scrobblingEnabled: z.boolean().optional(), + settingsRole: z.boolean().optional(), + shareRole: z.boolean().optional(), + streamRole: z.boolean().optional(), + uploadRole: z.boolean().optional(), + username: z.string(), + }), + response: baseResponse, +}; + +const updateUser = { + parameters: z.object({ + adminRole: z.boolean().optional(), + commentRole: z.boolean().optional(), + coverArtRole: z.boolean().optional(), + downloadRole: z.boolean().optional(), + email: z.string().optional(), + folder: z.array(z.number()).optional(), + jukeboxRole: z.boolean().optional(), + ldapAuthenticated: z.boolean().optional(), + musicFolderId: z.array(z.string()).optional(), + password: z.string().optional(), + playlistRole: z.boolean().optional(), + podcastRole: z.boolean().optional(), + scrobblingEnabled: z.boolean().optional(), + settingsRole: z.boolean().optional(), + shareRole: z.boolean().optional(), + streamRole: z.boolean().optional(), + uploadRole: z.boolean().optional(), + username: z.string(), + }), + response: baseResponse, +}; + +const deleteUser = { + parameters: z.object({ + username: z.string(), + }), + response: baseResponse, +}; + +const changePassword = { + parameters: z.object({ + password: z.string(), + }), + response: baseResponse, +}; + +const getScanStatus = { + parameters: z.object({}), + response: z.object({ + 'subsonic-response': baseResponseShape.extend({ + scanStatus: z.object({ + count: z.number(), + scanning: z.boolean(), + }), + }), + }), +}; + +const startScan = { + parameters: z.object({}), + response: baseResponse, +}; + +export const SubsonicApi = { + _baseTypes: { + album, + albumInfo, + albumListEntry, + artist, + artistInfo, + artistListEntry, + baseResponse, + genre, + musicFolder, + playlist, + playlistListEntry, + share, + shareListEntry, + song, + user, + }, + changePassword, + createInternetRadioStation, + createPlaylist, + createShare, + createUser, + deleteInternetRadioStation, + deletePlaylist, + deleteShare, + deleteUser, + getAlbum, + getAlbumInfo, + getAlbumInfo2, + getAlbumList, + getAlbumList2, + getArtist, + getArtistInfo, + getArtistInfo2, + getArtists, + getGenres, + getIndexes, + getInternetRadioStations, + getLicense, + getLyrics, + getMusicDirectory, + getMusicFolders, + getNowPlaying, + getOpenSubsonicExtensions, + getPlaylist, + getPlaylists, + getRandomSongs, + getScanStatus, + getShares, + getSimilarSongs, + getSimilarSongs2, + getSong, + getSongsByGenre, + getStarred, + getStarred2, + getTopSongs, + getUser, + getUsers, + ping, + scrobble, + search3, + setRating, + star, + startScan, + unstar, + updateInternetRadioStation, + updatePlaylist, + updateShare, + updateUser, };