navidrome cover art workaround

This commit is contained in:
Kendall Garner 2025-05-15 19:10:15 -07:00
parent a8fb7ff11e
commit 39c714a137
No known key found for this signature in database
GPG key ID: 9355F387FE765C94
8 changed files with 110 additions and 8 deletions

View file

@ -513,6 +513,8 @@
"discordListening_description": "show status as listening instead of playing", "discordListening_description": "show status as listening instead of playing",
"discordRichPresence": "{{discord}} rich presence", "discordRichPresence": "{{discord}} rich presence",
"discordRichPresence_description": "enable playback status in {{discord}} rich presence. Image keys are: {{icon}}, {{playing}}, and {{paused}} ", "discordRichPresence_description": "enable playback status in {{discord}} rich presence. Image keys are: {{icon}}, {{playing}}, and {{paused}} ",
"discordServeImage": "serve {{discord}} images from server",
"discordServeImage_description": "share cover art for {{discord}} rich presence from server itself, only available for jellyfin and navidrome",
"discordUpdateInterval": "{{discord}} rich presence update interval", "discordUpdateInterval": "{{discord}} rich presence update interval",
"discordUpdateInterval_description": "the time in seconds between each update (minimum 15 seconds)", "discordUpdateInterval_description": "the time in seconds between each update (minimum 15 seconds)",
"doubleClickBehavior": "queue all searched tracks when double clicking", "doubleClickBehavior": "queue all searched tracks when double clicking",

View file

