[enhancement]: use jellyfin 10.9.0 lyrics
This commit is contained in:
parent
cb2597d2c8
commit
087ea44737
4 changed files with 63 additions and 37 deletions
|
@ -204,7 +204,7 @@ export const contract = c.router({
|
||||||
},
|
},
|
||||||
getSongLyrics: {
|
getSongLyrics: {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
path: 'users/:userId/Items/:id/Lyrics',
|
path: 'audio/:id/Lyrics',
|
||||||
responses: {
|
responses: {
|
||||||
200: jfType._response.lyrics,
|
200: jfType._response.lyrics,
|
||||||
404: jfType._response.error,
|
404: jfType._response.error,
|
||||||
|
|
|
@ -61,7 +61,8 @@ 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';
|
import { ServerFeature } from '/@/renderer/api/features-types';
|
||||||
|
import { VersionInfo, getFeatures } from '/@/renderer/api/utils';
|
||||||
|
|
||||||
const formatCommaDelimitedString = (value: string[]) => {
|
const formatCommaDelimitedString = (value: string[]) => {
|
||||||
return value.join(',');
|
return value.join(',');
|
||||||
|
@ -937,7 +938,6 @@ const getLyrics = async (args: LyricsArgs): Promise<LyricsResponse> => {
|
||||||
const res = await jfApiClient(apiClientProps).getSongLyrics({
|
const res = await jfApiClient(apiClientProps).getSongLyrics({
|
||||||
params: {
|
params: {
|
||||||
id: query.songId,
|
id: query.songId,
|
||||||
userId: apiClientProps.server?.userId,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -969,6 +969,8 @@ const getSongDetail = async (args: SongDetailArgs): Promise<SongDetailResponse>
|
||||||
return jfNormalize.song(res.body, apiClientProps.server, '');
|
return jfNormalize.song(res.body, apiClientProps.server, '');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const VERSION_INFO: VersionInfo = [['10.9.0', { [ServerFeature.LYRICS_SINGLE_STRUCTURED]: [1] }]];
|
||||||
|
|
||||||
const getServerInfo = async (args: ServerInfoArgs): Promise<ServerInfo> => {
|
const getServerInfo = async (args: ServerInfoArgs): Promise<ServerInfo> => {
|
||||||
const { apiClientProps } = args;
|
const { apiClientProps } = args;
|
||||||
|
|
||||||
|
@ -978,9 +980,7 @@ const getServerInfo = async (args: ServerInfoArgs): Promise<ServerInfo> => {
|
||||||
throw new Error('Failed to get server info');
|
throw new Error('Failed to get server info');
|
||||||
}
|
}
|
||||||
|
|
||||||
const features: ServerFeatures = {
|
const features = getFeatures(VERSION_INFO, res.body.Version);
|
||||||
lyricsSingleStructured: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
features,
|
features,
|
||||||
|
|
|
@ -2,8 +2,6 @@ import { ndApiClient } from '/@/renderer/api/navidrome/navidrome-api';
|
||||||
import { ndNormalize } from '/@/renderer/api/navidrome/navidrome-normalize';
|
import { ndNormalize } from '/@/renderer/api/navidrome/navidrome-normalize';
|
||||||
import { ndType } from '/@/renderer/api/navidrome/navidrome-types';
|
import { ndType } from '/@/renderer/api/navidrome/navidrome-types';
|
||||||
import { ssApiClient } from '/@/renderer/api/subsonic/subsonic-api';
|
import { ssApiClient } from '/@/renderer/api/subsonic/subsonic-api';
|
||||||
import semverCoerce from 'semver/functions/coerce';
|
|
||||||
import semverGte from 'semver/functions/gte';
|
|
||||||
import {
|
import {
|
||||||
AlbumArtistDetailArgs,
|
AlbumArtistDetailArgs,
|
||||||
AlbumArtistDetailResponse,
|
AlbumArtistDetailResponse,
|
||||||
|
@ -52,7 +50,7 @@ import {
|
||||||
SimilarSongsArgs,
|
SimilarSongsArgs,
|
||||||
Song,
|
Song,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
import { hasFeature } from '/@/renderer/api/utils';
|
import { VersionInfo, getFeatures, hasFeature } from '/@/renderer/api/utils';
|
||||||
import { ServerFeature, ServerFeatures } from '/@/renderer/api/features-types';
|
import { ServerFeature, ServerFeatures } from '/@/renderer/api/features-types';
|
||||||
import { SubsonicExtensions } from '/@/renderer/api/subsonic/subsonic-types';
|
import { SubsonicExtensions } from '/@/renderer/api/subsonic/subsonic-types';
|
||||||
import { NDSongListSort } from '/@/renderer/api/navidrome.types';
|
import { NDSongListSort } from '/@/renderer/api/navidrome.types';
|
||||||
|
@ -486,37 +484,11 @@ const removeFromPlaylist = async (
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
// The order should be in decreasing version, as the highest version match
|
const VERSION_INFO: VersionInfo = [
|
||||||
// will automatically consider all lower versions matched
|
|
||||||
const VERSION_INFO: Array<[string, Record<string, number[]>]> = [
|
|
||||||
['0.49.3', { [ServerFeature.SHARING_ALBUM_SONG]: [1] }],
|
['0.49.3', { [ServerFeature.SHARING_ALBUM_SONG]: [1] }],
|
||||||
['0.48.0', { [ServerFeature.PLAYLISTS_SMART]: [1] }],
|
['0.48.0', { [ServerFeature.PLAYLISTS_SMART]: [1] }],
|
||||||
];
|
];
|
||||||
|
|
||||||
const getFeatures = (version: string): Record<string, number[]> => {
|
|
||||||
const cleanVersion = semverCoerce(version);
|
|
||||||
const features: Record<string, number[]> = {};
|
|
||||||
let matched = cleanVersion === null;
|
|
||||||
|
|
||||||
for (const [version, supportedFeatures] of VERSION_INFO) {
|
|
||||||
if (!matched) {
|
|
||||||
matched = semverGte(cleanVersion!, version);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (matched) {
|
|
||||||
for (const [feature, feat] of Object.entries(supportedFeatures)) {
|
|
||||||
if (feature in features) {
|
|
||||||
features[feature].push(...feat);
|
|
||||||
} else {
|
|
||||||
features[feature] = feat;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return features;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getServerInfo = async (args: ServerInfoArgs): Promise<ServerInfo> => {
|
const getServerInfo = async (args: ServerInfoArgs): Promise<ServerInfo> => {
|
||||||
const { apiClientProps } = args;
|
const { apiClientProps } = args;
|
||||||
|
|
||||||
|
@ -527,7 +499,10 @@ const getServerInfo = async (args: ServerInfoArgs): Promise<ServerInfo> => {
|
||||||
throw new Error('Failed to ping server');
|
throw new Error('Failed to ping server');
|
||||||
}
|
}
|
||||||
|
|
||||||
const navidromeFeatures: Record<string, number[]> = getFeatures(ping.body.serverVersion!);
|
const navidromeFeatures: Record<string, number[]> = getFeatures(
|
||||||
|
VERSION_INFO,
|
||||||
|
ping.body.serverVersion!,
|
||||||
|
);
|
||||||
|
|
||||||
if (ping.body.openSubsonic) {
|
if (ping.body.openSubsonic) {
|
||||||
const res = await ssApiClient(apiClientProps).getServerInfo();
|
const res = await ssApiClient(apiClientProps).getServerInfo();
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
import { AxiosHeaders } from 'axios';
|
import { AxiosHeaders } from 'axios';
|
||||||
|
import semverCoerce from 'semver/functions/coerce';
|
||||||
|
import semverGte from 'semver/functions/gte';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { toast } from '/@/renderer/components';
|
import { toast } from '/@/renderer/components';
|
||||||
import { useAuthStore } from '/@/renderer/store';
|
import { useAuthStore } from '/@/renderer/store';
|
||||||
|
@ -48,4 +50,53 @@ export const hasFeature = (server: ServerListItem | null, feature: ServerFeature
|
||||||
return server.features[feature] ?? false;
|
return server.features[feature] ?? false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type VersionInfo = ReadonlyArray<[string, Record<string, readonly number[]>]>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the available server features given the version string.
|
||||||
|
* @param versionInfo a list, in DECREASING VERSION order, of the features supported by the server.
|
||||||
|
* The first version match will automatically consider the rest matched.
|
||||||
|
* @example
|
||||||
|
* ```
|
||||||
|
* // The CORRECT way to order
|
||||||
|
* const VERSION_INFO: VersionInfo = [
|
||||||
|
* ['0.49.3', { [ServerFeature.SHARING_ALBUM_SONG]: [1] }],
|
||||||
|
* ['0.48.0', { [ServerFeature.PLAYLISTS_SMART]: [1] }],
|
||||||
|
* ];
|
||||||
|
* // INCORRECT way to order
|
||||||
|
* const VERSION_INFO: VersionInfo = [
|
||||||
|
* ['0.48.0', { [ServerFeature.PLAYLISTS_SMART]: [1] }],
|
||||||
|
* ['0.49.3', { [ServerFeature.SHARING_ALBUM_SONG]: [1] }],
|
||||||
|
* ];
|
||||||
|
* ```
|
||||||
|
* @param version the version string (SemVer)
|
||||||
|
* @returns a Record containing the matched features (if any) and their versions
|
||||||
|
*/
|
||||||
|
export const getFeatures = (
|
||||||
|
versionInfo: VersionInfo,
|
||||||
|
version: string,
|
||||||
|
): Record<string, number[]> => {
|
||||||
|
const cleanVersion = semverCoerce(version);
|
||||||
|
const features: Record<string, number[]> = {};
|
||||||
|
let matched = cleanVersion === null;
|
||||||
|
|
||||||
|
for (const [version, supportedFeatures] of versionInfo) {
|
||||||
|
if (!matched) {
|
||||||
|
matched = semverGte(cleanVersion!, version);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matched) {
|
||||||
|
for (const [feature, feat] of Object.entries(supportedFeatures)) {
|
||||||
|
if (feature in features) {
|
||||||
|
features[feature].push(...feat);
|
||||||
|
} else {
|
||||||
|
features[feature] = [...feat];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return features;
|
||||||
|
};
|
||||||
|
|
||||||
export const SEPARATOR_STRING = ' · ';
|
export const SEPARATOR_STRING = ' · ';
|
||||||
|
|
Reference in a new issue