Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[enhancement]: Better version checks #514

Merged
merged 6 commits into from
Mar 4, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
202 changes: 55 additions & 147 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,7 @@
"react-virtualized-auto-sizer": "^1.0.17",
"react-window": "^1.8.9",
"react-window-infinite-loader": "^1.0.9",
"semver": "^7.5.4",
"styled-components": "^6.0.8",
"swiper": "^9.3.1",
"zod": "^3.22.3",
Expand Down
3 changes: 1 addition & 2 deletions release/app/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions src/renderer/api/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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,
Expand Down
6 changes: 6 additions & 0 deletions src/renderer/api/features.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export enum ServerFeature {
SMART_PLAYLISTS = 'smartPlaylists',
SONG_LYRICS = 'songLyrics',
}

export type ServerFeatures = Record<Partial<ServerFeature>, boolean>;
2 changes: 1 addition & 1 deletion src/renderer/api/jellyfin/jellyfin-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
12 changes: 11 additions & 1 deletion src/renderer/api/jellyfin/jellyfin-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(',');
Expand Down Expand Up @@ -957,7 +958,16 @@ const getServerInfo = async (args: ServerInfoArgs): Promise<ServerInfo> => {
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 = {
Expand Down
3 changes: 2 additions & 1 deletion src/renderer/api/jellyfin/jellyfin-normalize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
4 changes: 4 additions & 0 deletions src/renderer/api/jellyfin/jellyfin-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -665,6 +665,10 @@ const serverInfo = z.object({
Version: z.string(),
});

export enum JellyfinExtensions {
SONG_LYRICS = 'songLyrics',
}

export const jfType = {
_enum: {
albumArtistList: albumArtistListSort,
Expand Down
2 changes: 1 addition & 1 deletion src/renderer/api/navidrome/navidrome-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down
91 changes: 86 additions & 5 deletions src/renderer/api/navidrome/navidrome-controller.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -39,11 +45,11 @@ 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';
import { ServerFeature, ServerFeatures } from '/@/renderer/api/features.types';

const authenticate = async (
url: string,
Expand Down Expand Up @@ -355,6 +361,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, ServerFeature.SMART_PLAYLISTS)
) {
customQuery.smart = undefined;
}

const res = await ndApiClient(apiClientProps).getPlaylistList({
query: {
Expand All @@ -363,7 +379,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,
},
});

Expand Down Expand Up @@ -465,6 +481,70 @@ const removeFromPlaylist = async (
return null;
};

const VERSION_INFO: Array<[string, Record<string, number[]>]> = [
['0.48.0', { [ServerFeature.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 navidromeFeatures: 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) {
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! };
};

export const ndController = {
addToPlaylist,
authenticate,
Expand All @@ -478,6 +558,7 @@ export const ndController = {
getPlaylistDetail,
getPlaylistList,
getPlaylistSongList,
getServerInfo,
getSongDetail,
getSongList,
getUserList,
Expand Down
3 changes: 2 additions & 1 deletion src/renderer/api/navidrome/navidrome-normalize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
4 changes: 4 additions & 0 deletions src/renderer/api/navidrome/navidrome-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
18 changes: 14 additions & 4 deletions src/renderer/api/subsonic/subsonic-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -381,8 +382,13 @@ const getServerInfo = async (args: ServerInfoArgs): Promise<ServerInfo> => {
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();
Expand All @@ -391,9 +397,13 @@ const getServerInfo = async (args: ServerInfoArgs): Promise<ServerInfo> => {
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) {
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 };
Expand Down
10 changes: 8 additions & 2 deletions src/renderer/api/subsonic/subsonic-normalize.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
6 changes: 6 additions & 0 deletions src/renderer/api/subsonic/subsonic-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
12 changes: 5 additions & 7 deletions src/renderer/api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
NDUserListSort,
NDGenreListSort,
} from './navidrome.types';
import { ServerFeatures } from '/@/renderer/api/features.types';

export enum LibraryItem {
ALBUM = 'album',
Expand Down Expand Up @@ -57,13 +58,16 @@ export type User = {

export type ServerListItem = {
credential: string;
features?: ServerFeatures;
id: string;
name: string;
ndCredential?: string;
savePassword?: boolean;
type: ServerType;
url: string;
userId: string | null;
username: string;
version?: string;
};

export enum ServerType {
Expand Down Expand Up @@ -1141,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<string, number[]>;
features: ServerFeatures;
id?: string;
version: string;
};
Expand Down
Loading