Normalize server feature set

This commit is contained in:
jeffvli 2024-03-03 22:15:49 -08:00
parent dae2f9bd0a
commit f1f6ccfd02
9 changed files with 63 additions and 30 deletions

View file

@ -0,0 +1,6 @@
export enum ServerFeature {
SMART_PLAYLISTS = 'smartPlaylists',
SONG_LYRICS = 'songLyrics',
}
export type ServerFeatures = Record<Partial<ServerFeature>, boolean>;

View file

@ -59,6 +59,7 @@ import packageJson from '../../../../package.json';
import { z } from 'zod'; import { z } from 'zod';
import { JFSongListSort, JFSortOrder } from '/@/renderer/api/jellyfin.types'; import { JFSongListSort, JFSortOrder } from '/@/renderer/api/jellyfin.types';
import isElectron from 'is-electron'; import isElectron from 'is-electron';
import { ServerFeatures } from '/@/renderer/api/features.types';
const formatCommaDelimitedString = (value: string[]) => { const formatCommaDelimitedString = (value: string[]) => {
return value.join(','); return value.join(',');
@ -957,7 +958,16 @@ const getServerInfo = async (args: ServerInfoArgs): Promise<ServerInfo> => {
throw new Error('Failed to get server info'); 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 = { export const jfController = {

View file

@ -665,6 +665,10 @@ const serverInfo = z.object({
Version: z.string(), Version: z.string(),
}); });
export enum JellyfinExtensions {
SONG_LYRICS = 'songLyrics',
}
export const jfType = { export const jfType = {
_enum: { _enum: {
albumArtistList: albumArtistListSort, albumArtistList: albumArtistListSort,

View file

@ -49,6 +49,7 @@ import {
ServerInfoArgs, ServerInfoArgs,
} from '../types'; } from '../types';
import { hasFeature } from '/@/renderer/api/utils'; import { hasFeature } from '/@/renderer/api/utils';
import { ServerFeature, ServerFeatures } from '/@/renderer/api/features.types';
const authenticate = async ( const authenticate = async (
url: string, url: string,
@ -366,7 +367,7 @@ const getPlaylistList = async (args: PlaylistListArgs): Promise<PlaylistListResp
if ( if (
customQuery && customQuery &&
customQuery.smart !== undefined && customQuery.smart !== undefined &&
!hasFeature(apiClientProps.server, NavidromeExtensions.SMART_PLAYLISTS) !hasFeature(apiClientProps.server, ServerFeature.SMART_PLAYLISTS)
) { ) {
customQuery.smart = undefined; customQuery.smart = undefined;
} }
@ -481,7 +482,7 @@ const removeFromPlaylist = async (
}; };
const VERSION_INFO: Array<[string, Record<string, number[]>]> = [ const VERSION_INFO: Array<[string, Record<string, number[]>]> = [
['0.48.0', { [NavidromeExtensions.SMART_PLAYLISTS]: [1] }], ['0.48.0', { [ServerFeature.SMART_PLAYLISTS]: [1] }],
]; ];
const getFeatures = (version: string): Record<string, number[]> => { const getFeatures = (version: string): Record<string, number[]> => {
@ -518,7 +519,7 @@ const getServerInfo = async (args: ServerInfoArgs): Promise<ServerInfo> => {
throw new Error('Failed to ping server'); throw new Error('Failed to ping server');
} }
const features: Record<string, number[]> = getFeatures(ping.body.serverVersion!); const navidromeFeatures: Record<string, number[]> = getFeatures(ping.body.serverVersion!);
if (ping.body.openSubsonic) { if (ping.body.openSubsonic) {
const res = await ssApiClient(apiClientProps).getServerInfo(); const res = await ssApiClient(apiClientProps).getServerInfo();
@ -528,10 +529,19 @@ const getServerInfo = async (args: ServerInfoArgs): Promise<ServerInfo> => {
} }
for (const extension of res.body.openSubsonicExtensions) { 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! }; return { features, id: apiClientProps.server?.id, version: ping.body.serverVersion! };
}; };

View file

@ -2,7 +2,7 @@ import md5 from 'md5';
import { z } from 'zod'; import { z } from 'zod';
import { ssApiClient } from '/@/renderer/api/subsonic/subsonic-api'; import { ssApiClient } from '/@/renderer/api/subsonic/subsonic-api';
import { ssNormalize } from '/@/renderer/api/subsonic/subsonic-normalize'; 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 { import {
ArtistInfoArgs, ArtistInfoArgs,
AuthenticationResponse, AuthenticationResponse,
@ -27,6 +27,7 @@ import {
StructuredLyric, StructuredLyric,
} from '/@/renderer/api/types'; } from '/@/renderer/api/types';
import { randomString } from '/@/renderer/utils'; import { randomString } from '/@/renderer/utils';
import { ServerFeatures } from '/@/renderer/api/features.types';
const authenticate = async ( const authenticate = async (
url: string, url: string,
@ -381,8 +382,13 @@ const getServerInfo = async (args: ServerInfoArgs): Promise<ServerInfo> => {
throw new Error('Failed to ping server'); throw new Error('Failed to ping server');
} }
const features: ServerFeatures = {
smartPlaylists: false,
songLyrics: false,
};
if (!ping.body.openSubsonic || !ping.body.serverVersion) { if (!ping.body.openSubsonic || !ping.body.serverVersion) {
return { version: ping.body.version }; return { features, version: ping.body.version };
} }
const res = await ssApiClient(apiClientProps).getServerInfo(); const res = await ssApiClient(apiClientProps).getServerInfo();
@ -391,9 +397,13 @@ const getServerInfo = async (args: ServerInfoArgs): Promise<ServerInfo> => {
throw new Error('Failed to get server extensions'); throw new Error('Failed to get server extensions');
} }
const features: Record<string, number[]> = {}; const subsonicFeatures: Record<string, number[]> = {};
for (const extension of res.body.openSubsonicExtensions) { 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 }; return { features, id: apiClientProps.server?.id, version: ping.body.serverVersion };

View file

@ -247,6 +247,12 @@ const structuredLyrics = z.object({
.optional(), .optional(),
}); });
export enum SubsonicExtensions {
FORM_POST = 'formPost',
SONG_LYRICS = 'songLyrics',
TRANSCODE_OFFSET = 'transcodeOffset',
}
export const ssType = { export const ssType = {
_parameters: { _parameters: {
albumList: albumListParameters, albumList: albumListParameters,

View file

@ -20,6 +20,7 @@ import {
NDUserListSort, NDUserListSort,
NDGenreListSort, NDGenreListSort,
} from './navidrome.types'; } from './navidrome.types';
import { ServerFeatures } from '/@/renderer/api/features.types';
export enum LibraryItem { export enum LibraryItem {
ALBUM = 'album', ALBUM = 'album',
@ -57,7 +58,7 @@ export type User = {
export type ServerListItem = { export type ServerListItem = {
credential: string; credential: string;
features?: Record<string, number[]>; features?: ServerFeatures;
id: string; id: string;
name: string; name: string;
ndCredential?: string; ndCredential?: string;
@ -1144,14 +1145,8 @@ export type FontData = {
export type ServerInfoArgs = BaseEndpointArgs; export type ServerInfoArgs = BaseEndpointArgs;
export enum SubsonicExtensions {
FORM_POST = 'formPost',
SONG_LYRICS = 'songLyrics',
TRANSCODE_OFFSET = 'transcodeOffset',
}
export type ServerInfo = { export type ServerInfo = {
features?: Record<string, number[]>; features: ServerFeatures;
id?: string; id?: string;
version: string; version: string;
}; };

View file

@ -3,6 +3,7 @@ import { z } from 'zod';
import { toast } from '/@/renderer/components'; import { toast } from '/@/renderer/components';
import { useAuthStore } from '/@/renderer/store'; import { useAuthStore } from '/@/renderer/store';
import { ServerListItem } from '/@/renderer/api/types'; 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 // Since ts-rest client returns a strict response type, we need to add the headers to the body object
export const resultWithHeaders = <ItemType extends z.ZodTypeAny>(itemSchema: ItemType) => { export const resultWithHeaders = <ItemType extends z.ZodTypeAny>(itemSchema: ItemType) => {
@ -39,19 +40,10 @@ export const authenticationFailure = (currentServer: ServerListItem | null) => {
} }
}; };
export const hasFeature = ( export const hasFeature = (server: ServerListItem | null, feature: ServerFeature): boolean => {
server: ServerListItem | null,
feature: string,
version = 1,
): boolean => {
if (!server || !server.features) { if (!server || !server.features) {
return false; return false;
} }
const versions = server.features[feature]; return server.features[feature];
if (!versions || versions.length === 0) {
return false;
}
return versions.includes(version);
}; };

View file

@ -6,7 +6,6 @@ import {
InternetProviderLyricResponse, InternetProviderLyricResponse,
FullLyricsMetadata, FullLyricsMetadata,
LyricGetQuery, LyricGetQuery,
SubsonicExtensions,
StructuredLyric, StructuredLyric,
ServerType, ServerType,
} from '/@/renderer/api/types'; } from '/@/renderer/api/types';
@ -16,6 +15,7 @@ import { queryKeys } from '/@/renderer/api/query-keys';
import { api } from '/@/renderer/api'; import { api } from '/@/renderer/api';
import isElectron from 'is-electron'; import isElectron from 'is-electron';
import { hasFeature } from '/@/renderer/api/utils'; import { hasFeature } from '/@/renderer/api/utils';
import { ServerFeature } from '/@/renderer/api/features.types';
const lyricsIpc = isElectron() ? window.electron.lyrics : null; const lyricsIpc = isElectron() ? window.electron.lyrics : null;
@ -113,7 +113,7 @@ export const useSongLyricsBySong = (
source: server?.name ?? 'music server', source: server?.name ?? 'music server',
}; };
} }
} else if (hasFeature(server, SubsonicExtensions.SONG_LYRICS)) { } else if (hasFeature(server, ServerFeature.SONG_LYRICS)) {
const subsonicLyrics = await api.controller const subsonicLyrics = await api.controller
.getStructuredLyrics({ .getStructuredLyrics({
apiClientProps: { server, signal }, apiClientProps: { server, signal },