mirror of
https://github.com/kilyabin/psysonic.git
synced 2026-07-21 22:15:40 +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:
@@ -9,7 +9,6 @@ import {
|
|||||||
startOrbitSession,
|
startOrbitSession,
|
||||||
buildOrbitShareLink,
|
buildOrbitShareLink,
|
||||||
generateSessionId,
|
generateSessionId,
|
||||||
slugifyOrbitName,
|
|
||||||
} from '../utils/orbit';
|
} from '../utils/orbit';
|
||||||
import { randomOrbitSessionName } from '../utils/orbitNames';
|
import { randomOrbitSessionName } from '../utils/orbitNames';
|
||||||
import { useAuthStore } from '../store/authStore';
|
import { useAuthStore } from '../store/authStore';
|
||||||
@@ -44,8 +43,8 @@ export default function OrbitStartModal({ onClose }: Props) {
|
|||||||
const onLan = isLanUrl(serverBase);
|
const onLan = isLanUrl(serverBase);
|
||||||
|
|
||||||
const shareLink = useMemo(
|
const shareLink = useMemo(
|
||||||
() => buildOrbitShareLink(serverBase, sid, slugifyOrbitName(name)),
|
() => buildOrbitShareLink(serverBase, sid),
|
||||||
[serverBase, sid, name],
|
[serverBase, sid],
|
||||||
);
|
);
|
||||||
|
|
||||||
const writeLinkToClipboard = async (): Promise<boolean> => {
|
const writeLinkToClipboard = async (): Promise<boolean> => {
|
||||||
|
|||||||
+1
-1
@@ -1501,7 +1501,7 @@ export const deTranslation = {
|
|||||||
joinModalTitle: 'Einer Orbit-Session beitreten',
|
joinModalTitle: 'Einer Orbit-Session beitreten',
|
||||||
joinModalSub: 'Füge den Einladungslink ein, den dir der Host geschickt hat.',
|
joinModalSub: 'Füge den Einladungslink ein, den dir der Host geschickt hat.',
|
||||||
joinModalLinkLabel: 'Einladungslink',
|
joinModalLinkLabel: 'Einladungslink',
|
||||||
joinModalLinkPlaceholder: 'psysonic2://orbit/…',
|
joinModalLinkPlaceholder: 'psysonic2-…',
|
||||||
joinModalLinkHelper: 'Akzeptiert jeden gültigen Orbit-Link. Der Link muss zu dem Server gehören, auf dem du gerade angemeldet bist.',
|
joinModalLinkHelper: 'Akzeptiert jeden gültigen Orbit-Link. Der Link muss zu dem Server gehören, auf dem du gerade angemeldet bist.',
|
||||||
joinModalPasteTooltip: 'Aus Zwischenablage einfügen',
|
joinModalPasteTooltip: 'Aus Zwischenablage einfügen',
|
||||||
joinModalSubmit: 'Beitreten',
|
joinModalSubmit: 'Beitreten',
|
||||||
|
|||||||
+1
-1
@@ -1505,7 +1505,7 @@ export const enTranslation = {
|
|||||||
joinModalTitle: 'Join an Orbit session',
|
joinModalTitle: 'Join an Orbit session',
|
||||||
joinModalSub: 'Paste the invite link your host sent you.',
|
joinModalSub: 'Paste the invite link your host sent you.',
|
||||||
joinModalLinkLabel: 'Invite link',
|
joinModalLinkLabel: 'Invite link',
|
||||||
joinModalLinkPlaceholder: 'psysonic2://orbit/…',
|
joinModalLinkPlaceholder: 'psysonic2-…',
|
||||||
joinModalLinkHelper: 'Works with any valid Orbit link. Must point to the server you\'re currently signed into.',
|
joinModalLinkHelper: 'Works with any valid Orbit link. Must point to the server you\'re currently signed into.',
|
||||||
joinModalPasteTooltip: 'Paste from clipboard',
|
joinModalPasteTooltip: 'Paste from clipboard',
|
||||||
joinModalSubmit: 'Join',
|
joinModalSubmit: 'Join',
|
||||||
|
|||||||
+1
-1
@@ -1488,7 +1488,7 @@ export const esTranslation = {
|
|||||||
joinModalTitle: 'Unirse a una sesión Orbit',
|
joinModalTitle: 'Unirse a una sesión Orbit',
|
||||||
joinModalSub: 'Pega el enlace de invitación que te envió tu anfitrión.',
|
joinModalSub: 'Pega el enlace de invitación que te envió tu anfitrión.',
|
||||||
joinModalLinkLabel: 'Enlace de invitación',
|
joinModalLinkLabel: 'Enlace de invitación',
|
||||||
joinModalLinkPlaceholder: 'psysonic2://orbit/…',
|
joinModalLinkPlaceholder: 'psysonic2-…',
|
||||||
joinModalLinkHelper: 'Funciona con cualquier enlace Orbit válido. Debe apuntar al servidor al que estás conectado actualmente.',
|
joinModalLinkHelper: 'Funciona con cualquier enlace Orbit válido. Debe apuntar al servidor al que estás conectado actualmente.',
|
||||||
joinModalPasteTooltip: 'Pegar desde el portapapeles',
|
joinModalPasteTooltip: 'Pegar desde el portapapeles',
|
||||||
joinModalSubmit: 'Unirse',
|
joinModalSubmit: 'Unirse',
|
||||||
|
|||||||
+1
-1
@@ -1483,7 +1483,7 @@ export const frTranslation = {
|
|||||||
joinModalTitle: 'Rejoindre une session Orbit',
|
joinModalTitle: 'Rejoindre une session Orbit',
|
||||||
joinModalSub: 'Colle le lien d\'invitation que ton hôte t\'a envoyé.',
|
joinModalSub: 'Colle le lien d\'invitation que ton hôte t\'a envoyé.',
|
||||||
joinModalLinkLabel: 'Lien d\'invitation',
|
joinModalLinkLabel: 'Lien d\'invitation',
|
||||||
joinModalLinkPlaceholder: 'psysonic2://orbit/…',
|
joinModalLinkPlaceholder: 'psysonic2-…',
|
||||||
joinModalLinkHelper: 'Fonctionne avec tout lien Orbit valide. Doit pointer vers le serveur auquel tu es actuellement connecté.',
|
joinModalLinkHelper: 'Fonctionne avec tout lien Orbit valide. Doit pointer vers le serveur auquel tu es actuellement connecté.',
|
||||||
joinModalPasteTooltip: 'Coller depuis le presse-papiers',
|
joinModalPasteTooltip: 'Coller depuis le presse-papiers',
|
||||||
joinModalSubmit: 'Rejoindre',
|
joinModalSubmit: 'Rejoindre',
|
||||||
|
|||||||
+1
-1
@@ -1482,7 +1482,7 @@ export const nbTranslation = {
|
|||||||
joinModalTitle: 'Bli med i en Orbit-økt',
|
joinModalTitle: 'Bli med i en Orbit-økt',
|
||||||
joinModalSub: 'Lim inn invitasjonslenken verten sendte deg.',
|
joinModalSub: 'Lim inn invitasjonslenken verten sendte deg.',
|
||||||
joinModalLinkLabel: 'Invitasjonslenke',
|
joinModalLinkLabel: 'Invitasjonslenke',
|
||||||
joinModalLinkPlaceholder: 'psysonic2://orbit/…',
|
joinModalLinkPlaceholder: 'psysonic2-…',
|
||||||
joinModalLinkHelper: 'Fungerer med enhver gyldig Orbit-lenke. Må peke til serveren du er innlogget på.',
|
joinModalLinkHelper: 'Fungerer med enhver gyldig Orbit-lenke. Må peke til serveren du er innlogget på.',
|
||||||
joinModalPasteTooltip: 'Lim inn fra utklippstavlen',
|
joinModalPasteTooltip: 'Lim inn fra utklippstavlen',
|
||||||
joinModalSubmit: 'Bli med',
|
joinModalSubmit: 'Bli med',
|
||||||
|
|||||||
+1
-1
@@ -1482,7 +1482,7 @@ export const nlTranslation = {
|
|||||||
joinModalTitle: 'Deelnemen aan een Orbit-sessie',
|
joinModalTitle: 'Deelnemen aan een Orbit-sessie',
|
||||||
joinModalSub: 'Plak de uitnodigingslink die je host je gestuurd heeft.',
|
joinModalSub: 'Plak de uitnodigingslink die je host je gestuurd heeft.',
|
||||||
joinModalLinkLabel: 'Uitnodigingslink',
|
joinModalLinkLabel: 'Uitnodigingslink',
|
||||||
joinModalLinkPlaceholder: 'psysonic2://orbit/…',
|
joinModalLinkPlaceholder: 'psysonic2-…',
|
||||||
joinModalLinkHelper: 'Werkt met elke geldige Orbit-link. Moet verwijzen naar de server waarop je nu bent aangemeld.',
|
joinModalLinkHelper: 'Werkt met elke geldige Orbit-link. Moet verwijzen naar de server waarop je nu bent aangemeld.',
|
||||||
joinModalPasteTooltip: 'Plakken vanaf klembord',
|
joinModalPasteTooltip: 'Plakken vanaf klembord',
|
||||||
joinModalSubmit: 'Deelnemen',
|
joinModalSubmit: 'Deelnemen',
|
||||||
|
|||||||
+1
-1
@@ -1562,7 +1562,7 @@ export const ruTranslation = {
|
|||||||
joinModalTitle: 'Присоединиться к сессии Orbit',
|
joinModalTitle: 'Присоединиться к сессии Orbit',
|
||||||
joinModalSub: 'Вставь ссылку-приглашение от хоста.',
|
joinModalSub: 'Вставь ссылку-приглашение от хоста.',
|
||||||
joinModalLinkLabel: 'Ссылка-приглашение',
|
joinModalLinkLabel: 'Ссылка-приглашение',
|
||||||
joinModalLinkPlaceholder: 'psysonic2://orbit/…',
|
joinModalLinkPlaceholder: 'psysonic2-…',
|
||||||
joinModalLinkHelper: 'Работает с любой действительной ссылкой Orbit. Должна указывать на сервер, на котором ты сейчас авторизован.',
|
joinModalLinkHelper: 'Работает с любой действительной ссылкой Orbit. Должна указывать на сервер, на котором ты сейчас авторизован.',
|
||||||
joinModalPasteTooltip: 'Вставить из буфера обмена',
|
joinModalPasteTooltip: 'Вставить из буфера обмена',
|
||||||
joinModalSubmit: 'Присоединиться',
|
joinModalSubmit: 'Присоединиться',
|
||||||
|
|||||||
+1
-1
@@ -1475,7 +1475,7 @@ export const zhTranslation = {
|
|||||||
joinModalTitle: '加入 Orbit 会话',
|
joinModalTitle: '加入 Orbit 会话',
|
||||||
joinModalSub: '粘贴主持人发给你的邀请链接。',
|
joinModalSub: '粘贴主持人发给你的邀请链接。',
|
||||||
joinModalLinkLabel: '邀请链接',
|
joinModalLinkLabel: '邀请链接',
|
||||||
joinModalLinkPlaceholder: 'psysonic2://orbit/…',
|
joinModalLinkPlaceholder: 'psysonic2-…',
|
||||||
joinModalLinkHelper: '适用于任何有效的 Orbit 链接。必须指向你当前登录的服务器。',
|
joinModalLinkHelper: '适用于任何有效的 Orbit 链接。必须指向你当前登录的服务器。',
|
||||||
joinModalPasteTooltip: '从剪贴板粘贴',
|
joinModalPasteTooltip: '从剪贴板粘贴',
|
||||||
joinModalSubmit: '加入',
|
joinModalSubmit: '加入',
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import type { TFunction } from 'i18next';
|
|||||||
import { getAlbum, getArtist, getSong, type SubsonicSong } from '../api/subsonic';
|
import { getAlbum, getArtist, getSong, type SubsonicSong } from '../api/subsonic';
|
||||||
import { useAuthStore } from '../store/authStore';
|
import { useAuthStore } from '../store/authStore';
|
||||||
import { songToTrack, usePlayerStore } from '../store/playerStore';
|
import { songToTrack, usePlayerStore } from '../store/playerStore';
|
||||||
import { findServerIdForShareUrl, type SharePayloadV1 } from './shareLink';
|
import { findServerIdForShareUrl, type EntitySharePayloadV1 } from './shareLink';
|
||||||
import { showToast } from './toast';
|
import { showToast } from './toast';
|
||||||
|
|
||||||
const RESOLVE_QUEUE_CHUNK = 12;
|
const RESOLVE_QUEUE_CHUNK = 12;
|
||||||
@@ -14,7 +14,7 @@ const RESOLVE_QUEUE_CHUNK = 12;
|
|||||||
* the payload was already decoded successfully.
|
* the payload was already decoded successfully.
|
||||||
*/
|
*/
|
||||||
export async function applySharePastePayload(
|
export async function applySharePastePayload(
|
||||||
payload: SharePayloadV1,
|
payload: EntitySharePayloadV1,
|
||||||
navigate: NavigateFunction,
|
navigate: NavigateFunction,
|
||||||
t: TFunction,
|
t: TFunction,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
|||||||
+13
-50
@@ -10,6 +10,7 @@ import {
|
|||||||
import { useAuthStore } from '../store/authStore';
|
import { useAuthStore } from '../store/authStore';
|
||||||
import { useOrbitStore } from '../store/orbitStore';
|
import { useOrbitStore } from '../store/orbitStore';
|
||||||
import { usePlayerStore, songToTrack } from '../store/playerStore';
|
import { usePlayerStore, songToTrack } from '../store/playerStore';
|
||||||
|
import { encodeSharePayload, decodeOrbitSharePayloadFromText } from './shareLink';
|
||||||
import {
|
import {
|
||||||
makeInitialOrbitState,
|
makeInitialOrbitState,
|
||||||
orbitOutboxPlaylistName,
|
orbitOutboxPlaylistName,
|
||||||
@@ -44,21 +45,6 @@ export function generateSessionId(): string {
|
|||||||
return Array.from(bytes, b => b.toString(16).padStart(2, '0')).join('');
|
return Array.from(bytes, b => b.toString(16).padStart(2, '0')).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Turn a human session name into a URL-safe slug. Ignores non-ASCII
|
|
||||||
* characters so the output is stable across locales and safe in a
|
|
||||||
* `psysonic2://` link. Returns an empty string for names that slugify
|
|
||||||
* to nothing — callers should fall back to a slug-less link in that case.
|
|
||||||
*/
|
|
||||||
export function slugifyOrbitName(name: string): string {
|
|
||||||
return name
|
|
||||||
.trim()
|
|
||||||
.toLowerCase()
|
|
||||||
.replace(/[^a-z0-9]+/g, '-')
|
|
||||||
.replace(/^-+|-+$/g, '')
|
|
||||||
.slice(0, 40);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Serialisation ───────────────────────────────────────────────────────
|
// ── Serialisation ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -307,8 +293,6 @@ export async function updateOrbitSettings(patch: Partial<import('../api/orbit').
|
|||||||
|
|
||||||
// ── Share link ──────────────────────────────────────────────────────────
|
// ── Share link ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export const ORBIT_SHARE_SCHEME = 'psysonic2://orbit/';
|
|
||||||
|
|
||||||
export interface OrbitShareLink {
|
export interface OrbitShareLink {
|
||||||
/** Base URL of the Navidrome server (decoded). */
|
/** Base URL of the Navidrome server (decoded). */
|
||||||
serverBase: string;
|
serverBase: string;
|
||||||
@@ -317,42 +301,21 @@ export interface OrbitShareLink {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse a `psysonic2://orbit/<server-b64>/<sid>` link. Returns null on any
|
* Parse an orbit invite from pasted text. Accepts the magic-string format
|
||||||
* shape mismatch — the caller decides what to do (show error toast etc.).
|
* `psysonic2-<base64url-json>` (same prefix family as library shares and
|
||||||
* Accepts both the `psysonic2://` prefix and a bare string if the OS-level
|
* server invites). The caller decides what to do on null (show toast, etc.).
|
||||||
* handler has already stripped the scheme.
|
|
||||||
*/
|
*/
|
||||||
export function parseOrbitShareLink(url: string): OrbitShareLink | null {
|
export function parseOrbitShareLink(text: string): OrbitShareLink | null {
|
||||||
if (!url) return null;
|
if (!text) return null;
|
||||||
const stripped = url.startsWith(ORBIT_SHARE_SCHEME)
|
const payload = decodeOrbitSharePayloadFromText(text);
|
||||||
? url.slice(ORBIT_SHARE_SCHEME.length)
|
if (!payload) return null;
|
||||||
: url.startsWith('orbit/') ? url.slice('orbit/'.length) : null;
|
try { new URL(payload.srv); } catch { return null; }
|
||||||
if (stripped == null) return null;
|
return { serverBase: payload.srv, sid: payload.sid };
|
||||||
const slash = stripped.indexOf('/');
|
|
||||||
if (slash <= 0) return null;
|
|
||||||
const serverB64 = stripped.slice(0, slash);
|
|
||||||
const tail = stripped.slice(slash + 1).replace(/\/+$/, '');
|
|
||||||
// Tail is either `<sid>` or `<slug>-<sid>` — the SID is always the
|
|
||||||
// terminal 8-hex group. The slug is purely cosmetic for the sender.
|
|
||||||
const m = tail.match(/(?:^|-)([0-9a-f]{8})$/i);
|
|
||||||
if (!m) return null;
|
|
||||||
const sid = m[1].toLowerCase();
|
|
||||||
let serverBase: string;
|
|
||||||
try {
|
|
||||||
serverBase = atob(serverB64);
|
|
||||||
} catch { return null; }
|
|
||||||
try { new URL(serverBase); } catch { return null; }
|
|
||||||
return { serverBase, sid };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Build an orbit invite magic string for a live session. */
|
||||||
* Build a share link for a live session. When `slug` is provided (and
|
export function buildOrbitShareLink(serverBase: string, sid: string): string {
|
||||||
* non-empty) it is prepended to the SID for a friendlier-looking URL
|
return encodeSharePayload({ srv: serverBase, k: 'orbit', sid });
|
||||||
* — the parser strips it on the receiving side.
|
|
||||||
*/
|
|
||||||
export function buildOrbitShareLink(serverBase: string, sid: string, slug?: string): string {
|
|
||||||
const tail = slug && slug.length > 0 ? `${slug}-${sid}` : sid;
|
|
||||||
return `${ORBIT_SHARE_SCHEME}${btoa(serverBase)}/${tail}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Playlist lookup ─────────────────────────────────────────────────────
|
// ── Playlist lookup ─────────────────────────────────────────────────────
|
||||||
|
|||||||
+59
-16
@@ -5,10 +5,17 @@ export const PSYSONIC_SHARE_PREFIX = 'psysonic2-';
|
|||||||
|
|
||||||
export type EntityShareKind = 'track' | 'album' | 'artist';
|
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: EntityShareKind; id: string }
|
||||||
| { srv: string; k: 'queue'; ids: 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 {
|
export function normalizeShareServerUrl(url: string): string {
|
||||||
const t = url.trim();
|
const t = url.trim();
|
||||||
if (!t) return '';
|
if (!t) return '';
|
||||||
@@ -38,24 +45,38 @@ function isEntityKind(k: unknown): k is EntityShareKind {
|
|||||||
|
|
||||||
export function encodeSharePayload(payload: SharePayloadV1): string {
|
export function encodeSharePayload(payload: SharePayloadV1): string {
|
||||||
const srvNorm = normalizeShareServerUrl(payload.srv);
|
const srvNorm = normalizeShareServerUrl(payload.srv);
|
||||||
const body =
|
let body: string;
|
||||||
payload.k === 'queue'
|
if (payload.k === 'queue') {
|
||||||
? JSON.stringify({
|
body = JSON.stringify({
|
||||||
v: 1,
|
v: 1,
|
||||||
srv: srvNorm,
|
srv: srvNorm,
|
||||||
k: 'queue',
|
k: 'queue',
|
||||||
ids: payload.ids.map(id => String(id).trim()).filter(Boolean),
|
ids: payload.ids.map(id => String(id).trim()).filter(Boolean),
|
||||||
})
|
});
|
||||||
: JSON.stringify({
|
} else if (payload.k === 'orbit') {
|
||||||
v: 1,
|
body = JSON.stringify({
|
||||||
srv: srvNorm,
|
v: 1,
|
||||||
k: payload.k,
|
srv: srvNorm,
|
||||||
id: String(payload.id).trim(),
|
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);
|
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);
|
const idx = text.indexOf(PSYSONIC_SHARE_PREFIX);
|
||||||
if (idx < 0) return null;
|
if (idx < 0) return null;
|
||||||
const after = text.slice(idx + PSYSONIC_SHARE_PREFIX.length);
|
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) : '';
|
const srv = typeof raw.srv === 'string' ? normalizeShareServerUrl(raw.srv) : '';
|
||||||
if (!srv) return null;
|
if (!srv) return null;
|
||||||
const k = raw.k;
|
const k = raw.k;
|
||||||
|
if (k === 'orbit') return null;
|
||||||
if (k === 'queue') {
|
if (k === 'queue') {
|
||||||
const idsRaw = raw.ids;
|
const idsRaw = raw.ids;
|
||||||
if (!Array.isArray(idsRaw) || idsRaw.length === 0) return null;
|
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);
|
const hit = servers.find(s => normalizeShareServerUrl(s.url) === norm);
|
||||||
return hit?.id ?? null;
|
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