mirror of
https://github.com/kilyabin/psysonic.git
synced 2026-07-22 14:35:41 +00:00
0d18d5dfa9
Aligns orbit invites with the existing magic-string family (psysonic1- for server invites, psysonic2- for library shares) by folding orbit into the psysonic2- payload as a new k:'orbit' variant. The JSON body is intentionally extendable so future layers (passwords, permissions, invite expiry) can be added without a format migration. - SharePayloadV1 split into EntitySharePayloadV1 + OrbitSharePayloadV1 - decodeSharePayloadFromText filters orbit out; orbit has its own decoder - applySharePastePayload param narrowed to EntitySharePayloadV1 - buildOrbitShareLink / parseOrbitShareLink kept as thin wrappers - slug parameter dropped (magic string is opaque, so the slug was cosmetic-only in the old URL form); slugifyOrbitName removed as dead code - joinModalLinkPlaceholder updated in all 8 locales Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
114 lines
3.6 KiB
TypeScript
114 lines
3.6 KiB
TypeScript
import type { NavigateFunction } from 'react-router-dom';
|
|
import type { TFunction } from 'i18next';
|
|
import { getAlbum, getArtist, getSong, type SubsonicSong } from '../api/subsonic';
|
|
import { useAuthStore } from '../store/authStore';
|
|
import { songToTrack, usePlayerStore } from '../store/playerStore';
|
|
import { findServerIdForShareUrl, type EntitySharePayloadV1 } from './shareLink';
|
|
import { showToast } from './toast';
|
|
|
|
const RESOLVE_QUEUE_CHUNK = 12;
|
|
|
|
/**
|
|
* Switches to the matching server, validates the entity on the server, then
|
|
* plays or navigates. Caller should `preventDefault` on the paste event when
|
|
* the payload was already decoded successfully.
|
|
*/
|
|
export async function applySharePastePayload(
|
|
payload: EntitySharePayloadV1,
|
|
navigate: NavigateFunction,
|
|
t: TFunction,
|
|
): Promise<void> {
|
|
const { servers, isLoggedIn, setActiveServer } = useAuthStore.getState();
|
|
if (!isLoggedIn) {
|
|
showToast(t('sharePaste.notLoggedIn'), 4000, 'info');
|
|
return;
|
|
}
|
|
|
|
const serverId = findServerIdForShareUrl(servers, payload.srv);
|
|
if (!serverId) {
|
|
showToast(t('sharePaste.noMatchingServer', { url: payload.srv }), 6000, 'error');
|
|
return;
|
|
}
|
|
|
|
if (useAuthStore.getState().activeServerId !== serverId) {
|
|
setActiveServer(serverId);
|
|
}
|
|
|
|
try {
|
|
if (payload.k === 'track') {
|
|
const song = await getSong(payload.id);
|
|
if (!song) {
|
|
showToast(t('sharePaste.trackUnavailable'), 5000, 'error');
|
|
return;
|
|
}
|
|
const track = songToTrack(song);
|
|
usePlayerStore.getState().clearQueue();
|
|
usePlayerStore.getState().playTrack(track, [track]);
|
|
showToast(t('sharePaste.openedTrack'), 3000, 'info');
|
|
return;
|
|
}
|
|
|
|
if (payload.k === 'album') {
|
|
try {
|
|
await getAlbum(payload.id);
|
|
} catch {
|
|
showToast(t('sharePaste.albumUnavailable'), 5000, 'error');
|
|
return;
|
|
}
|
|
navigate(`/album/${payload.id}`);
|
|
showToast(t('sharePaste.openedAlbum'), 3000, 'info');
|
|
return;
|
|
}
|
|
|
|
if (payload.k === 'artist') {
|
|
try {
|
|
await getArtist(payload.id);
|
|
} catch {
|
|
showToast(t('sharePaste.artistUnavailable'), 5000, 'error');
|
|
return;
|
|
}
|
|
navigate(`/artist/${payload.id}`);
|
|
showToast(t('sharePaste.openedArtist'), 3000, 'info');
|
|
return;
|
|
}
|
|
|
|
if (payload.k === 'queue') {
|
|
const { ids } = payload;
|
|
if (ids.length === 0) {
|
|
showToast(t('sharePaste.genericError'), 5000, 'error');
|
|
return;
|
|
}
|
|
const resolved: SubsonicSong[] = [];
|
|
for (let i = 0; i < ids.length; i += RESOLVE_QUEUE_CHUNK) {
|
|
const chunk = ids.slice(i, i + RESOLVE_QUEUE_CHUNK);
|
|
const songs = await Promise.all(chunk.map(id => getSong(id)));
|
|
for (let j = 0; j < songs.length; j++) {
|
|
const s = songs[j];
|
|
if (s) resolved.push(s);
|
|
}
|
|
}
|
|
const skipped = ids.length - resolved.length;
|
|
if (resolved.length === 0) {
|
|
showToast(t('sharePaste.queueAllUnavailable'), 6000, 'error');
|
|
return;
|
|
}
|
|
const tracks = resolved.map(songToTrack);
|
|
usePlayerStore.getState().clearQueue();
|
|
usePlayerStore.getState().playTrack(tracks[0]!, tracks);
|
|
if (skipped > 0) {
|
|
showToast(
|
|
t('sharePaste.openedQueuePartial', { played: tracks.length, total: ids.length, skipped }),
|
|
5000,
|
|
'info',
|
|
);
|
|
} else {
|
|
showToast(t('sharePaste.openedQueue', { count: tracks.length }), 3000, 'info');
|
|
}
|
|
return;
|
|
}
|
|
} catch (e) {
|
|
console.error('[psysonic] share paste failed', e);
|
|
showToast(t('sharePaste.genericError'), 5000, 'error');
|
|
}
|
|
}
|