Add image URL generation at runtime to allow for dynamic image sizes (#1439)

* add getImageUrl to domain endpoints

* add new ItemImage component and hooks to generate image url

* add configuration for image resolution based on types
This commit is contained in:
Jeff
2025-12-23 20:18:52 -08:00
committed by GitHub
parent 96f38e597c
commit 25bfb65b6d
39 changed files with 823 additions and 670 deletions
@@ -1,9 +1,16 @@
import isElectron from 'is-electron';
import { useEffect, useMemo } from 'react';
import { useItemImageUrl } from '/@/renderer/components/item-image/item-image';
import { usePlayerEvents } from '/@/renderer/features/player/audio-player/hooks/use-player-events';
import { usePlayer } from '/@/renderer/features/player/context/player-context';
import { usePlaybackSettings, useSettingsStore, useTimestampStoreBase } from '/@/renderer/store';
import {
usePlaybackSettings,
usePlayerSong,
useSettingsStore,
useTimestampStoreBase,
} from '/@/renderer/store';
import { LibraryItem } from '/@/shared/types/domain-types';
import { PlayerStatus, PlayerType } from '/@/shared/types/types';
const mediaSession = navigator.mediaSession;
@@ -13,6 +20,14 @@ export const useMediaSession = () => {
const player = usePlayer();
const skip = useSettingsStore((state) => state.general.skipButtons);
const playbackType = useSettingsStore((state) => state.playback.type);
const currentSong = usePlayerSong();
const imageUrl = useItemImageUrl({
id: currentSong?.id,
imageUrl: currentSong?.imageUrl,
itemType: LibraryItem.SONG,
type: 'itemCard',
});
const isMediaSessionEnabled = useMemo(() => {
// Always enable media session on web
@@ -94,7 +109,7 @@ export const useMediaSession = () => {
mediaSession.metadata = new MediaMetadata({
album: song?.album ?? '',
artist: song?.artistName ?? '',
artwork: song?.imageUrl ? [{ src: song.imageUrl, type: 'image/png' }] : [],
artwork: imageUrl ? [{ src: imageUrl, type: 'image/png' }] : [],
title: song?.name ?? '',
});
},
@@ -107,6 +122,6 @@ export const useMediaSession = () => {
mediaSession.playbackState = status === PlayerStatus.PLAYING ? 'playing' : 'paused';
},
},
[isMediaSessionEnabled, mediaSession],
[isMediaSessionEnabled, mediaSession, imageUrl],
);
};
+18 -21
View File
@@ -1,8 +1,10 @@
import isElectron from 'is-electron';
import { useEffect, useRef } from 'react';
import { useEffect } from 'react';
import { useItemImageUrl } from '/@/renderer/components/item-image/item-image';
import { usePlayerEvents } from '/@/renderer/features/player/audio-player/hooks/use-player-events';
import { usePlayerStore } from '/@/renderer/store';
import { usePlayerSong, usePlayerStore } from '/@/renderer/store';
import { LibraryItem } from '/@/shared/types/domain-types';
import { PlayerShuffle } from '/@/shared/types/types';
const ipc = isElectron() ? window.api.ipc : null;
@@ -11,6 +13,14 @@ const mpris = isElectron() && utils?.isLinux() ? window.api.mpris : null;
export const useMPRIS = () => {
const player = usePlayerStore();
const currentSong = usePlayerSong();
const imageUrl = useItemImageUrl({
id: currentSong?.id,
imageUrl: currentSong?.imageUrl,
itemType: LibraryItem.SONG,
type: 'itemCard',
});
useEffect(() => {
if (!mpris) {
@@ -41,32 +51,19 @@ export const useMPRIS = () => {
};
}, [player]);
const isInitializedRef = useRef(false);
// Update MPRIS when song or imageUrl changes
useEffect(() => {
if (isInitializedRef.current) {
if (!mpris) {
return;
}
isInitializedRef.current = true;
const currentSong = player.getCurrentSong();
if (!currentSong) {
return;
}
mpris?.updateSong(currentSong);
}, [player]);
mpris?.updateSong(currentSong, imageUrl);
}, [currentSong, imageUrl]);
usePlayerEvents(
{
onCurrentSongChange: (properties) => {
if (!mpris) {
return;
}
mpris?.updateSong(properties.song);
onCurrentSongChange: () => {
// The effect above will handle the update when currentSong changes
},
onPlayerProgress: (properties) => {
if (!mpris) {
@@ -1,11 +1,12 @@
import { useCallback, useRef, useState } from 'react';
import { useCallback, useEffect, useRef, useState } from 'react';
import { useItemImageUrl } from '/@/renderer/components/item-image/item-image';
import { usePlayerEvents } from '/@/renderer/features/player/audio-player/hooks/use-player-events';
import { useSendScrobble } from '/@/renderer/features/player/mutations/scrobble-mutation';
import { useAppStore, usePlaybackSettings, usePlayerStore } from '/@/renderer/store';
import { useAppStore, usePlaybackSettings, usePlayerSong, usePlayerStore } from '/@/renderer/store';
import { LogCategory, logFn } from '/@/renderer/utils/logger';
import { logMsg } from '/@/renderer/utils/logger-message';
import { QueueSong, ServerType } from '/@/shared/types/domain-types';
import { LibraryItem, QueueSong, ServerType } from '/@/shared/types/domain-types';
import { PlayerStatus } from '/@/shared/types/types';
/*
@@ -59,7 +60,16 @@ export const useScrobble = () => {
const isScrobbleEnabled = scrobbleSettings?.enabled;
const isPrivateModeEnabled = useAppStore((state) => state.privateMode);
const sendScrobble = useSendScrobble();
const currentSong = usePlayerSong();
const imageUrl = useItemImageUrl({
id: currentSong?.id,
imageUrl: currentSong?.imageUrl,
itemType: LibraryItem.SONG,
type: 'itemCard',
});
const imageUrlRef = useRef<null | string | undefined>(imageUrl);
const [isCurrentSongScrobbled, setIsCurrentSongScrobbled] = useState(false);
const previousSongRef = useRef<QueueSong | undefined>(undefined);
const previousTimestampRef = useRef<number>(0);
@@ -68,6 +78,10 @@ export const useScrobble = () => {
const songChangeTimeoutRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
const notifyTimeoutRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
useEffect(() => {
imageUrlRef.current = imageUrl;
}, [imageUrl]);
const handleScrobbleFromProgress = useCallback(
(properties: { timestamp: number }, prev: { timestamp: number }) => {
if (!isScrobbleEnabled || isPrivateModeEnabled) return;
@@ -198,7 +212,7 @@ export const useScrobble = () => {
new Notification(`${currentSong.name}`, {
body: `${artists}\n${currentSong.album}`,
icon: currentSong.imageUrl || undefined,
icon: imageUrlRef.current || undefined,
silent: true,
});
}