diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index a4ca8fa3..80be1378 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -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", diff --git a/src/renderer/api/controller.ts b/src/renderer/api/controller.ts index 559c4405..04f23731 100644 --- a/src/renderer/api/controller.ts +++ b/src/renderer/api/controller.ts @@ -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); }, diff --git a/src/renderer/api/navidrome/navidrome-controller.ts b/src/renderer/api/navidrome/navidrome-controller.ts index 4c0c0443..b3887e4b 100644 --- a/src/renderer/api/navidrome/navidrome-controller.ts +++ b/src/renderer/api/navidrome/navidrome-controller.ts @@ -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; diff --git a/src/renderer/api/subsonic/subsonic-api.ts b/src/renderer/api/subsonic/subsonic-api.ts index d40fcc23..23c919d1 100644 --- a/src/renderer/api/subsonic/subsonic-api.ts +++ b/src/renderer/api/subsonic/subsonic-api.ts @@ -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', diff --git a/src/renderer/api/subsonic/subsonic-types.ts b/src/renderer/api/subsonic/subsonic-types.ts index 35fae9f4..96c80105 100644 --- a/src/renderer/api/subsonic/subsonic-types.ts +++ b/src/renderer/api/subsonic/subsonic-types.ts @@ -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, diff --git a/src/renderer/api/types.ts b/src/renderer/api/types.ts index a73b8834..303da366 100644 --- a/src/renderer/api/types.ts +++ b/src/renderer/api/types.ts @@ -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 | null | undefined; @@ -1246,6 +1251,7 @@ export type ControllerEndpoint = { getAlbumArtistList: (args: AlbumArtistListArgs) => Promise; getAlbumArtistListCount: (args: AlbumArtistListArgs) => Promise; getAlbumDetail: (args: AlbumDetailArgs) => Promise; + getAlbumInfo?: (args: AlbumDetailArgs) => Promise; getAlbumList: (args: AlbumListArgs) => Promise; getAlbumListCount: (args: AlbumListArgs) => Promise; // getArtistInfo?: (args: any) => void; diff --git a/src/renderer/features/discord-rpc/use-discord-rpc.ts b/src/renderer/features/discord-rpc/use-discord-rpc.ts index d9eed7cf..12cdacdd 100644 --- a/src/renderer/features/discord-rpc/use-discord-rpc.ts +++ b/src/renderer/features/discord-rpc/use-discord-rpc.ts @@ -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`, ); diff --git a/src/renderer/features/settings/components/window/discord-settings.tsx b/src/renderer/features/settings/components/window/discord-settings.tsx index 065514be..d124ed13 100644 --- a/src/renderer/features/settings/components/window/discord-settings.tsx +++ b/src/renderer/features/settings/components/window/discord-settings.tsx @@ -147,6 +147,32 @@ export const DiscordSettings = () => { postProcess: 'sentenceCase', }), }, + { + control: ( + { + 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: (