mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-22 23:35:44 +00:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f133492e5c | |||
| a8bed6cc91 |
+26
-5
@@ -16,11 +16,20 @@ import {
|
|||||||
import { neededProbeIds } from '../serverCapabilities/resolve';
|
import { neededProbeIds } from '../serverCapabilities/resolve';
|
||||||
import {
|
import {
|
||||||
SUBSONIC_CLIENT,
|
SUBSONIC_CLIENT,
|
||||||
|
SUBSONIC_API_VERSION,
|
||||||
api,
|
api,
|
||||||
apiWithCredentials,
|
apiWithCredentials,
|
||||||
secureRandomSalt,
|
secureRandomSalt,
|
||||||
} from './subsonicClient';
|
} from './subsonicClient';
|
||||||
import type { PingWithCredentialsResult, SubsonicSong } from './subsonicTypes';
|
import type { PingFailure, PingWithCredentialsResult, SubsonicSong } from './subsonicTypes';
|
||||||
|
|
||||||
|
/** Map a Subsonic error code to a coarse failure category for the UI. */
|
||||||
|
function classifyPingError(code: number | undefined, message: string | undefined): PingFailure {
|
||||||
|
let reason: PingFailure['reason'] = 'server';
|
||||||
|
if (code === 40 || code === 41 || code === 50) reason = 'auth';
|
||||||
|
else if (code === 20 || code === 30) reason = 'version';
|
||||||
|
return { reason, code, message };
|
||||||
|
}
|
||||||
|
|
||||||
export async function ping(): Promise<boolean> {
|
export async function ping(): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
@@ -43,21 +52,33 @@ export async function pingWithCredentials(
|
|||||||
const salt = secureRandomSalt();
|
const salt = secureRandomSalt();
|
||||||
const token = md5(password + salt);
|
const token = md5(password + salt);
|
||||||
const resp = await axios.get(`${base}/rest/ping.view`, {
|
const resp = await axios.get(`${base}/rest/ping.view`, {
|
||||||
params: { u: username, t: token, s: salt, v: '1.16.1', c: SUBSONIC_CLIENT, f: 'json' },
|
params: { u: username, t: token, s: salt, v: SUBSONIC_API_VERSION, c: SUBSONIC_CLIENT, f: 'json' },
|
||||||
paramsSerializer: { indexes: null },
|
paramsSerializer: { indexes: null },
|
||||||
timeout: 15000,
|
timeout: 15000,
|
||||||
});
|
});
|
||||||
const data = resp.data?.['subsonic-response'];
|
const data = resp.data?.['subsonic-response'];
|
||||||
const ok = data?.status === 'ok';
|
const ok = data?.status === 'ok';
|
||||||
return {
|
const identity = {
|
||||||
ok,
|
|
||||||
type: typeof data?.type === 'string' ? data.type : undefined,
|
type: typeof data?.type === 'string' ? data.type : undefined,
|
||||||
serverVersion: typeof data?.serverVersion === 'string' ? data.serverVersion : undefined,
|
serverVersion: typeof data?.serverVersion === 'string' ? data.serverVersion : undefined,
|
||||||
openSubsonic: data?.openSubsonic === true,
|
openSubsonic: data?.openSubsonic === true,
|
||||||
};
|
};
|
||||||
|
if (ok) return { ok: true, ...identity };
|
||||||
|
// Reachable server that rejected the ping — keep the Subsonic reason so the
|
||||||
|
// UI can show a specific message (code 30 = protocol too high, 40 = bad
|
||||||
|
// credentials, …) instead of an opaque failure.
|
||||||
|
const code = typeof data?.error?.code === 'number' ? data.error.code : undefined;
|
||||||
|
const message = typeof data?.error?.message === 'string' ? data.error.message : undefined;
|
||||||
|
console.warn('[psysonic] ping rejected by server:', serverUrl, 'sentVersion=', SUBSONIC_API_VERSION, data?.error ?? data);
|
||||||
|
return { ok: false, failure: classifyPingError(code, message), ...identity };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
// Never reached the server (DNS, refused, timeout, TLS-cert not trusted, or
|
||||||
|
// a blocked cross-origin request). The WebView hides the exact cause, so
|
||||||
|
// pass the raw detail through for the toast + log.
|
||||||
|
const detail =
|
||||||
|
(err as { message?: string })?.message || (err as { code?: string })?.code || 'network error';
|
||||||
console.warn('[psysonic] pingWithCredentials failed:', serverUrl, err);
|
console.warn('[psysonic] pingWithCredentials failed:', serverUrl, err);
|
||||||
return { ok: false };
|
return { ok: false, failure: { reason: 'network', message: String(detail) } };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,15 @@ import { findServerByIdOrIndexKey, resolveServerIdForIndexKey } from '../utils/s
|
|||||||
|
|
||||||
export const SUBSONIC_CLIENT = `psysonic/${version}`;
|
export const SUBSONIC_CLIENT = `psysonic/${version}`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subsonic REST protocol version sent on every request. A server returns
|
||||||
|
* error 30 ("Incompatible REST protocol version") if the client asks for a
|
||||||
|
* HIGHER version than it supports — surfaced to the user via the connection
|
||||||
|
* error toast. OpenSubsonic extensions are negotiated via the `openSubsonic`
|
||||||
|
* response flag, not this number.
|
||||||
|
*/
|
||||||
|
export const SUBSONIC_API_VERSION = '1.16.1';
|
||||||
|
|
||||||
export function secureRandomSalt(): string {
|
export function secureRandomSalt(): string {
|
||||||
const buf = new Uint8Array(8);
|
const buf = new Uint8Array(8);
|
||||||
crypto.getRandomValues(buf);
|
crypto.getRandomValues(buf);
|
||||||
@@ -17,7 +26,7 @@ export function secureRandomSalt(): string {
|
|||||||
export function getAuthParams(username: string, password: string) {
|
export function getAuthParams(username: string, password: string) {
|
||||||
const salt = secureRandomSalt();
|
const salt = secureRandomSalt();
|
||||||
const token = md5(password + salt);
|
const token = md5(password + salt);
|
||||||
return { u: username, t: token, s: salt, v: '1.16.1', c: SUBSONIC_CLIENT, f: 'json' };
|
return { u: username, t: token, s: salt, v: SUBSONIC_API_VERSION, c: SUBSONIC_CLIENT, f: 'json' };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function restBaseFromUrl(serverUrl: string): string {
|
export function restBaseFromUrl(serverUrl: string): string {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import type { CoverArtTier } from '../cover/types';
|
|||||||
import { useAuthStore } from '../store/authStore';
|
import { useAuthStore } from '../store/authStore';
|
||||||
import { connectBaseUrlForServer } from '../utils/server/serverEndpoint';
|
import { connectBaseUrlForServer } from '../utils/server/serverEndpoint';
|
||||||
import { findServerByIdOrIndexKey } from '../utils/server/serverLookup';
|
import { findServerByIdOrIndexKey } from '../utils/server/serverLookup';
|
||||||
import { restBaseFromUrl, SUBSONIC_CLIENT, secureRandomSalt } from './subsonicClient';
|
import { restBaseFromUrl, SUBSONIC_CLIENT, SUBSONIC_API_VERSION, secureRandomSalt } from './subsonicClient';
|
||||||
|
|
||||||
function coverArtQueryParams(username: string, password: string, id: string, size: number): URLSearchParams {
|
function coverArtQueryParams(username: string, password: string, id: string, size: number): URLSearchParams {
|
||||||
const salt = secureRandomSalt();
|
const salt = secureRandomSalt();
|
||||||
@@ -16,7 +16,7 @@ function coverArtQueryParams(username: string, password: string, id: string, siz
|
|||||||
u: username,
|
u: username,
|
||||||
t: token,
|
t: token,
|
||||||
s: salt,
|
s: salt,
|
||||||
v: '1.16.1',
|
v: SUBSONIC_API_VERSION,
|
||||||
c: SUBSONIC_CLIENT,
|
c: SUBSONIC_CLIENT,
|
||||||
f: 'json',
|
f: 'json',
|
||||||
});
|
});
|
||||||
@@ -36,7 +36,7 @@ function streamUrlFromProfile(
|
|||||||
u: username,
|
u: username,
|
||||||
t: token,
|
t: token,
|
||||||
s: salt,
|
s: salt,
|
||||||
v: '1.16.1',
|
v: SUBSONIC_API_VERSION,
|
||||||
c: SUBSONIC_CLIENT,
|
c: SUBSONIC_CLIENT,
|
||||||
f: 'json',
|
f: 'json',
|
||||||
});
|
});
|
||||||
@@ -116,7 +116,7 @@ export function buildDownloadUrl(id: string): string {
|
|||||||
const p = new URLSearchParams({
|
const p = new URLSearchParams({
|
||||||
id,
|
id,
|
||||||
u: server?.username ?? '',
|
u: server?.username ?? '',
|
||||||
t: token, s: salt, v: '1.16.1', c: SUBSONIC_CLIENT, f: 'json',
|
t: token, s: salt, v: SUBSONIC_API_VERSION, c: SUBSONIC_CLIENT, f: 'json',
|
||||||
});
|
});
|
||||||
return `${baseUrl}/rest/download.view?${p.toString()}`;
|
return `${baseUrl}/rest/download.view?${p.toString()}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -201,7 +201,20 @@ export interface SubsonicDirectory {
|
|||||||
child: SubsonicDirectoryEntry[];
|
child: SubsonicDirectoryEntry[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PingWithCredentialsResult = SubsonicServerIdentity & { ok: boolean };
|
export interface PingFailure {
|
||||||
|
/** Coarse category that drives the user-facing message. */
|
||||||
|
reason: 'auth' | 'version' | 'network' | 'server';
|
||||||
|
/** Subsonic error code, when the server returned a structured error. */
|
||||||
|
code?: number;
|
||||||
|
/** Server's error message, or the network/TLS error detail. */
|
||||||
|
message?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PingWithCredentialsResult = SubsonicServerIdentity & {
|
||||||
|
ok: boolean;
|
||||||
|
/** Populated when `ok` is false — why the ping was rejected. */
|
||||||
|
failure?: PingFailure;
|
||||||
|
};
|
||||||
|
|
||||||
export interface RandomSongsFilters {
|
export interface RandomSongsFilters {
|
||||||
size?: number;
|
size?: number;
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { useLibraryIndexSync } from '../../hooks/useLibraryIndexSync';
|
|||||||
import ServerLibraryIndexControls from './ServerLibraryIndexControls';
|
import ServerLibraryIndexControls from './ServerLibraryIndexControls';
|
||||||
import type { ServerProfile } from '../../store/authStoreTypes';
|
import type { ServerProfile } from '../../store/authStoreTypes';
|
||||||
import { pingWithCredentials, scheduleInstantMixProbeForServer } from '../../api/subsonic';
|
import { pingWithCredentials, scheduleInstantMixProbeForServer } from '../../api/subsonic';
|
||||||
|
import { pingFailureMessage } from '../../utils/server/pingFailureMessage';
|
||||||
import { useDragDrop } from '../../contexts/DragDropContext';
|
import { useDragDrop } from '../../contexts/DragDropContext';
|
||||||
import { type ServerMagicPayload } from '../../utils/server/serverMagicString';
|
import { type ServerMagicPayload } from '../../utils/server/serverMagicString';
|
||||||
import { ensureConnectUrlResolved, invalidateReachableEndpointCache } from '../../utils/server/serverEndpoint';
|
import { ensureConnectUrlResolved, invalidateReachableEndpointCache } from '../../utils/server/serverEndpoint';
|
||||||
@@ -226,8 +227,6 @@ export function ServersTab({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleAddServer = async (data: Omit<ServerProfile, 'id'>) => {
|
const handleAddServer = async (data: Omit<ServerProfile, 'id'>) => {
|
||||||
setShowAddForm(false);
|
|
||||||
setPastedServerInvite(null);
|
|
||||||
const tempId = '_new';
|
const tempId = '_new';
|
||||||
setConnStatus(s => ({ ...s, [tempId]: 'testing' }));
|
setConnStatus(s => ({ ...s, [tempId]: 'testing' }));
|
||||||
try {
|
try {
|
||||||
@@ -241,6 +240,7 @@ export function ServersTab({
|
|||||||
data.password,
|
data.password,
|
||||||
);
|
);
|
||||||
if (!announceVerifyResult(verify)) {
|
if (!announceVerifyResult(verify)) {
|
||||||
|
// announceVerifyResult already toasts the reason; keep the form open.
|
||||||
setConnStatus(s => ({ ...s, [tempId]: 'error' }));
|
setConnStatus(s => ({ ...s, [tempId]: 'error' }));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -258,11 +258,22 @@ export function ServersTab({
|
|||||||
setConnStatus(s => ({ ...s, [id]: 'ok' }));
|
setConnStatus(s => ({ ...s, [id]: 'ok' }));
|
||||||
const added = useAuthStore.getState().servers.find(s => s.id === id);
|
const added = useAuthStore.getState().servers.find(s => s.id === id);
|
||||||
if (added) void bootstrapIndexedServer(added);
|
if (added) void bootstrapIndexedServer(added);
|
||||||
|
// Close the form only once the server actually landed.
|
||||||
|
setShowAddForm(false);
|
||||||
|
setPastedServerInvite(null);
|
||||||
} else {
|
} else {
|
||||||
|
// Ping failed (reachable server that rejected us, or a network/TLS
|
||||||
|
// problem). Don't drop the add silently: show a specific reason and
|
||||||
|
// keep the form open so the entered details aren't lost.
|
||||||
setConnStatus(s => ({ ...s, [tempId]: 'error' }));
|
setConnStatus(s => ({ ...s, [tempId]: 'error' }));
|
||||||
|
const { key, vars } = pingFailureMessage(ping.failure);
|
||||||
|
console.warn('[servers] add aborted:', ping.failure?.reason ?? 'unknown', data.url, ping.failure);
|
||||||
|
showToast(t(key, vars), 6000, 'error');
|
||||||
}
|
}
|
||||||
} catch {
|
} catch (e) {
|
||||||
setConnStatus(s => ({ ...s, [tempId]: 'error' }));
|
setConnStatus(s => ({ ...s, [tempId]: 'error' }));
|
||||||
|
console.error('[servers] add failed:', data.url, e);
|
||||||
|
showToast(t('settings.serverFailed'), 4000, 'error');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -52,6 +52,8 @@ export const settings = {
|
|||||||
serverConnecting: 'Verbinde…',
|
serverConnecting: 'Verbinde…',
|
||||||
serverConnected: 'Verbunden!',
|
serverConnected: 'Verbunden!',
|
||||||
serverFailed: 'Verbindung fehlgeschlagen.',
|
serverFailed: 'Verbindung fehlgeschlagen.',
|
||||||
|
serverAddUnreachable: 'Server nicht erreichbar — Adresse, Netzwerk und das TLS-Zertifikat des Servers prüfen. ({{detail}})',
|
||||||
|
serverAddRejected: 'Der Server hat die Verbindung abgelehnt: {{message}}',
|
||||||
testBtn: 'Verbindung testen',
|
testBtn: 'Verbindung testen',
|
||||||
testingBtn: 'Teste…',
|
testingBtn: 'Teste…',
|
||||||
serverCompatible: 'Für Navidrome entwickelt. Andere Subsonic-kompatible Server (Gonic, Airsonic, …) funktionieren ggf. eingeschränkt, weil Psysonic viele Navidrome-spezifische API-Endpunkte nutzt.',
|
serverCompatible: 'Für Navidrome entwickelt. Andere Subsonic-kompatible Server (Gonic, Airsonic, …) funktionieren ggf. eingeschränkt, weil Psysonic viele Navidrome-spezifische API-Endpunkte nutzt.',
|
||||||
|
|||||||
@@ -52,6 +52,8 @@ export const settings = {
|
|||||||
serverConnecting: 'Connecting…',
|
serverConnecting: 'Connecting…',
|
||||||
serverConnected: 'Connected!',
|
serverConnected: 'Connected!',
|
||||||
serverFailed: 'Connection failed.',
|
serverFailed: 'Connection failed.',
|
||||||
|
serverAddUnreachable: "Couldn't reach the server — check the address, your network, and the server's TLS certificate. ({{detail}})",
|
||||||
|
serverAddRejected: 'The server rejected the connection: {{message}}',
|
||||||
testBtn: 'Test Connection',
|
testBtn: 'Test Connection',
|
||||||
testingBtn: 'Testing…',
|
testingBtn: 'Testing…',
|
||||||
serverCompatible: 'Built for Navidrome. Other Subsonic-compatible servers (Gonic, Airsonic, …) may work with reduced functionality, since Psysonic uses many Navidrome-specific API endpoints.',
|
serverCompatible: 'Built for Navidrome. Other Subsonic-compatible servers (Gonic, Airsonic, …) may work with reduced functionality, since Psysonic uses many Navidrome-specific API endpoints.',
|
||||||
|
|||||||
@@ -51,6 +51,8 @@ export const settings = {
|
|||||||
serverConnecting: 'Conectando…',
|
serverConnecting: 'Conectando…',
|
||||||
serverConnected: '¡Conectado!',
|
serverConnected: '¡Conectado!',
|
||||||
serverFailed: 'Conexión fallida.',
|
serverFailed: 'Conexión fallida.',
|
||||||
|
serverAddUnreachable: 'No se pudo conectar con el servidor — comprueba la dirección, tu red y el certificado TLS del servidor. ({{detail}})',
|
||||||
|
serverAddRejected: 'El servidor rechazó la conexión: {{message}}',
|
||||||
testBtn: 'Probar Conexión',
|
testBtn: 'Probar Conexión',
|
||||||
testingBtn: 'Probando…',
|
testingBtn: 'Probando…',
|
||||||
serverCompatible: 'Diseñado para Navidrome. Otros servidores compatibles con Subsonic (Gonic, Airsonic, …) pueden funcionar con funcionalidad reducida, ya que Psysonic utiliza muchos endpoints específicos de la API de Navidrome.',
|
serverCompatible: 'Diseñado para Navidrome. Otros servidores compatibles con Subsonic (Gonic, Airsonic, …) pueden funcionar con funcionalidad reducida, ya que Psysonic utiliza muchos endpoints específicos de la API de Navidrome.',
|
||||||
|
|||||||
@@ -51,6 +51,8 @@ export const settings = {
|
|||||||
serverConnecting: 'Connexion…',
|
serverConnecting: 'Connexion…',
|
||||||
serverConnected: 'Connecté !',
|
serverConnected: 'Connecté !',
|
||||||
serverFailed: 'Connexion échouée.',
|
serverFailed: 'Connexion échouée.',
|
||||||
|
serverAddUnreachable: 'Serveur injoignable — vérifiez l’adresse, votre réseau et le certificat TLS du serveur. ({{detail}})',
|
||||||
|
serverAddRejected: 'Le serveur a refusé la connexion : {{message}}',
|
||||||
testBtn: 'Tester la connexion',
|
testBtn: 'Tester la connexion',
|
||||||
testingBtn: 'Test en cours…',
|
testingBtn: 'Test en cours…',
|
||||||
serverCompatible: 'Conçu pour Navidrome. Les autres serveurs compatibles Subsonic (Gonic, Airsonic, …) peuvent fonctionner avec des fonctionnalités réduites, car Psysonic utilise de nombreux endpoints d’API spécifiques à Navidrome.',
|
serverCompatible: 'Conçu pour Navidrome. Les autres serveurs compatibles Subsonic (Gonic, Airsonic, …) peuvent fonctionner avec des fonctionnalités réduites, car Psysonic utilise de nombreux endpoints d’API spécifiques à Navidrome.',
|
||||||
|
|||||||
@@ -51,6 +51,8 @@ export const settings = {
|
|||||||
serverConnecting: 'Kobler til…',
|
serverConnecting: 'Kobler til…',
|
||||||
serverConnected: 'Tilkoblet!',
|
serverConnected: 'Tilkoblet!',
|
||||||
serverFailed: 'Tilkobling mislyktes.',
|
serverFailed: 'Tilkobling mislyktes.',
|
||||||
|
serverAddUnreachable: 'Kunne ikke nå serveren — sjekk adressen, nettverket ditt og serverens TLS-sertifikat. ({{detail}})',
|
||||||
|
serverAddRejected: 'Serveren avviste tilkoblingen: {{message}}',
|
||||||
testBtn: 'Test tilkobling',
|
testBtn: 'Test tilkobling',
|
||||||
testingBtn: 'Tester…',
|
testingBtn: 'Tester…',
|
||||||
serverCompatible: 'Laget for Navidrome. Andre Subsonic-kompatible servere (Gonic, Airsonic, …) kan fungere med begrenset funksjonalitet, fordi Psysonic bruker mange Navidrome-spesifikke API-endepunkter.',
|
serverCompatible: 'Laget for Navidrome. Andre Subsonic-kompatible servere (Gonic, Airsonic, …) kan fungere med begrenset funksjonalitet, fordi Psysonic bruker mange Navidrome-spesifikke API-endepunkter.',
|
||||||
|
|||||||
@@ -51,6 +51,8 @@ export const settings = {
|
|||||||
serverConnecting: 'Verbinden…',
|
serverConnecting: 'Verbinden…',
|
||||||
serverConnected: 'Verbonden!',
|
serverConnected: 'Verbonden!',
|
||||||
serverFailed: 'Verbinding mislukt.',
|
serverFailed: 'Verbinding mislukt.',
|
||||||
|
serverAddUnreachable: 'Server niet bereikbaar — controleer het adres, je netwerk en het TLS-certificaat van de server. ({{detail}})',
|
||||||
|
serverAddRejected: 'De server weigerde de verbinding: {{message}}',
|
||||||
testBtn: 'Verbinding testen',
|
testBtn: 'Verbinding testen',
|
||||||
testingBtn: 'Testen…',
|
testingBtn: 'Testen…',
|
||||||
serverCompatible: 'Gemaakt voor Navidrome. Andere Subsonic-compatibele servers (Gonic, Airsonic, …) kunnen met beperkte functionaliteit werken, omdat Psysonic veel Navidrome-specifieke API-endpoints gebruikt.',
|
serverCompatible: 'Gemaakt voor Navidrome. Andere Subsonic-compatibele servers (Gonic, Airsonic, …) kunnen met beperkte functionaliteit werken, omdat Psysonic veel Navidrome-specifieke API-endpoints gebruikt.',
|
||||||
|
|||||||
@@ -51,6 +51,8 @@ export const settings = {
|
|||||||
serverConnecting: 'Se conectează…',
|
serverConnecting: 'Se conectează…',
|
||||||
serverConnected: 'Conectat!',
|
serverConnected: 'Conectat!',
|
||||||
serverFailed: 'Conexiune eșuată.',
|
serverFailed: 'Conexiune eșuată.',
|
||||||
|
serverAddUnreachable: 'Serverul nu poate fi accesat — verifică adresa, rețeaua ta și certificatul TLS al serverului. ({{detail}})',
|
||||||
|
serverAddRejected: 'Serverul a refuzat conexiunea: {{message}}',
|
||||||
testBtn: 'Testează Conexiunea',
|
testBtn: 'Testează Conexiunea',
|
||||||
testingBtn: 'Se testează…',
|
testingBtn: 'Se testează…',
|
||||||
serverCompatible: 'Făcut pentru Navidrome. Alte servere compatibile cu Subsonic (Gonic, Airsonic, …) pot rula cu funcționalitate redusă, deoarece Psysonic folosește multe endpointuri API specifice Navidrome.',
|
serverCompatible: 'Făcut pentru Navidrome. Alte servere compatibile cu Subsonic (Gonic, Airsonic, …) pot rula cu funcționalitate redusă, deoarece Psysonic folosește multe endpointuri API specifice Navidrome.',
|
||||||
|
|||||||
@@ -51,6 +51,8 @@ export const settings = {
|
|||||||
serverConnecting: 'Подключение…',
|
serverConnecting: 'Подключение…',
|
||||||
serverConnected: 'Подключено!',
|
serverConnected: 'Подключено!',
|
||||||
serverFailed: 'Ошибка подключения.',
|
serverFailed: 'Ошибка подключения.',
|
||||||
|
serverAddUnreachable: 'Не удалось подключиться к серверу — проверьте адрес, сеть и TLS-сертификат сервера. ({{detail}})',
|
||||||
|
serverAddRejected: 'Сервер отклонил подключение: {{message}}',
|
||||||
testBtn: 'Проверить',
|
testBtn: 'Проверить',
|
||||||
testingBtn: 'Проверка…',
|
testingBtn: 'Проверка…',
|
||||||
serverCompatible: 'Разработано для Navidrome. Другие Subsonic-совместимые серверы (Gonic, Airsonic, …) могут работать с ограничениями, так как Psysonic использует много специфичных для Navidrome API-эндпоинтов.',
|
serverCompatible: 'Разработано для Navidrome. Другие Subsonic-совместимые серверы (Gonic, Airsonic, …) могут работать с ограничениями, так как Psysonic использует много специфичных для Navidrome API-эндпоинтов.',
|
||||||
|
|||||||
@@ -51,6 +51,8 @@ export const settings = {
|
|||||||
serverConnecting: '正在连接…',
|
serverConnecting: '正在连接…',
|
||||||
serverConnected: '已连接!',
|
serverConnected: '已连接!',
|
||||||
serverFailed: '连接失败。',
|
serverFailed: '连接失败。',
|
||||||
|
serverAddUnreachable: '无法连接服务器 — 请检查地址、网络以及服务器的 TLS 证书。({{detail}})',
|
||||||
|
serverAddRejected: '服务器拒绝了连接:{{message}}',
|
||||||
testBtn: '测试连接',
|
testBtn: '测试连接',
|
||||||
testingBtn: '正在测试…',
|
testingBtn: '正在测试…',
|
||||||
serverCompatible: '专为 Navidrome 构建。其他兼容 Subsonic 的服务器(Gonic、Airsonic 等)可能功能受限,因为 Psysonic 使用了许多 Navidrome 特有的 API 端点。',
|
serverCompatible: '专为 Navidrome 构建。其他兼容 Subsonic 的服务器(Gonic、Airsonic 等)可能功能受限,因为 Psysonic 使用了许多 Navidrome 特有的 API 端点。',
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import type { PingFailure } from '../../api/subsonicTypes';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map a ping failure to an i18n key (+ interpolation vars) for the connection
|
||||||
|
* error toast, so the user sees a specific reason instead of a generic
|
||||||
|
* "Connection failed":
|
||||||
|
* - network/TLS → actionable "couldn't reach the server" message with detail
|
||||||
|
* - any reachable-but-rejected case (auth, version, other) → the server's own
|
||||||
|
* Subsonic error message, which is already human-readable.
|
||||||
|
*/
|
||||||
|
export function pingFailureMessage(failure: PingFailure | undefined): {
|
||||||
|
key: string;
|
||||||
|
vars?: Record<string, string | number>;
|
||||||
|
} {
|
||||||
|
if (failure?.reason === 'network') {
|
||||||
|
return { key: 'settings.serverAddUnreachable', vars: { detail: failure.message ?? '' } };
|
||||||
|
}
|
||||||
|
if (failure?.message) {
|
||||||
|
return { key: 'settings.serverAddRejected', vars: { message: failure.message } };
|
||||||
|
}
|
||||||
|
return { key: 'settings.serverFailed' };
|
||||||
|
}
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import md5 from 'md5';
|
import md5 from 'md5';
|
||||||
import { apiWithCredentials, restBaseFromUrl, secureRandomSalt, SUBSONIC_CLIENT } from '../../api/subsonicClient';
|
import { apiWithCredentials, restBaseFromUrl, secureRandomSalt, SUBSONIC_CLIENT, SUBSONIC_API_VERSION } from '../../api/subsonicClient';
|
||||||
import type { ServerProfile } from '../../store/authStoreTypes';
|
import type { ServerProfile } from '../../store/authStoreTypes';
|
||||||
import { allNormalizedAddresses } from './serverEndpoint';
|
import { allNormalizedAddresses } from './serverEndpoint';
|
||||||
|
|
||||||
@@ -64,7 +64,7 @@ async function fetchPingFingerprint(
|
|||||||
u: username,
|
u: username,
|
||||||
t: token,
|
t: token,
|
||||||
s: salt,
|
s: salt,
|
||||||
v: '1.16.1',
|
v: SUBSONIC_API_VERSION,
|
||||||
c: SUBSONIC_CLIENT,
|
c: SUBSONIC_CLIENT,
|
||||||
f: 'json',
|
f: 'json',
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user