From f1f6ccfd02a973bef95540841027e381dd602afe Mon Sep 17 00:00:00 2001 From: jeffvli Date: Sun, 3 Mar 2024 22:15:49 -0800 Subject: [PATCH] Normalize server feature set --- src/renderer/api/features.types.ts | 6 ++++++ .../api/jellyfin/jellyfin-controller.ts | 12 +++++++++++- src/renderer/api/jellyfin/jellyfin-types.ts | 4 ++++ .../api/navidrome/navidrome-controller.ts | 18 ++++++++++++++---- .../api/subsonic/subsonic-controller.ts | 18 ++++++++++++++---- src/renderer/api/subsonic/subsonic-types.ts | 6 ++++++ src/renderer/api/types.ts | 11 +++-------- src/renderer/api/utils.ts | 14 +++----------- .../features/lyrics/queries/lyric-query.ts | 4 ++-- 9 files changed, 63 insertions(+), 30 deletions(-) create mode 100644 src/renderer/api/features.types.ts diff --git a/src/renderer/api/features.types.ts b/src/renderer/api/features.types.ts new file mode 100644 index 00000000..be5606a5 --- /dev/null +++ b/src/renderer/api/features.types.ts @@ -0,0 +1,6 @@ +export enum ServerFeature { + SMART_PLAYLISTS = 'smartPlaylists', + SONG_LYRICS = 'songLyrics', +} + +export type ServerFeatures = Record, boolean>; diff --git a/src/renderer/api/jellyfin/jellyfin-controller.ts b/src/renderer/api/jellyfin/jellyfin-controller.ts index 1d1339ce..385c6ef3 100644 --- a/src/renderer/api/jellyfin/jellyfin-controller.ts +++ b/src/renderer/api/jellyfin/jellyfin-controller.ts @@ -59,6 +59,7 @@ import packageJson from '../../../../package.json'; import { z } from 'zod'; import { JFSongListSort, JFSortOrder } from '/@/renderer/api/jellyfin.types'; import isElectron from 'is-electron'; +import { ServerFeatures } from '/@/renderer/api/features.types'; const formatCommaDelimitedString = (value: string[]) => { return value.join(','); @@ -957,7 +958,16 @@ const getServerInfo = async (args: ServerInfoArgs): Promise => { throw new Error('Failed to get server info'); } - return { id: apiClientProps.server?.id, version: res.body.Version }; + const features: ServerFeatures = { + smartPlaylists: false, + songLyrics: true, + }; + + return { + features, + id: apiClientProps.server?.id, + version: res.body.Version, + }; }; export const jfController = { diff --git a/src/renderer/api/jellyfin/jellyfin-types.ts b/src/renderer/api/jellyfin/jellyfin-types.ts index 3ce22e37..8ed18b8c 100644 --- a/src/renderer/api/jellyfin/jellyfin-types.ts +++ b/src/renderer/api/jellyfin/jellyfin-types.ts @@ -665,6 +665,10 @@ const serverInfo = z.object({ Version: z.string(), }); +export enum JellyfinExtensions { + SONG_LYRICS = 'songLyrics', +} + export const jfType = { _enum: { albumArtistList: albumArtistListSort, diff --git a/src/renderer/api/navidrome/navidrome-controller.ts b/src/renderer/api/navidrome/navidrome-controller.ts index 162be26a..a3fd3a8d 100644 --- a/src/renderer/api/navidrome/navidrome-controller.ts +++ b/src/renderer/api/navidrome/navidrome-controller.ts @@ -49,6 +49,7 @@ import { ServerInfoArgs, } from '../types'; import { hasFeature } from '/@/renderer/api/utils'; +import { ServerFeature, ServerFeatures } from '/@/renderer/api/features.types'; const authenticate = async ( url: string, @@ -366,7 +367,7 @@ const getPlaylistList = async (args: PlaylistListArgs): Promise]> = [ - ['0.48.0', { [NavidromeExtensions.SMART_PLAYLISTS]: [1] }], + ['0.48.0', { [ServerFeature.SMART_PLAYLISTS]: [1] }], ]; const getFeatures = (version: string): Record => { @@ -518,7 +519,7 @@ const getServerInfo = async (args: ServerInfoArgs): Promise => { throw new Error('Failed to ping server'); } - const features: Record = getFeatures(ping.body.serverVersion!); + const navidromeFeatures: Record = getFeatures(ping.body.serverVersion!); if (ping.body.openSubsonic) { const res = await ssApiClient(apiClientProps).getServerInfo(); @@ -528,10 +529,19 @@ const getServerInfo = async (args: ServerInfoArgs): Promise => { } for (const extension of res.body.openSubsonicExtensions) { - features[extension.name] = extension.versions; + navidromeFeatures[extension.name] = extension.versions; } } + const features: ServerFeatures = { + smartPlaylists: false, + songLyrics: true, + }; + + if (navidromeFeatures[NavidromeExtensions.SMART_PLAYLISTS]) { + features[ServerFeature.SMART_PLAYLISTS] = true; + } + return { features, id: apiClientProps.server?.id, version: ping.body.serverVersion! }; }; diff --git a/src/renderer/api/subsonic/subsonic-controller.ts b/src/renderer/api/subsonic/subsonic-controller.ts index b67200e8..457672de 100644 --- a/src/renderer/api/subsonic/subsonic-controller.ts +++ b/src/renderer/api/subsonic/subsonic-controller.ts @@ -2,7 +2,7 @@ import md5 from 'md5'; import { z } from 'zod'; import { ssApiClient } from '/@/renderer/api/subsonic/subsonic-api'; import { ssNormalize } from '/@/renderer/api/subsonic/subsonic-normalize'; -import { ssType } from '/@/renderer/api/subsonic/subsonic-types'; +import { SubsonicExtensions, ssType } from '/@/renderer/api/subsonic/subsonic-types'; import { ArtistInfoArgs, AuthenticationResponse, @@ -27,6 +27,7 @@ import { StructuredLyric, } from '/@/renderer/api/types'; import { randomString } from '/@/renderer/utils'; +import { ServerFeatures } from '/@/renderer/api/features.types'; const authenticate = async ( url: string, @@ -381,8 +382,13 @@ const getServerInfo = async (args: ServerInfoArgs): Promise => { throw new Error('Failed to ping server'); } + const features: ServerFeatures = { + smartPlaylists: false, + songLyrics: false, + }; + if (!ping.body.openSubsonic || !ping.body.serverVersion) { - return { version: ping.body.version }; + return { features, version: ping.body.version }; } const res = await ssApiClient(apiClientProps).getServerInfo(); @@ -391,9 +397,13 @@ const getServerInfo = async (args: ServerInfoArgs): Promise => { throw new Error('Failed to get server extensions'); } - const features: Record = {}; + const subsonicFeatures: Record = {}; for (const extension of res.body.openSubsonicExtensions) { - features[extension.name] = extension.versions; + subsonicFeatures[extension.name] = extension.versions; + } + + if (subsonicFeatures[SubsonicExtensions.SONG_LYRICS]) { + features.songLyrics = true; } return { features, id: apiClientProps.server?.id, version: ping.body.serverVersion }; diff --git a/src/renderer/api/subsonic/subsonic-types.ts b/src/renderer/api/subsonic/subsonic-types.ts index 9005fe8c..b113446f 100644 --- a/src/renderer/api/subsonic/subsonic-types.ts +++ b/src/renderer/api/subsonic/subsonic-types.ts @@ -247,6 +247,12 @@ const structuredLyrics = z.object({ .optional(), }); +export enum SubsonicExtensions { + FORM_POST = 'formPost', + SONG_LYRICS = 'songLyrics', + TRANSCODE_OFFSET = 'transcodeOffset', +} + export const ssType = { _parameters: { albumList: albumListParameters, diff --git a/src/renderer/api/types.ts b/src/renderer/api/types.ts index c8abd9e1..7001d179 100644 --- a/src/renderer/api/types.ts +++ b/src/renderer/api/types.ts @@ -20,6 +20,7 @@ import { NDUserListSort, NDGenreListSort, } from './navidrome.types'; +import { ServerFeatures } from '/@/renderer/api/features.types'; export enum LibraryItem { ALBUM = 'album', @@ -57,7 +58,7 @@ export type User = { export type ServerListItem = { credential: string; - features?: Record; + features?: ServerFeatures; id: string; name: string; ndCredential?: string; @@ -1144,14 +1145,8 @@ export type FontData = { export type ServerInfoArgs = BaseEndpointArgs; -export enum SubsonicExtensions { - FORM_POST = 'formPost', - SONG_LYRICS = 'songLyrics', - TRANSCODE_OFFSET = 'transcodeOffset', -} - export type ServerInfo = { - features?: Record; + features: ServerFeatures; id?: string; version: string; }; diff --git a/src/renderer/api/utils.ts b/src/renderer/api/utils.ts index a24748c6..9e209591 100644 --- a/src/renderer/api/utils.ts +++ b/src/renderer/api/utils.ts @@ -3,6 +3,7 @@ import { z } from 'zod'; import { toast } from '/@/renderer/components'; import { useAuthStore } from '/@/renderer/store'; import { ServerListItem } from '/@/renderer/api/types'; +import { ServerFeature } from '/@/renderer/api/features.types'; // Since ts-rest client returns a strict response type, we need to add the headers to the body object export const resultWithHeaders = (itemSchema: ItemType) => { @@ -39,19 +40,10 @@ export const authenticationFailure = (currentServer: ServerListItem | null) => { } }; -export const hasFeature = ( - server: ServerListItem | null, - feature: string, - version = 1, -): boolean => { +export const hasFeature = (server: ServerListItem | null, feature: ServerFeature): boolean => { if (!server || !server.features) { return false; } - const versions = server.features[feature]; - if (!versions || versions.length === 0) { - return false; - } - - return versions.includes(version); + return server.features[feature]; }; diff --git a/src/renderer/features/lyrics/queries/lyric-query.ts b/src/renderer/features/lyrics/queries/lyric-query.ts index fd6aa5e5..6a862e07 100644 --- a/src/renderer/features/lyrics/queries/lyric-query.ts +++ b/src/renderer/features/lyrics/queries/lyric-query.ts @@ -6,7 +6,6 @@ import { InternetProviderLyricResponse, FullLyricsMetadata, LyricGetQuery, - SubsonicExtensions, StructuredLyric, ServerType, } from '/@/renderer/api/types'; @@ -16,6 +15,7 @@ import { queryKeys } from '/@/renderer/api/query-keys'; import { api } from '/@/renderer/api'; import isElectron from 'is-electron'; import { hasFeature } from '/@/renderer/api/utils'; +import { ServerFeature } from '/@/renderer/api/features.types'; const lyricsIpc = isElectron() ? window.electron.lyrics : null; @@ -113,7 +113,7 @@ export const useSongLyricsBySong = ( source: server?.name ?? 'music server', }; } - } else if (hasFeature(server, SubsonicExtensions.SONG_LYRICS)) { + } else if (hasFeature(server, ServerFeature.SONG_LYRICS)) { const subsonicLyrics = await api.controller .getStructuredLyrics({ apiClientProps: { server, signal },