Normalize server feature set
This commit is contained in:
parent
dae2f9bd0a
commit
f1f6ccfd02
9 changed files with 63 additions and 30 deletions
6
src/renderer/api/features.types.ts
Normal file
6
src/renderer/api/features.types.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
export enum ServerFeature {
|
||||||
|
SMART_PLAYLISTS = 'smartPlaylists',
|
||||||
|
SONG_LYRICS = 'songLyrics',
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ServerFeatures = Record<Partial<ServerFeature>, boolean>;
|
|
@ -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 = {
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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! };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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 };
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
|
@ -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);
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -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 },
|
||||||
|
|
Reference in a new issue