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",
"discordRichPresence": "{{discord}} rich presence",
"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_description": "the time in seconds between each update (minimum 15 seconds)",
"doubleClickBehavior": "queue all searched tracks when double clicking",

View file

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

View file

@ -242,6 +242,26 @@ export const NavidromeController: ControllerEndpoint = {
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) => {
const { query, apiClientProps } = args;

View file

@ -51,6 +51,14 @@ export const contract = c.router({
200: ssType._response.getAlbum,
},
},
getAlbumInfo2: {
method: 'GET',
path: 'getAlbumInfo2.view',
query: ssType._parameters.albumInfo,
responses: {
200: ssType._response.albumInfo,
},
},
getAlbumList2: {
method: 'GET',
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 = {
_parameters: {
albumInfo: albumInfoParameters,
albumList: albumListParameters,
artistInfo: artistInfoParameters,
authenticate: authenticateParameters,
@ -555,6 +571,7 @@ export const ssType = {
album,
albumArtist,
albumArtistList,
albumInfo,
albumList,
albumListEntry,
artistInfo,

View file

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

View file

@ -2,6 +2,7 @@
import isElectron from 'is-electron';
import { useCallback, useEffect, useRef } from 'react';
import {
getServerById,
useCurrentSong,
useCurrentStatus,
useDiscordSetttings,
@ -11,6 +12,7 @@ import {
import { SetActivity } from '@xhayper/discord-rpc';
import { PlayerStatus } from '/@/renderer/types';
import { ServerType } from '/@/renderer/api/types';
import { controller } from '/@/renderer/api/controller';
const discordRpc = isElectron() ? window.electron.discordRpc : null;
@ -61,16 +63,34 @@ export const useDiscordRpc = () => {
activity.smallImageKey = 'paused';
}
if (
song?.serverType === ServerType.JELLYFIN &&
discordSettings.showServerImage &&
song?.imageUrl
) {
activity.largeImageKey = song?.imageUrl;
if (discordSettings.showServerImage && song) {
if (song.serverType === ServerType.JELLYFIN && song.imageUrl) {
activity.largeImageKey = song.imageUrl;
} else if (song.serverType === ServerType.NAVIDROME) {
const server = getServerById(song.serverId);
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) {
console.log('Fetching album info for', song.album, song.albumArtists[0].name);
if (
activity.largeImageKey === undefined &&
generalSettings.lastfmApiKey &&
song?.album &&
song?.albumArtists.length
) {
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`,
);

View file

@ -147,6 +147,32 @@ export const DiscordSettings = () => {
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: (
<TextInput