From 2c5659b425a7be330f40889852d4912009dfb265 Mon Sep 17 00:00:00 2001 From: Frank Stellmacher <171614930+Psychotoxical@users.noreply.github.com> Date: Tue, 12 May 2026 20:17:59 +0200 Subject: [PATCH] =?UTF-8?q?refactor(player):=20E.31=20=E2=80=94=20extract?= =?UTF-8?q?=20Last.fm=20love=20actions=20as=20factory=20(#595)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four Last.fm-related actions move out of the playerStore create() body into `src/store/lastfmActions.ts` as a `createLastfmActions(set, get)` factory: - `toggleLastfmLove` — flip love + write through to the cache map - `setLastfmLoved` — force-set (used by external events) - `setLastfmLovedForSong` — write cache for an arbitrary title/artist - `syncLastfmLovedTracks` — startup bulk fetch + merge The store body uses `...createLastfmActions(set, get)` to inline them back into the actions map. First action-factory cut — sets the pattern for follow-up Phase E extractions of larger action groups (playback transport, queue mutators, etc.). Side-cleanup: 3 last.fm imports drop from playerStore (`lastfmLoveTrack`, `lastfmUnloveTrack`, `lastfmGetAllLovedTracks`) since they only fed the moved actions. playerStore 1550 → 1504 LOC. --- src/store/lastfmActions.ts | 87 ++++++++++++++++++++++++++++++++++++++ src/store/playerStore.ts | 52 ++--------------------- 2 files changed, 90 insertions(+), 49 deletions(-) create mode 100644 src/store/lastfmActions.ts diff --git a/src/store/lastfmActions.ts b/src/store/lastfmActions.ts new file mode 100644 index 00000000..084b85cd --- /dev/null +++ b/src/store/lastfmActions.ts @@ -0,0 +1,87 @@ +import { + lastfmGetAllLovedTracks, + lastfmLoveTrack, + lastfmUnloveTrack, +} from '../api/lastfm'; +import { useAuthStore } from './authStore'; +import type { PlayerState } from './playerStoreTypes'; + +type SetState = ( + partial: Partial | ((state: PlayerState) => Partial), +) => void; +type GetState = () => PlayerState; + +/** + * Four Last.fm love-related actions, factored out of the playerStore + * `create()` body so the action set can be tested + reasoned about + * separately: + * + * - `toggleLastfmLove` — flip the current track's love state on the + * server, write through to the local cache map keyed by + * `${title}::${artist}` so other queue rows showing the same song + * update too. + * - `setLastfmLoved` — force-set the boolean (used by the + * `track:lastfm-loved` SSE-style event). Updates the cache when a + * current track exists. + * - `setLastfmLovedForSong` — write the cache for an arbitrary + * title/artist pair (used by the QueuePanel love button on + * not-yet-current tracks). + * - `syncLastfmLovedTracks` — startup-time bulk fetch of the user's + * loved-tracks list, merged into the local cache (local likes win + * on conflict) plus a recompute of the current track's flag. + */ +export function createLastfmActions(set: SetState, get: GetState): Pick< + PlayerState, + 'toggleLastfmLove' | 'setLastfmLoved' | 'setLastfmLovedForSong' | 'syncLastfmLovedTracks' +> { + return { + toggleLastfmLove: () => { + const { currentTrack, lastfmLoved } = get(); + const { lastfmSessionKey } = useAuthStore.getState(); + if (!currentTrack || !lastfmSessionKey) return; + const newLoved = !lastfmLoved; + const cacheKey = `${currentTrack.title}::${currentTrack.artist}`; + set(s => ({ lastfmLoved: newLoved, lastfmLovedCache: { ...s.lastfmLovedCache, [cacheKey]: newLoved } })); + if (newLoved) { + lastfmLoveTrack(currentTrack, lastfmSessionKey); + } else { + lastfmUnloveTrack(currentTrack, lastfmSessionKey); + } + }, + + setLastfmLoved: (v) => { + const { currentTrack } = get(); + if (currentTrack) { + const cacheKey = `${currentTrack.title}::${currentTrack.artist}`; + set(s => ({ lastfmLoved: v, lastfmLovedCache: { ...s.lastfmLovedCache, [cacheKey]: v } })); + } else { + set({ lastfmLoved: v }); + } + }, + + syncLastfmLovedTracks: async () => { + const { lastfmSessionKey, lastfmUsername } = useAuthStore.getState(); + if (!lastfmSessionKey || !lastfmUsername) return; + const tracks = await lastfmGetAllLovedTracks(lastfmUsername, lastfmSessionKey); + const newCache: Record = {}; + for (const t of tracks) newCache[`${t.title}::${t.artist}`] = true; + // Merge with existing cache (local likes take precedence) + set(s => ({ lastfmLovedCache: { ...newCache, ...s.lastfmLovedCache } })); + // Update current track's loved state if it's in the new cache + const { currentTrack } = get(); + if (currentTrack) { + const loved = newCache[`${currentTrack.title}::${currentTrack.artist}`] ?? false; + set({ lastfmLoved: loved }); + } + }, + + setLastfmLovedForSong: (title, artist, v) => { + const cacheKey = `${title}::${artist}`; + const isCurrentTrack = get().currentTrack?.title === title && get().currentTrack?.artist === artist; + set(s => ({ + lastfmLovedCache: { ...s.lastfmLovedCache, [cacheKey]: v }, + ...(isCurrentTrack ? { lastfmLoved: v } : {}), + })); + }, + }; +} diff --git a/src/store/playerStore.ts b/src/store/playerStore.ts index a73cd890..596df221 100644 --- a/src/store/playerStore.ts +++ b/src/store/playerStore.ts @@ -6,7 +6,7 @@ import i18n from '../i18n'; import { buildStreamUrl, getPlayQueue, savePlayQueue, reportNowPlaying, getSong, getSimilarSongs2, getTopSongs, setRating } from '../api/subsonic'; import { resolvePlaybackUrl } from '../utils/resolvePlaybackUrl'; import { setDeferHotCachePrefetch } from '../utils/hotCacheGate'; -import { lastfmUpdateNowPlaying, lastfmLoveTrack, lastfmUnloveTrack, lastfmGetTrackLoved, lastfmGetAllLovedTracks } from '../api/lastfm'; +import { lastfmUpdateNowPlaying, lastfmGetTrackLoved } from '../api/lastfm'; import { useAuthStore } from './authStore'; import { useOfflineStore } from './offlineStore'; import { useHotCacheStore } from './hotCacheStore'; @@ -174,6 +174,7 @@ export { import type { PlayerState, Track } from './playerStoreTypes'; export type { PlayerState, Track }; import { applyQueueHistorySnapshot } from './applyQueueHistorySnapshot'; +import { createLastfmActions } from './lastfmActions'; // ─── Module-level playback primitives ───────────────────────────────────────── @@ -257,54 +258,7 @@ export const usePlayerStore = create()( }, toggleFullscreen: () => set(state => ({ isFullscreenOpen: !state.isFullscreenOpen })), - toggleLastfmLove: () => { - const { currentTrack, lastfmLoved } = get(); - const { lastfmSessionKey } = useAuthStore.getState(); - if (!currentTrack || !lastfmSessionKey) return; - const newLoved = !lastfmLoved; - const cacheKey = `${currentTrack.title}::${currentTrack.artist}`; - set(s => ({ lastfmLoved: newLoved, lastfmLovedCache: { ...s.lastfmLovedCache, [cacheKey]: newLoved } })); - if (newLoved) { - lastfmLoveTrack(currentTrack, lastfmSessionKey); - } else { - lastfmUnloveTrack(currentTrack, lastfmSessionKey); - } - }, - - setLastfmLoved: (v) => { - const { currentTrack } = get(); - if (currentTrack) { - const cacheKey = `${currentTrack.title}::${currentTrack.artist}`; - set(s => ({ lastfmLoved: v, lastfmLovedCache: { ...s.lastfmLovedCache, [cacheKey]: v } })); - } else { - set({ lastfmLoved: v }); - } - }, - - syncLastfmLovedTracks: async () => { - const { lastfmSessionKey, lastfmUsername } = useAuthStore.getState(); - if (!lastfmSessionKey || !lastfmUsername) return; - const tracks = await lastfmGetAllLovedTracks(lastfmUsername, lastfmSessionKey); - const newCache: Record = {}; - for (const t of tracks) newCache[`${t.title}::${t.artist}`] = true; - // Merge with existing cache (local likes take precedence) - set(s => ({ lastfmLovedCache: { ...newCache, ...s.lastfmLovedCache } })); - // Update current track's loved state if it's in the new cache - const { currentTrack } = get(); - if (currentTrack) { - const loved = newCache[`${currentTrack.title}::${currentTrack.artist}`] ?? false; - set({ lastfmLoved: loved }); - } - }, - - setLastfmLovedForSong: (title, artist, v) => { - const cacheKey = `${title}::${artist}`; - const isCurrentTrack = get().currentTrack?.title === title && get().currentTrack?.artist === artist; - set(s => ({ - lastfmLovedCache: { ...s.lastfmLovedCache, [cacheKey]: v }, - ...(isCurrentTrack ? { lastfmLoved: v } : {}), - })); - }, + ...createLastfmActions(set, get), toggleRepeat: () => set(state => { const modes = ['off', 'all', 'one'] as const;