mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-22 07:15:47 +00:00
6e4ebca938
Four-cut cluster pulling the Smart-playlist machinery out of Playlists.tsx. 763 → 480 LOC (−283). runPlaylistsOpenSmartEditor — open-existing flow: tries ndGetSmartPlaylist first (freshest rules), falls back to ndListSmartPlaylists if that fails or doesn't return the playlist; populates the editor with parsed filters or a name-only seed for shared / migrated edge cases; degrades gracefully with a warning toast if everything fails. runPlaylistsSaveSmart — create / update flow: dedupes the base name against existing playlists by appending `-2`, `-3` … on creation (skipped on edit); builds rules via buildSmartRulesPayload; calls ndCreate or ndUpdate; tracks the result in pendingSmart so the polling hook can observe rules processing on the server. usePendingSmartPolling — every 10 s polls fetchPlaylists + getPlaylist for each pending item; rehydrates the playlist store when the detail endpoint reports fresh metadata before the list endpoint catches up; stops polling an item when it has songs + its cover changed (or after ~3 minutes hard timeout). PlaylistsSmartEditor — the full smart-editor card (three sections: Basic / Genres / Years + Filters). Owns no state of its own; every input is a controlled component against smartFilters via setSmartFilters. The cancel button still resets through the page's setters. Playlists drops the inline definitions plus its direct '../api/navidromeSmart' import (now consumed inside the two orchestrators). Pure code move otherwise.
86 lines
3.3 KiB
TypeScript
86 lines
3.3 KiB
TypeScript
import type React from 'react';
|
|
import type { TFunction } from 'i18next';
|
|
import { ndCreateSmartPlaylist, ndUpdateSmartPlaylist } from '../api/navidromeSmart';
|
|
import type { SubsonicPlaylist } from '../api/subsonicTypes';
|
|
import { usePlaylistStore } from '../store/playlistStore';
|
|
import {
|
|
buildSmartRulesPayload, defaultSmartFilters, SMART_PREFIX,
|
|
type PendingSmartPlaylist, type SmartFilters,
|
|
} from './playlistsSmart';
|
|
import { showToast } from './toast';
|
|
|
|
export interface RunPlaylistsSaveSmartDeps {
|
|
isNavidromeServer: boolean;
|
|
smartFilters: SmartFilters;
|
|
editingSmartId: string | null;
|
|
playlists: SubsonicPlaylist[];
|
|
fetchPlaylists: () => Promise<void>;
|
|
t: TFunction;
|
|
setPendingSmart: React.Dispatch<React.SetStateAction<PendingSmartPlaylist[]>>;
|
|
setCreatingSmart: React.Dispatch<React.SetStateAction<boolean>>;
|
|
setEditingSmartId: React.Dispatch<React.SetStateAction<string | null>>;
|
|
setSmartFilters: React.Dispatch<React.SetStateAction<SmartFilters>>;
|
|
setGenreQuery: React.Dispatch<React.SetStateAction<string>>;
|
|
setCreatingSmartBusy: React.Dispatch<React.SetStateAction<boolean>>;
|
|
}
|
|
|
|
export async function runPlaylistsSaveSmart(deps: RunPlaylistsSaveSmartDeps): Promise<void> {
|
|
const {
|
|
isNavidromeServer, smartFilters, editingSmartId, playlists, fetchPlaylists, t,
|
|
setPendingSmart, setCreatingSmart, setEditingSmartId, setSmartFilters,
|
|
setGenreQuery, setCreatingSmartBusy,
|
|
} = deps;
|
|
|
|
if (!isNavidromeServer) {
|
|
showToast(t('smartPlaylists.navidromeOnly'), 3500, 'error');
|
|
return;
|
|
}
|
|
setCreatingSmartBusy(true);
|
|
try {
|
|
let baseName = smartFilters.name.trim() || `mix-${new Date().toISOString().slice(0, 10)}`;
|
|
if (!editingSmartId) {
|
|
const existingNames = new Set(playlists.map((p) => (p.name ?? '').toLowerCase()));
|
|
const requestedBaseName = baseName;
|
|
let ordinal = 2;
|
|
while (existingNames.has(`${SMART_PREFIX}${baseName}`.toLowerCase())) {
|
|
baseName = `${requestedBaseName}-${ordinal}`;
|
|
ordinal += 1;
|
|
}
|
|
}
|
|
const rules = buildSmartRulesPayload(smartFilters);
|
|
const fullName = `${SMART_PREFIX}${baseName}`;
|
|
if (editingSmartId) {
|
|
await ndUpdateSmartPlaylist(editingSmartId, fullName, rules, true);
|
|
} else {
|
|
await ndCreateSmartPlaylist(fullName, rules, true);
|
|
}
|
|
await fetchPlaylists();
|
|
const createdName = fullName;
|
|
const updatedId = editingSmartId;
|
|
setPendingSmart(prev => {
|
|
const existing = prev.find(p => p.id === updatedId || p.name === createdName);
|
|
if (existing) return prev;
|
|
const created = usePlaylistStore.getState().playlists.find((p) => p.id === updatedId || p.name === createdName);
|
|
return [
|
|
...prev,
|
|
{
|
|
name: createdName,
|
|
id: updatedId ?? created?.id,
|
|
firstSeenCoverArt: created?.coverArt,
|
|
attempts: 0,
|
|
},
|
|
];
|
|
});
|
|
setCreatingSmart(false);
|
|
setEditingSmartId(null);
|
|
setSmartFilters(defaultSmartFilters);
|
|
setGenreQuery('');
|
|
if (updatedId) showToast(t('smartPlaylists.updated', { name: createdName }), 3500, 'success');
|
|
else showToast(t('smartPlaylists.created', { name: createdName }), 3500, 'success');
|
|
} catch {
|
|
showToast(editingSmartId ? t('smartPlaylists.updateFailed') : t('smartPlaylists.createFailed'), 3500, 'error');
|
|
} finally {
|
|
setCreatingSmartBusy(false);
|
|
}
|
|
}
|