@ -117,6 +117,9 @@ export const controller: GeneralController = {
getMusicFolderList(args) { getMusicFolderList(args) {
return apiController('getMusicFolderList', args.apiClientProps.server?.type)?.(args); return apiController('getMusicFolderList', args.apiClientProps.server?.type)?.(args);
}, },
getAlbumInfo(args) {
return apiController('getAlbumInfo', args.apiClientProps.server?.type)?.(args);
},
getPlaylistDetail(args) { getPlaylistDetail(args) {
return apiController('getPlaylistDetail', args.apiClientProps.server?.type)?.(args); return apiController('getPlaylistDetail', args.apiClientProps.server?.type)?.(args);
}, },

View file

@ -242,6 +242,26 @@ export const NavidromeController: ControllerEndpoint = {
apiClientProps.server, apiClientProps.server,
); );
}, },
getAlbumInfo: async (args) => {
const { query, apiClientProps } = args;
const albumInfo = await ssApiClient(apiClientProps).getAlbumInfo2({
query: {
id: query.id,
},
});
if (albumInfo.status !== 200) {
throw new Error('Failed to get album info');
}
const info = albumInfo.body.albumInfo;
return {
imageUrl: info.largeImageUrl || info.mediumImageUrl || info.smallImageUrl || null,
notes: info.notes || null,
};
},
getAlbumList: async (args) => { getAlbumList: async (args) => {
const { query, apiClientProps } = args; const { query, apiClientProps } = args;

View file

@ -51,6 +51,14 @@ export const contract = c.router({
200: ssType._response.getAlbum, 200: ssType._response.getAlbum,
}, },
}, },
getAlbumInfo2: {
method: 'GET',
path: 'getAlbumInfo2.view',
query: ssType._parameters.albumInfo,
responses: {
200: ssType._response.albumInfo,
},
},
getAlbumList2: { getAlbumList2: {
method: 'GET', method: 'GET',
path: 'getAlbumList2.view', path: 'getAlbumList2.view',

View file

@ -522,8 +522,24 @@ const getAlbumList2 = z.object({
}), }),
}); });
const albumInfoParameters = z.object({
id: z.string(),
});
const albumInfo = z.object({
albumInfo: z.object({
largeImageUrl: z.string().optional(),
lastFmUrl: z.string().optional(),
mediumImageUrl: z.string().optional(),
musicBrainzId: z.string().optional(),
notes: z.string().optional(),
smallImageUrl: z.string().optional(),
}),
});
export const ssType = { export const ssType = {
_parameters: { _parameters: {
albumInfo: albumInfoParameters,
albumList: albumListParameters, albumList: albumListParameters,
artistInfo: artistInfoParameters, artistInfo: artistInfoParameters,
authenticate: authenticateParameters, authenticate: authenticateParameters,
@ -555,6 +571,7 @@ export const ssType = {
album, album,
albumArtist, albumArtist,
albumArtistList, albumArtistList,
albumInfo,
albumList, albumList,
albumListEntry, albumListEntry,
artistInfo, artistInfo,

View file

@ -465,6 +465,11 @@ export type AlbumDetailQuery = { id: string };
export type AlbumDetailArgs = { query: AlbumDetailQuery } & BaseEndpointArgs; export type AlbumDetailArgs = { query: AlbumDetailQuery } & BaseEndpointArgs;
export type AlbumInfo = {
imageUrl: string | null;
notes: string | null;
};
// Song List // Song List
export type SongListResponse = BasePaginatedResponse<Song[]> | null | undefined; export type SongListResponse = BasePaginatedResponse<Song[]> | null | undefined;
@ -1246,6 +1251,7 @@ export type ControllerEndpoint = {
getAlbumArtistList: (args: AlbumArtistListArgs) => Promise<AlbumArtistListResponse>; getAlbumArtistList: (args: AlbumArtistListArgs) => Promise<AlbumArtistListResponse>;
getAlbumArtistListCount: (args: AlbumArtistListArgs) => Promise<number>; getAlbumArtistListCount: (args: AlbumArtistListArgs) => Promise<number>;
getAlbumDetail: (args: AlbumDetailArgs) => Promise<AlbumDetailResponse>; getAlbumDetail: (args: AlbumDetailArgs) => Promise<AlbumDetailResponse>;
getAlbumInfo?: (args: AlbumDetailArgs) => Promise<AlbumInfo>;
getAlbumList: (args: AlbumListArgs) => Promise<AlbumListResponse>; getAlbumList: (args: AlbumListArgs) => Promise<AlbumListResponse>;
getAlbumListCount: (args: AlbumListArgs) => Promise<number>; getAlbumListCount: (args: AlbumListArgs) => Promise<number>;
// getArtistInfo?: (args: any) => void; // getArtistInfo?: (args: any) => void;

View file

@ -2,6 +2,7 @@
import isElectron from 'is-electron'; import isElectron from 'is-electron';
import { useCallback, useEffect, useRef } from 'react'; import { useCallback, useEffect, useRef } from 'react';
import { import {
getServerById,
useCurrentSong, useCurrentSong,
useCurrentStatus, useCurrentStatus,
useDiscordSetttings, useDiscordSetttings,
@ -11,6 +12,7 @@ import {
import { SetActivity } from '@xhayper/discord-rpc'; import { SetActivity } from '@xhayper/discord-rpc';
import { PlayerStatus } from '/@/renderer/types'; import { PlayerStatus } from '/@/renderer/types';
import { ServerType } from '/@/renderer/api/types'; import { ServerType } from '/@/renderer/api/types';
import { controller } from '/@/renderer/api/controller';
const discordRpc = isElectron() ? window.electron.discordRpc : null; const discordRpc = isElectron() ? window.electron.discordRpc : null;
@ -61,16 +63,34 @@ export const useDiscordRpc = () => {
activity.smallImageKey = 'paused'; activity.smallImageKey = 'paused';
} }
if ( if (discordSettings.showServerImage && song) {
song?.serverType === ServerType.JELLYFIN && if (song.serverType === ServerType.JELLYFIN && song.imageUrl) {
discordSettings.showServerImage && activity.largeImageKey = song.imageUrl;
song?.imageUrl } else if (song.serverType === ServerType.NAVIDROME) {
) { const server = getServerById(song.serverId);
activity.largeImageKey = song?.imageUrl;
try {
const info = await controller.getAlbumInfo({
apiClientProps: { server },
query: { id: song.albumId },
});
if (info.imageUrl) {
console.log(info.imageUrl);
activity.largeImageKey = info.imageUrl;
}
} catch {
/* empty */
}
}
} }
if (generalSettings.lastfmApiKey && song?.album && song?.albumArtists.length) { if (
console.log('Fetching album info for', song.album, song.albumArtists[0].name); activity.largeImageKey === undefined &&
generalSettings.lastfmApiKey &&
song?.album &&
song?.albumArtists.length
) {
const albumInfo = await fetch( const albumInfo = await fetch(
`https://ws.audioscrobbler.com/2.0/?method=album.getinfo&api_key=${generalSettings.lastfmApiKey}&artist=${encodeURIComponent(song.albumArtists[0].name)}&album=${encodeURIComponent(song.album)}&format=json`, `https://ws.audioscrobbler.com/2.0/?method=album.getinfo&api_key=${generalSettings.lastfmApiKey}&artist=${encodeURIComponent(song.albumArtists[0].name)}&album=${encodeURIComponent(song.album)}&format=json`,
); );

View file

@ -147,6 +147,32 @@ export const DiscordSettings = () => {
postProcess: 'sentenceCase', postProcess: 'sentenceCase',
}), }),
}, },
{
control: (
<Switch
checked={settings.showServerImage}
onChange={(e) => {
setSettings({
discord: {
...settings,
showServerImage: e.currentTarget.checked,
},
});
}}
/>
),
description: t('setting.discordServeImage', {
context: 'description',
discord: 'Discord',
postProcess: 'sentenceCase',
}),
isHidden: !isElectron(),
title: t('setting.discordServeImage', {
discord: 'Discord',
postProcess: 'sentenceCase',
}),
},
{ {
control: ( control: (
<TextInput <TextInput