mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-22 07:15:47 +00:00
refactor(orbit): switch share link to psysonic2- magic string format
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>
This commit is contained in:
+59
-16
@@ -5,10 +5,17 @@ export const PSYSONIC_SHARE_PREFIX = 'psysonic2-';
|
||||
|
||||
export type EntityShareKind = 'track' | 'album' | 'artist';
|
||||
|
||||
export type SharePayloadV1 =
|
||||
/** Entity / queue shares — what {@link applySharePastePayload} dispatches on. */
|
||||
export type EntitySharePayloadV1 =
|
||||
| { srv: string; k: EntityShareKind; id: string }
|
||||
| { srv: string; k: 'queue'; ids: string[] };
|
||||
|
||||
/** Orbit invite — session id + originating server. Decoded separately so that
|
||||
* entity-share consumers can't accidentally receive an orbit payload. */
|
||||
export type OrbitSharePayloadV1 = { srv: string; k: 'orbit'; sid: string };
|
||||
|
||||
export type SharePayloadV1 = EntitySharePayloadV1 | OrbitSharePayloadV1;
|
||||
|
||||
export function normalizeShareServerUrl(url: string): string {
|
||||
const t = url.trim();
|
||||
if (!t) return '';
|
||||
@@ -38,24 +45,38 @@ function isEntityKind(k: unknown): k is EntityShareKind {
|
||||
|
||||
export function encodeSharePayload(payload: SharePayloadV1): string {
|
||||
const srvNorm = normalizeShareServerUrl(payload.srv);
|
||||
const body =
|
||||
payload.k === 'queue'
|
||||
? JSON.stringify({
|
||||
v: 1,
|
||||
srv: srvNorm,
|
||||
k: 'queue',
|
||||
ids: payload.ids.map(id => String(id).trim()).filter(Boolean),
|
||||
})
|
||||
: JSON.stringify({
|
||||
v: 1,
|
||||
srv: srvNorm,
|
||||
k: payload.k,
|
||||
id: String(payload.id).trim(),
|
||||
});
|
||||
let body: string;
|
||||
if (payload.k === 'queue') {
|
||||
body = JSON.stringify({
|
||||
v: 1,
|
||||
srv: srvNorm,
|
||||
k: 'queue',
|
||||
ids: payload.ids.map(id => String(id).trim()).filter(Boolean),
|
||||
});
|
||||
} else if (payload.k === 'orbit') {
|
||||
body = JSON.stringify({
|
||||
v: 1,
|
||||
srv: srvNorm,
|
||||
k: 'orbit',
|
||||
sid: String(payload.sid).trim(),
|
||||
});
|
||||
} else {
|
||||
body = JSON.stringify({
|
||||
v: 1,
|
||||
srv: srvNorm,
|
||||
k: payload.k,
|
||||
id: String(payload.id).trim(),
|
||||
});
|
||||
}
|
||||
return PSYSONIC_SHARE_PREFIX + utf8ToBase64Url(body);
|
||||
}
|
||||
|
||||
export function decodeSharePayloadFromText(text: string): SharePayloadV1 | null {
|
||||
/**
|
||||
* Decode an entity / queue share from pasted text. Returns null for orbit
|
||||
* payloads (use {@link decodeOrbitSharePayloadFromText}) — so entity-share
|
||||
* consumers can't be fed an orbit invite by accident.
|
||||
*/
|
||||
export function decodeSharePayloadFromText(text: string): EntitySharePayloadV1 | null {
|
||||
const idx = text.indexOf(PSYSONIC_SHARE_PREFIX);
|
||||
if (idx < 0) return null;
|
||||
const after = text.slice(idx + PSYSONIC_SHARE_PREFIX.length);
|
||||
@@ -67,6 +88,7 @@ export function decodeSharePayloadFromText(text: string): SharePayloadV1 | null
|
||||
const srv = typeof raw.srv === 'string' ? normalizeShareServerUrl(raw.srv) : '';
|
||||
if (!srv) return null;
|
||||
const k = raw.k;
|
||||
if (k === 'orbit') return null;
|
||||
if (k === 'queue') {
|
||||
const idsRaw = raw.ids;
|
||||
if (!Array.isArray(idsRaw) || idsRaw.length === 0) return null;
|
||||
@@ -87,3 +109,24 @@ export function findServerIdForShareUrl(servers: ServerProfile[], shareSrv: stri
|
||||
const hit = servers.find(s => normalizeShareServerUrl(s.url) === norm);
|
||||
return hit?.id ?? null;
|
||||
}
|
||||
|
||||
/** Decode an orbit invite from pasted text. Returns null for entity / queue shares. */
|
||||
export function decodeOrbitSharePayloadFromText(text: string): OrbitSharePayloadV1 | null {
|
||||
const idx = text.indexOf(PSYSONIC_SHARE_PREFIX);
|
||||
if (idx < 0) return null;
|
||||
const after = text.slice(idx + PSYSONIC_SHARE_PREFIX.length);
|
||||
const token = after.match(/^([A-Za-z0-9_-]+)/)?.[1];
|
||||
if (!token) return null;
|
||||
try {
|
||||
const raw = JSON.parse(base64UrlToUtf8(token)) as Record<string, unknown>;
|
||||
if (raw.v !== 1) return null;
|
||||
if (raw.k !== 'orbit') return null;
|
||||
const srv = typeof raw.srv === 'string' ? normalizeShareServerUrl(raw.srv) : '';
|
||||
if (!srv) return null;
|
||||
const sid = typeof raw.sid === 'string' ? raw.sid.trim().toLowerCase() : '';
|
||||
if (!/^[0-9a-f]{8}$/.test(sid)) return null;
|
||||
return { srv, k: 'orbit', sid };
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user