mirror of
https://github.com/kilyabin/psysonic.git
synced 2026-07-22 14:35:41 +00:00
7a7a9f5e6b
111 of 122 top-level src/utils/ files move into 16 topic folders (audio, cache, cover, share, server, playback, playlist, deviceSync, waveform, mix, format, export, changelog, ui, perf, componentHelpers). True singletons with no cluster stay at the utils/ root. Pure file-move: a path-aware codemod rewrote 539 relative-import specifiers across 275 files; no logic touched. The hot-path coverage gate list (.github/frontend-hot-path-files.txt) is updated to the new paths for the 11 gated utils files — a mechanical consequence of the move, not a CI change. tsc is green.
189 lines
7.8 KiB
TypeScript
189 lines
7.8 KiB
TypeScript
import { useEffect } from 'react';
|
|
import { listen } from '@tauri-apps/api/event';
|
|
import { invoke } from '@tauri-apps/api/core';
|
|
import type { NavigateFunction } from 'react-router-dom';
|
|
import { getSimilarSongs } from '../../api/subsonicArtists';
|
|
import { getMusicFolders } from '../../api/subsonicLibrary';
|
|
import { search as subsonicSearch } from '../../api/subsonicSearch';
|
|
import { shuffleArray } from '../../utils/playback/shuffleArray';
|
|
import { songToTrack } from '../../utils/playback/songToTrack';
|
|
import { showToast } from '../../utils/ui/toast';
|
|
import { switchActiveServer } from '../../utils/server/switchActiveServer';
|
|
import i18n from '../../i18n';
|
|
import { usePlayerStore } from '../../store/playerStore';
|
|
import { useAuthStore } from '../../store/authStore';
|
|
import { executeCliPlayerCommand } from '../../config/shortcutActions';
|
|
|
|
/** The full `cli:*` listener surface forwarded from the Rust single-instance
|
|
* handler: audio-device, instant-mix, library / server resolution, search and
|
|
* player commands. */
|
|
export function useCliBridge(navigate: NavigateFunction) {
|
|
// CLI: `--player audio-device set …` (forwarded on Linux via single-instance).
|
|
useEffect(() => {
|
|
let unlisten: (() => void) | undefined;
|
|
listen<string>('cli:audio-device-set', async e => {
|
|
const raw = typeof e.payload === 'string' ? e.payload : '';
|
|
const deviceName = raw.length > 0 ? raw : null;
|
|
try {
|
|
await invoke('audio_set_device', { deviceName });
|
|
useAuthStore.getState().setAudioOutputDevice(deviceName);
|
|
} catch {
|
|
/* device open failed — do not persist (same as Settings) */
|
|
}
|
|
}).then(u => { unlisten = u; });
|
|
return () => { unlisten?.(); };
|
|
}, []);
|
|
|
|
// CLI: `--player mix append|new` from the currently playing track.
|
|
useEffect(() => {
|
|
let unlisten: (() => void) | undefined;
|
|
listen<string>('cli:instant-mix', async e => {
|
|
const mode = e.payload === 'append' ? 'append' : 'new';
|
|
const state = usePlayerStore.getState();
|
|
const song = state.currentTrack;
|
|
if (!song) {
|
|
showToast(i18n.t('contextMenu.cliMixNeedsTrack'), 5000, 'error');
|
|
return;
|
|
}
|
|
const serverId = useAuthStore.getState().activeServerId;
|
|
try {
|
|
const similar = await getSimilarSongs(song.id, 50);
|
|
if (serverId) useAuthStore.getState().setAudiomuseNavidromeIssue(serverId, false);
|
|
const base = similar.filter(s => s.id !== song.id).map(s => songToTrack(s));
|
|
if (mode === 'append') {
|
|
const toAdd = shuffleArray(base.map(t => ({ ...t, autoAdded: true as const })));
|
|
if (toAdd.length > 0) usePlayerStore.getState().enqueue(toAdd);
|
|
} else {
|
|
// New queue from seed: collapse to [song] first, then radio tail (not append onto old queue).
|
|
usePlayerStore.getState().reseedQueueForInstantMix(song);
|
|
const shuffled = shuffleArray(
|
|
base.map(t => ({ ...t, radioAdded: true as const })),
|
|
);
|
|
if (shuffled.length > 0) {
|
|
const aid = song.artistId?.trim() || undefined;
|
|
usePlayerStore.getState().enqueueRadio(shuffled, aid);
|
|
}
|
|
}
|
|
} catch (err) {
|
|
console.error('CLI instant mix failed', err);
|
|
if (serverId) useAuthStore.getState().setAudiomuseNavidromeIssue(serverId, true);
|
|
showToast(i18n.t('contextMenu.instantMixFailed'), 5000, 'error');
|
|
}
|
|
}).then(u => { unlisten = u; });
|
|
return () => { unlisten?.(); };
|
|
}, []);
|
|
|
|
// CLI: `--player library list` (Rust polls the JSON file) / `library set`.
|
|
useEffect(() => {
|
|
let u1: (() => void) | undefined;
|
|
let u2: (() => void) | undefined;
|
|
listen('cli:library-list', async () => {
|
|
try {
|
|
const folders = await getMusicFolders();
|
|
const auth = useAuthStore.getState();
|
|
const sid = auth.activeServerId;
|
|
const selected = sid ? (auth.musicLibraryFilterByServer[sid] ?? 'all') : 'all';
|
|
await invoke('cli_publish_library_list', {
|
|
payload: {
|
|
folders: folders.map(f => ({ id: f.id, name: f.name })),
|
|
selected,
|
|
active_server_id: sid,
|
|
},
|
|
});
|
|
} catch (e) {
|
|
console.error('CLI library list failed', e);
|
|
await invoke('cli_publish_library_list', {
|
|
payload: { folders: [], selected: 'all', active_server_id: null },
|
|
}).catch(() => {});
|
|
}
|
|
}).then(u => { u1 = u; });
|
|
listen<string>('cli:library-set', e => {
|
|
const raw = typeof e.payload === 'string' ? e.payload : '';
|
|
if (raw === 'all') useAuthStore.getState().setMusicLibraryFilter('all');
|
|
else if (raw.length > 0) useAuthStore.getState().setMusicLibraryFilter(raw);
|
|
}).then(u => { u2 = u; });
|
|
return () => {
|
|
u1?.();
|
|
u2?.();
|
|
};
|
|
}, []);
|
|
|
|
// CLI: servers, search, transport extras, mute, star, rating, play-by-id, reload.
|
|
useEffect(() => {
|
|
const unsubs: Array<() => void> = [];
|
|
listen('cli:server-list', async () => {
|
|
const auth = useAuthStore.getState();
|
|
await invoke('cli_publish_server_list', {
|
|
payload: {
|
|
active_server_id: auth.activeServerId,
|
|
servers: auth.servers.map(s => ({ id: s.id, name: s.name })),
|
|
},
|
|
});
|
|
}).then(u => unsubs.push(u));
|
|
listen<string>('cli:server-set', async e => {
|
|
const raw = typeof e.payload === 'string' ? e.payload : '';
|
|
const id = raw.trim();
|
|
if (!id) return;
|
|
const server = useAuthStore.getState().servers.find(s => s.id === id);
|
|
if (!server) {
|
|
showToast(i18n.t('contextMenu.cliServerNotFound', { defaultValue: 'Server id not found.' }), 4000, 'error');
|
|
return;
|
|
}
|
|
const ok = await switchActiveServer(server);
|
|
if (!ok) {
|
|
showToast(i18n.t('contextMenu.cliServerSwitchFailed', { defaultValue: 'Could not switch server (ping failed).' }), 5000, 'error');
|
|
}
|
|
}).then(u => unsubs.push(u));
|
|
listen<{ scope: string; query: string }>('cli:search', async e => {
|
|
const { scope, query } = e.payload;
|
|
const base = { scope, query, ready: false };
|
|
try {
|
|
const r = await subsonicSearch(query, { songCount: 50, albumCount: 30, artistCount: 30 });
|
|
const payload =
|
|
scope === 'track'
|
|
? {
|
|
...base,
|
|
songs: r.songs.map(s => ({ id: s.id, title: s.title, artist: s.artist })),
|
|
albums: [] as { id: string; name: string; artist: string }[],
|
|
artists: [] as { id: string; name: string }[],
|
|
ready: true,
|
|
}
|
|
: scope === 'album'
|
|
? {
|
|
...base,
|
|
songs: [] as { id: string; title: string; artist: string }[],
|
|
albums: r.albums.map(a => ({ id: a.id, name: a.name, artist: a.artist })),
|
|
artists: [] as { id: string; name: string }[],
|
|
ready: true,
|
|
}
|
|
: {
|
|
...base,
|
|
songs: [] as { id: string; title: string; artist: string }[],
|
|
albums: [] as { id: string; name: string; artist: string }[],
|
|
artists: r.artists.map(a => ({ id: a.id, name: a.name })),
|
|
ready: true,
|
|
};
|
|
await invoke('cli_publish_search_results', { payload });
|
|
} catch (err) {
|
|
console.error('CLI search failed', err);
|
|
await invoke('cli_publish_search_results', {
|
|
payload: {
|
|
...base,
|
|
songs: [],
|
|
albums: [],
|
|
artists: [],
|
|
ready: true,
|
|
error: err instanceof Error ? err.message : 'search failed',
|
|
},
|
|
}).catch(() => {});
|
|
}
|
|
}).then(u => unsubs.push(u));
|
|
listen<any>('cli:player-command', async e => {
|
|
await executeCliPlayerCommand({ payload: e.payload ?? {}, navigate });
|
|
}).then(u => unsubs.push(u));
|
|
return () => {
|
|
unsubs.forEach(u => u());
|
|
};
|
|
}, []);
|
|
}
|