mirror of
https://github.com/antebudimir/feishin.git
synced 2025-12-31 18:13:31 +00:00
[feat]: actually include version checks
This commit is contained in:
commit
dae2f9bd0a
36 changed files with 224 additions and 222 deletions
|
|
@ -52,8 +52,8 @@ import type {
|
|||
ServerInfoArgs,
|
||||
StructuredLyricsArgs,
|
||||
StructuredLyric,
|
||||
ServerType,
|
||||
} from '/@/renderer/api/types';
|
||||
import { ServerType } from '/@/renderer/types';
|
||||
import { DeletePlaylistResponse, RandomSongListArgs } from './types';
|
||||
import { ndController } from '/@/renderer/api/navidrome/navidrome-controller';
|
||||
import { ssController } from '/@/renderer/api/subsonic/subsonic-controller';
|
||||
|
|
@ -173,7 +173,7 @@ const endpoints: ApiController = {
|
|||
getPlaylistList: ndController.getPlaylistList,
|
||||
getPlaylistSongList: ndController.getPlaylistSongList,
|
||||
getRandomSongList: ssController.getRandomSongList,
|
||||
getServerInfo: ssController.getServerInfo,
|
||||
getServerInfo: ndController.getServerInfo,
|
||||
getSongDetail: ndController.getSongDetail,
|
||||
getSongList: ndController.getSongList,
|
||||
getStructuredLyrics: ssController.getStructuredLyrics,
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { jfType } from '/@/renderer/api/jellyfin/jellyfin-types';
|
|||
import { initClient, initContract } from '@ts-rest/core';
|
||||
import axios, { AxiosError, AxiosResponse, isAxiosError, Method } from 'axios';
|
||||
import qs from 'qs';
|
||||
import { ServerListItem } from '/@/renderer/types';
|
||||
import { ServerListItem } from '/@/renderer/api/types';
|
||||
import omitBy from 'lodash/omitBy';
|
||||
import { z } from 'zod';
|
||||
import { authenticationFailure } from '/@/renderer/api/utils';
|
||||
|
|
|
|||
|
|
@ -10,8 +10,9 @@ import {
|
|||
Playlist,
|
||||
MusicFolder,
|
||||
Genre,
|
||||
ServerListItem,
|
||||
ServerType,
|
||||
} from '/@/renderer/api/types';
|
||||
import { ServerListItem, ServerType } from '/@/renderer/types';
|
||||
|
||||
const getStreamUrl = (args: {
|
||||
container?: string;
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import qs from 'qs';
|
|||
import { ndType } from './navidrome-types';
|
||||
import { authenticationFailure, resultWithHeaders } from '/@/renderer/api/utils';
|
||||
import { useAuthStore } from '/@/renderer/store';
|
||||
import { ServerListItem } from '/@/renderer/types';
|
||||
import { ServerListItem } from '/@/renderer/api/types';
|
||||
import { toast } from '/@/renderer/components';
|
||||
import i18n from '/@/i18n/i18n';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,9 @@
|
|||
import { ndApiClient } from '/@/renderer/api/navidrome/navidrome-api';
|
||||
import { ndNormalize } from '/@/renderer/api/navidrome/navidrome-normalize';
|
||||
import { NavidromeExtensions, ndType } from '/@/renderer/api/navidrome/navidrome-types';
|
||||
import { ssApiClient } from '/@/renderer/api/subsonic/subsonic-api';
|
||||
import semverCoerce from 'semver/functions/coerce';
|
||||
import semverGte from 'semver/functions/gte';
|
||||
import {
|
||||
AlbumArtistDetailArgs,
|
||||
AlbumArtistDetailResponse,
|
||||
|
|
@ -39,11 +45,10 @@ import {
|
|||
RemoveFromPlaylistResponse,
|
||||
RemoveFromPlaylistArgs,
|
||||
genreListSortMap,
|
||||
ServerInfo,
|
||||
ServerInfoArgs,
|
||||
} from '../types';
|
||||
import { ndApiClient } from '/@/renderer/api/navidrome/navidrome-api';
|
||||
import { ndNormalize } from '/@/renderer/api/navidrome/navidrome-normalize';
|
||||
import { ndType } from '/@/renderer/api/navidrome/navidrome-types';
|
||||
import { ssApiClient } from '/@/renderer/api/subsonic/subsonic-api';
|
||||
import { hasFeature } from '/@/renderer/api/utils';
|
||||
|
||||
const authenticate = async (
|
||||
url: string,
|
||||
|
|
@ -355,6 +360,16 @@ const deletePlaylist = async (args: DeletePlaylistArgs): Promise<DeletePlaylistR
|
|||
|
||||
const getPlaylistList = async (args: PlaylistListArgs): Promise<PlaylistListResponse> => {
|
||||
const { query, apiClientProps } = args;
|
||||
const customQuery = query._custom?.navidrome;
|
||||
|
||||
// Smart playlists only became available in 0.48.0. Do not filter for previous versions
|
||||
if (
|
||||
customQuery &&
|
||||
customQuery.smart !== undefined &&
|
||||
!hasFeature(apiClientProps.server, NavidromeExtensions.SMART_PLAYLISTS)
|
||||
) {
|
||||
customQuery.smart = undefined;
|
||||
}
|
||||
|
||||
const res = await ndApiClient(apiClientProps).getPlaylistList({
|
||||
query: {
|
||||
|
|
@ -363,7 +378,7 @@ const getPlaylistList = async (args: PlaylistListArgs): Promise<PlaylistListResp
|
|||
_sort: query.sortBy ? playlistListSortMap.navidrome[query.sortBy] : undefined,
|
||||
_start: query.startIndex,
|
||||
q: query.searchTerm,
|
||||
...query._custom?.navidrome,
|
||||
...customQuery,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -465,6 +480,61 @@ const removeFromPlaylist = async (
|
|||
return null;
|
||||
};
|
||||
|
||||
const VERSION_INFO: Array<[string, Record<string, number[]>]> = [
|
||||
['0.48.0', { [NavidromeExtensions.SMART_PLAYLISTS]: [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 { apiClientProps } = args;
|
||||
|
||||
// Navidrome will always populate serverVersion
|
||||
const ping = await ssApiClient(apiClientProps).ping();
|
||||
|
||||
if (ping.status !== 200) {
|
||||
throw new Error('Failed to ping server');
|
||||
}
|
||||
|
||||
const features: Record<string, number[]> = getFeatures(ping.body.serverVersion!);
|
||||
|
||||
if (ping.body.openSubsonic) {
|
||||
const res = await ssApiClient(apiClientProps).getServerInfo();
|
||||
|
||||
if (res.status !== 200) {
|
||||
throw new Error('Failed to get server extensions');
|
||||
}
|
||||
|
||||
for (const extension of res.body.openSubsonicExtensions) {
|
||||
features[extension.name] = extension.versions;
|
||||
}
|
||||
}
|
||||
|
||||
return { features, id: apiClientProps.server?.id, version: ping.body.serverVersion! };
|
||||
};
|
||||
|
||||
export const ndController = {
|
||||
addToPlaylist,
|
||||
authenticate,
|
||||
|
|
@ -478,6 +548,7 @@ export const ndController = {
|
|||
getPlaylistDetail,
|
||||
getPlaylistList,
|
||||
getPlaylistSongList,
|
||||
getServerInfo,
|
||||
getSongDetail,
|
||||
getSongList,
|
||||
getUserList,
|
||||
|
|
|
|||
|
|
@ -7,8 +7,9 @@ import {
|
|||
User,
|
||||
AlbumArtist,
|
||||
Genre,
|
||||
ServerListItem,
|
||||
ServerType,
|
||||
} from '/@/renderer/api/types';
|
||||
import { ServerListItem, ServerType } from '/@/renderer/types';
|
||||
import z from 'zod';
|
||||
import { ndType } from './navidrome-types';
|
||||
import { ssType } from '/@/renderer/api/subsonic/subsonic-types';
|
||||
|
|
|
|||
|
|
@ -343,6 +343,10 @@ const removeFromPlaylistParameters = z.object({
|
|||
id: z.array(z.string()),
|
||||
});
|
||||
|
||||
export enum NavidromeExtensions {
|
||||
SMART_PLAYLISTS = 'smartPlaylists',
|
||||
}
|
||||
|
||||
export const ndType = {
|
||||
_enum: {
|
||||
albumArtistList: ndAlbumArtistListSort,
|
||||
|
|
|
|||
|
|
@ -1,8 +1,14 @@
|
|||
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 { ServerListItem, ServerType } from '/@/renderer/types';
|
||||
import {
|
||||
QueueSong,
|
||||
LibraryItem,
|
||||
AlbumArtist,
|
||||
Album,
|
||||
ServerListItem,
|
||||
ServerType,
|
||||
} from '/@/renderer/api/types';
|
||||
|
||||
const getCoverArtUrl = (args: {
|
||||
baseUrl: string | undefined;
|
||||
|
|
|
|||
|
|
@ -57,13 +57,16 @@ export type User = {
|
|||
|
||||
export type ServerListItem = {
|
||||
credential: string;
|
||||
features?: Record<string, number[]>;
|
||||
id: string;
|
||||
name: string;
|
||||
ndCredential?: string;
|
||||
savePassword?: boolean;
|
||||
type: ServerType;
|
||||
url: string;
|
||||
userId: string | null;
|
||||
username: string;
|
||||
version?: string;
|
||||
};
|
||||
|
||||
export enum ServerType {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { AxiosHeaders } from 'axios';
|
|||
import { z } from 'zod';
|
||||
import { toast } from '/@/renderer/components';
|
||||
import { useAuthStore } from '/@/renderer/store';
|
||||
import { ServerListItem } from '/@/renderer/types';
|
||||
import { ServerListItem } from '/@/renderer/api/types';
|
||||
|
||||
// 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) => {
|
||||
|
|
@ -38,3 +38,20 @@ export const authenticationFailure = (currentServer: ServerListItem | null) => {
|
|||
useAuthStore.getState().actions.setCurrentServer(null);
|
||||
}
|
||||
};
|
||||
|
||||
export const hasFeature = (
|
||||
server: ServerListItem | null,
|
||||
feature: string,
|
||||
version = 1,
|
||||
): boolean => {
|
||||
if (!server || !server.features) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const versions = server.features[feature];
|
||||
if (!versions || versions.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return versions.includes(version);
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue