mirror of
https://github.com/ZHANGTIANYAO1/teamspeak-music-bot.git
synced 2026-10-02 21:12:49 +08:00
feat(spotify): Connect-Spotify settings card (config + OAuth login + status) [S4.4]
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
6e0a0f7392
commit
488734c0db
3 files changed
+432
-2
No files matched your search
@@ -0,0 +1,107 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import {
|
||||
buildSpotifyPayload,
|
||||
parseSpotifyRedirect,
|
||||
statusSummary,
|
||||
SPOTIFY_DISCLAIMER,
|
||||
type SpotifyConfigForm,
|
||||
type SpotifyStatus,
|
||||
} from "./useSpotifySettings.js";
|
||||
|
||||
function form(overrides: Partial<SpotifyConfigForm> = {}): SpotifyConfigForm {
|
||||
return {
|
||||
enabled: true,
|
||||
backend: "auto",
|
||||
clientId: "abc",
|
||||
clientSecret: "",
|
||||
deviceName: "TS-Bot",
|
||||
bitrate: 320,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe("buildSpotifyPayload", () => {
|
||||
it("always includes enabled/backend/clientId/deviceName/bitrate under a spotify key", () => {
|
||||
const { spotify } = buildSpotifyPayload(form());
|
||||
expect(spotify).toMatchObject({
|
||||
enabled: true,
|
||||
backend: "auto",
|
||||
clientId: "abc",
|
||||
deviceName: "TS-Bot",
|
||||
bitrate: 320,
|
||||
});
|
||||
});
|
||||
|
||||
it("omits clientSecret when blank (means unchanged, never wipes)", () => {
|
||||
const { spotify } = buildSpotifyPayload(form({ clientSecret: "" }));
|
||||
expect("clientSecret" in spotify).toBe(false);
|
||||
});
|
||||
|
||||
it("includes clientSecret only when non-blank", () => {
|
||||
const { spotify } = buildSpotifyPayload(form({ clientSecret: "s3cr3t" }));
|
||||
expect(spotify.clientSecret).toBe("s3cr3t");
|
||||
});
|
||||
|
||||
it("carries a non-default backend through unchanged", () => {
|
||||
const { spotify } = buildSpotifyPayload(form({ backend: "librespot" }));
|
||||
expect(spotify.backend).toBe("librespot");
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseSpotifyRedirect", () => {
|
||||
it("maps ?spotify=success to 'success'", () => {
|
||||
expect(parseSpotifyRedirect("?spotify=success")).toBe("success");
|
||||
});
|
||||
|
||||
it("maps ?spotify=error to 'error'", () => {
|
||||
expect(parseSpotifyRedirect("?spotify=error")).toBe("error");
|
||||
});
|
||||
|
||||
it("returns null for unrelated params", () => {
|
||||
expect(parseSpotifyRedirect("?x=1")).toBeNull();
|
||||
});
|
||||
|
||||
it("returns null for an empty search string", () => {
|
||||
expect(parseSpotifyRedirect("")).toBeNull();
|
||||
});
|
||||
|
||||
it("returns null for an unexpected spotify value", () => {
|
||||
expect(parseSpotifyRedirect("?spotify=maybe")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("statusSummary", () => {
|
||||
const ok: SpotifyStatus = { authorized: true, backend: "go-librespot", deviceName: "d", binaryAvailable: true };
|
||||
|
||||
it("is 'off' tone when disabled, regardless of status", () => {
|
||||
expect(statusSummary(null, false).tone).toBe("off");
|
||||
expect(statusSummary(ok, false).tone).toBe("off");
|
||||
});
|
||||
|
||||
it("is 'warn' tone when enabled but status is unknown (null)", () => {
|
||||
expect(statusSummary(null, true).tone).toBe("warn");
|
||||
});
|
||||
|
||||
it("is 'warn' tone when the binary is unavailable", () => {
|
||||
const s: SpotifyStatus = { authorized: false, backend: "auto", deviceName: "d", binaryAvailable: false };
|
||||
expect(statusSummary(s, true).tone).toBe("warn");
|
||||
});
|
||||
|
||||
it("is 'warn' tone when the binary exists but not authorized", () => {
|
||||
const s: SpotifyStatus = { authorized: false, backend: "auto", deviceName: "d", binaryAvailable: true };
|
||||
expect(statusSummary(s, true).tone).toBe("warn");
|
||||
});
|
||||
|
||||
it("is 'ok' tone when enabled, authorized and the binary is available", () => {
|
||||
const summary = statusSummary(ok, true);
|
||||
expect(summary.tone).toBe("ok");
|
||||
expect(summary.label).toContain("go-librespot");
|
||||
});
|
||||
});
|
||||
|
||||
describe("SPOTIFY_DISCLAIMER", () => {
|
||||
it("mentions the Premium + grey-area risk copy", () => {
|
||||
expect(SPOTIFY_DISCLAIMER).toContain("Premium");
|
||||
expect(SPOTIFY_DISCLAIMER.length).toBeGreaterThan(20);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,48 @@
|
||||
export interface SpotifyConfigForm {
|
||||
enabled: boolean;
|
||||
backend: "auto" | "go-librespot" | "librespot";
|
||||
clientId: string;
|
||||
clientSecret: string; // blank means "unchanged"
|
||||
deviceName: string;
|
||||
bitrate: number;
|
||||
}
|
||||
|
||||
export interface SpotifyStatus {
|
||||
authorized: boolean;
|
||||
backend: string;
|
||||
deviceName: string;
|
||||
binaryAvailable: boolean;
|
||||
}
|
||||
|
||||
// 实验性 · 灰色地带 · 需要 Premium · 使用你自己的开发者应用凭据
|
||||
export const SPOTIFY_DISCLAIMER =
|
||||
"实验性功能:通过 librespot 播放 Spotify 需要 Spotify Premium 账号,并使用你自己注册的 Spotify 开发者应用凭据。" +
|
||||
"该方式处于 Spotify 服务条款的灰色地带,风险自负;默认关闭,不会内置任何共享凭据。";
|
||||
|
||||
export function buildSpotifyPayload(f: SpotifyConfigForm): { spotify: Record<string, unknown> } {
|
||||
const spotify: Record<string, unknown> = {
|
||||
enabled: f.enabled,
|
||||
backend: f.backend,
|
||||
clientId: f.clientId,
|
||||
deviceName: f.deviceName,
|
||||
bitrate: f.bitrate,
|
||||
};
|
||||
if (f.clientSecret && f.clientSecret.length > 0) spotify.clientSecret = f.clientSecret;
|
||||
return { spotify };
|
||||
}
|
||||
|
||||
export function parseSpotifyRedirect(search: string): "success" | "error" | null {
|
||||
const v = new URLSearchParams(search).get("spotify");
|
||||
return v === "success" || v === "error" ? v : null;
|
||||
}
|
||||
|
||||
export function statusSummary(
|
||||
s: SpotifyStatus | null,
|
||||
enabled: boolean,
|
||||
): { label: string; tone: "ok" | "warn" | "off" } {
|
||||
if (!enabled) return { label: "已关闭", tone: "off" };
|
||||
if (!s) return { label: "未知", tone: "warn" };
|
||||
if (!s.binaryAvailable) return { label: "未检测到 librespot 可执行文件", tone: "warn" };
|
||||
if (!s.authorized) return { label: "未授权(点击“连接 Spotify”登录)", tone: "warn" };
|
||||
return { label: `已就绪 · 后端 ${s.backend}`, tone: "ok" };
|
||||
}
|
||||
Reference in new issue
Block a user