This commit is contained in:
afkarxyz
2026-04-19 23:14:12 +07:00
parent 7c52c2d9b4
commit 2f78f7e7c7
3 changed files with 0 additions and 43 deletions
-1
View File
@@ -18,4 +18,3 @@ func NewRequestWithDefaultHeaders(method string, rawURL string, body io.Reader)
return req, nil return req, nil
} }
-3
View File
@@ -3,7 +3,6 @@ import { SearchCheck, CheckCircle2, XCircle, Loader2 } from "lucide-react";
import { TidalIcon, QobuzIcon, AmazonIcon, MusicBrainzIcon, AppleMusicIcon, DeezerIcon } from "./PlatformIcons"; import { TidalIcon, QobuzIcon, AmazonIcon, MusicBrainzIcon, AppleMusicIcon, DeezerIcon } from "./PlatformIcons";
import { useApiStatus } from "@/hooks/useApiStatus"; import { useApiStatus } from "@/hooks/useApiStatus";
import { SPOTIFLAC_NEXT_SOURCES } from "@/lib/api-status"; import { SPOTIFLAC_NEXT_SOURCES } from "@/lib/api-status";
function renderStatusIcon(status: "checking" | "online" | "offline" | "idle") { function renderStatusIcon(status: "checking" | "online" | "offline" | "idle") {
if (status === "online") { if (status === "online") {
return <CheckCircle2 className="h-5 w-5 text-emerald-500"/>; return <CheckCircle2 className="h-5 w-5 text-emerald-500"/>;
@@ -13,7 +12,6 @@ function renderStatusIcon(status: "checking" | "online" | "offline" | "idle") {
} }
return null; return null;
} }
function renderPlatformIcon(type: string) { function renderPlatformIcon(type: string) {
if (type === "tidal") { if (type === "tidal") {
return <TidalIcon className="w-5 h-5 shrink-0 text-muted-foreground"/>; return <TidalIcon className="w-5 h-5 shrink-0 text-muted-foreground"/>;
@@ -32,7 +30,6 @@ function renderPlatformIcon(type: string) {
} }
return <QobuzIcon className="w-5 h-5 shrink-0 text-muted-foreground"/>; return <QobuzIcon className="w-5 h-5 shrink-0 text-muted-foreground"/>;
} }
export function ApiStatusTab() { export function ApiStatusTab() {
const { sources, statuses, nextStatuses, checkingSources, checkOne } = useApiStatus(); const { sources, statuses, nextStatuses, checkingSources, checkOne } = useApiStatus();
return (<div className="space-y-6"> return (<div className="space-y-6">
-39
View File
@@ -1,20 +1,16 @@
import { CheckAPIStatus } from "../../wailsjs/go/main/App"; import { CheckAPIStatus } from "../../wailsjs/go/main/App";
import { CHECK_TIMEOUT_MS, withTimeout } from "@/lib/async-timeout"; import { CHECK_TIMEOUT_MS, withTimeout } from "@/lib/async-timeout";
export type ApiCheckStatus = "checking" | "online" | "offline" | "idle"; export type ApiCheckStatus = "checking" | "online" | "offline" | "idle";
export interface ApiSource { export interface ApiSource {
id: string; id: string;
type: string; type: string;
name: string; name: string;
url: string; url: string;
} }
interface SpotiFLACNextSource { interface SpotiFLACNextSource {
id: string; id: string;
name: string; name: string;
} }
type SpotiFLACNextStatusResponse = { type SpotiFLACNextStatusResponse = {
tidal?: string; tidal?: string;
qobuz_a?: string; qobuz_a?: string;
@@ -27,14 +23,12 @@ type SpotiFLACNextStatusResponse = {
amazon_c?: string; amazon_c?: string;
apple?: string; apple?: string;
}; };
export const API_SOURCES: ApiSource[] = [ export const API_SOURCES: ApiSource[] = [
{ id: "tidal", type: "tidal", name: "Tidal", url: "" }, { id: "tidal", type: "tidal", name: "Tidal", url: "" },
{ id: "qobuz", type: "qobuz", name: "Qobuz", url: "" }, { id: "qobuz", type: "qobuz", name: "Qobuz", url: "" },
{ id: "amazon", type: "amazon", name: "Amazon Music", url: "" }, { id: "amazon", type: "amazon", name: "Amazon Music", url: "" },
{ id: "musicbrainz", type: "musicbrainz", name: "MusicBrainz", url: "https://musicbrainz.org" }, { id: "musicbrainz", type: "musicbrainz", name: "MusicBrainz", url: "https://musicbrainz.org" },
]; ];
export const SPOTIFLAC_NEXT_SOURCES: SpotiFLACNextSource[] = [ export const SPOTIFLAC_NEXT_SOURCES: SpotiFLACNextSource[] = [
{ id: "tidal", name: "Tidal" }, { id: "tidal", name: "Tidal" },
{ id: "qobuz", name: "Qobuz" }, { id: "qobuz", name: "Qobuz" },
@@ -42,38 +36,31 @@ export const SPOTIFLAC_NEXT_SOURCES: SpotiFLACNextSource[] = [
{ id: "deezer", name: "Deezer" }, { id: "deezer", name: "Deezer" },
{ id: "apple", name: "Apple Music" }, { id: "apple", name: "Apple Music" },
]; ];
const SPOTIFLAC_NEXT_STATUS_URL = "https://status.spotbye.qzz.io/status"; const SPOTIFLAC_NEXT_STATUS_URL = "https://status.spotbye.qzz.io/status";
const SPOTIFLAC_NEXT_MAX_ATTEMPTS = 3; const SPOTIFLAC_NEXT_MAX_ATTEMPTS = 3;
const SPOTIFLAC_NEXT_RETRY_DELAY_MS = 1200; const SPOTIFLAC_NEXT_RETRY_DELAY_MS = 1200;
type ApiStatusState = { type ApiStatusState = {
checkingSources: Record<string, boolean>; checkingSources: Record<string, boolean>;
statuses: Record<string, ApiCheckStatus>; statuses: Record<string, ApiCheckStatus>;
nextStatuses: Record<string, ApiCheckStatus>; nextStatuses: Record<string, ApiCheckStatus>;
}; };
let apiStatusState: ApiStatusState = { let apiStatusState: ApiStatusState = {
checkingSources: {}, checkingSources: {},
statuses: {}, statuses: {},
nextStatuses: {}, nextStatuses: {},
}; };
let activeCheckNextOnly: Promise<void> | null = null; let activeCheckNextOnly: Promise<void> | null = null;
const activeSourceChecks = new Map<string, Promise<void>>(); const activeSourceChecks = new Map<string, Promise<void>>();
const listeners = new Set<() => void>(); const listeners = new Set<() => void>();
function emitApiStatusChange() { function emitApiStatusChange() {
for (const listener of listeners) { for (const listener of listeners) {
listener(); listener();
} }
} }
function setApiStatusState(updater: (current: ApiStatusState) => ApiStatusState) { function setApiStatusState(updater: (current: ApiStatusState) => ApiStatusState) {
apiStatusState = updater(apiStatusState); apiStatusState = updater(apiStatusState);
emitApiStatusChange(); emitApiStatusChange();
} }
async function checkSourceStatus(source: ApiSource): Promise<ApiCheckStatus> { async function checkSourceStatus(source: ApiSource): Promise<ApiCheckStatus> {
try { try {
const isOnline = await withTimeout(CheckAPIStatus(source.type, source.url), CHECK_TIMEOUT_MS, `API status check timed out after 10 seconds for ${source.name}`); const isOnline = await withTimeout(CheckAPIStatus(source.type, source.url), CHECK_TIMEOUT_MS, `API status check timed out after 10 seconds for ${source.name}`);
@@ -83,19 +70,15 @@ async function checkSourceStatus(source: ApiSource): Promise<ApiCheckStatus> {
return "offline"; return "offline";
} }
} }
function statusFromNextValue(value: string | undefined): ApiCheckStatus { function statusFromNextValue(value: string | undefined): ApiCheckStatus {
return value === "up" ? "online" : "offline"; return value === "up" ? "online" : "offline";
} }
function anyNextVariantUp(values: Array<string | undefined>): ApiCheckStatus { function anyNextVariantUp(values: Array<string | undefined>): ApiCheckStatus {
return values.some((value) => value === "up") ? "online" : "offline"; return values.some((value) => value === "up") ? "online" : "offline";
} }
function delay(ms: number): Promise<void> { function delay(ms: number): Promise<void> {
return new Promise((resolve) => window.setTimeout(resolve, ms)); return new Promise((resolve) => window.setTimeout(resolve, ms));
} }
function getSafeNextStatusesFallback(currentStatuses: Record<string, ApiCheckStatus>): Record<string, ApiCheckStatus> { function getSafeNextStatusesFallback(currentStatuses: Record<string, ApiCheckStatus>): Record<string, ApiCheckStatus> {
return SPOTIFLAC_NEXT_SOURCES.reduce<Record<string, ApiCheckStatus>>((acc, source) => { return SPOTIFLAC_NEXT_SOURCES.reduce<Record<string, ApiCheckStatus>>((acc, source) => {
const current = currentStatuses[source.id]; const current = currentStatuses[source.id];
@@ -103,7 +86,6 @@ function getSafeNextStatusesFallback(currentStatuses: Record<string, ApiCheckSta
return acc; return acc;
}, {}); }, {});
} }
async function fetchSpotiFLACNextStatusesOnce(): Promise<Record<string, ApiCheckStatus>> { async function fetchSpotiFLACNextStatusesOnce(): Promise<Record<string, ApiCheckStatus>> {
const response = await withTimeout(fetch(SPOTIFLAC_NEXT_STATUS_URL, { const response = await withTimeout(fetch(SPOTIFLAC_NEXT_STATUS_URL, {
method: "GET", method: "GET",
@@ -112,11 +94,9 @@ async function fetchSpotiFLACNextStatusesOnce(): Promise<Record<string, ApiCheck
Accept: "application/json", Accept: "application/json",
}, },
}), CHECK_TIMEOUT_MS, "SpotiFLAC Next status check timed out after 10 seconds"); }), CHECK_TIMEOUT_MS, "SpotiFLAC Next status check timed out after 10 seconds");
if (!response.ok) { if (!response.ok) {
throw new Error(`SpotiFLAC Next status returned ${response.status}`); throw new Error(`SpotiFLAC Next status returned ${response.status}`);
} }
const payload = (await response.json()) as SpotiFLACNextStatusResponse; const payload = (await response.json()) as SpotiFLACNextStatusResponse;
return { return {
tidal: statusFromNextValue(payload.tidal), tidal: statusFromNextValue(payload.tidal),
@@ -126,10 +106,8 @@ async function fetchSpotiFLACNextStatusesOnce(): Promise<Record<string, ApiCheck
apple: statusFromNextValue(payload.apple), apple: statusFromNextValue(payload.apple),
}; };
} }
async function checkSpotiFLACNextStatuses(): Promise<Record<string, ApiCheckStatus>> { async function checkSpotiFLACNextStatuses(): Promise<Record<string, ApiCheckStatus>> {
let lastError: unknown = null; let lastError: unknown = null;
for (let attempt = 1; attempt <= SPOTIFLAC_NEXT_MAX_ATTEMPTS; attempt++) { for (let attempt = 1; attempt <= SPOTIFLAC_NEXT_MAX_ATTEMPTS; attempt++) {
try { try {
return await fetchSpotiFLACNextStatusesOnce(); return await fetchSpotiFLACNextStatusesOnce();
@@ -141,33 +119,27 @@ async function checkSpotiFLACNextStatuses(): Promise<Record<string, ApiCheckStat
} }
} }
} }
throw lastError instanceof Error ? lastError : new Error("SpotiFLAC Next status check failed"); throw lastError instanceof Error ? lastError : new Error("SpotiFLAC Next status check failed");
} }
export function getApiStatusState(): ApiStatusState { export function getApiStatusState(): ApiStatusState {
return apiStatusState; return apiStatusState;
} }
export function subscribeApiStatus(listener: () => void): () => void { export function subscribeApiStatus(listener: () => void): () => void {
listeners.add(listener); listeners.add(listener);
return () => { return () => {
listeners.delete(listener); listeners.delete(listener);
}; };
} }
function hasSpotiFLACNextResults(): boolean { function hasSpotiFLACNextResults(): boolean {
return SPOTIFLAC_NEXT_SOURCES.some((source) => { return SPOTIFLAC_NEXT_SOURCES.some((source) => {
const status = apiStatusState.nextStatuses[source.id]; const status = apiStatusState.nextStatuses[source.id];
return status === "online" || status === "offline"; return status === "online" || status === "offline";
}); });
} }
export async function checkSpotiFLACNextStatusesOnly(): Promise<void> { export async function checkSpotiFLACNextStatusesOnly(): Promise<void> {
if (activeCheckNextOnly) { if (activeCheckNextOnly) {
return activeCheckNextOnly; return activeCheckNextOnly;
} }
activeCheckNextOnly = (async () => { activeCheckNextOnly = (async () => {
const checkingNextStatuses = Object.fromEntries(SPOTIFLAC_NEXT_SOURCES.map((source) => [source.id, "checking" as ApiCheckStatus])); const checkingNextStatuses = Object.fromEntries(SPOTIFLAC_NEXT_SOURCES.map((source) => [source.id, "checking" as ApiCheckStatus]));
setApiStatusState((current) => ({ setApiStatusState((current) => ({
@@ -177,15 +149,12 @@ export async function checkSpotiFLACNextStatusesOnly(): Promise<void> {
...checkingNextStatuses, ...checkingNextStatuses,
}, },
})); }));
try { try {
setApiStatusState((current) => ({ setApiStatusState((current) => ({
...current, ...current,
nextStatuses: { ...current.nextStatuses }, nextStatuses: { ...current.nextStatuses },
})); }));
const nextStatuses = await checkSpotiFLACNextStatuses(); const nextStatuses = await checkSpotiFLACNextStatuses();
setApiStatusState((current) => ({ setApiStatusState((current) => ({
...current, ...current,
nextStatuses: { nextStatuses: {
@@ -204,27 +173,22 @@ export async function checkSpotiFLACNextStatusesOnly(): Promise<void> {
activeCheckNextOnly = null; activeCheckNextOnly = null;
} }
})(); })();
return activeCheckNextOnly; return activeCheckNextOnly;
} }
export function ensureSpotiFLACNextStatusCheckStarted(): void { export function ensureSpotiFLACNextStatusCheckStarted(): void {
if (!activeCheckNextOnly && !hasSpotiFLACNextResults()) { if (!activeCheckNextOnly && !hasSpotiFLACNextResults()) {
void checkSpotiFLACNextStatusesOnly(); void checkSpotiFLACNextStatusesOnly();
} }
} }
export async function checkApiStatus(sourceId: string): Promise<void> { export async function checkApiStatus(sourceId: string): Promise<void> {
const source = API_SOURCES.find((item) => item.id === sourceId); const source = API_SOURCES.find((item) => item.id === sourceId);
if (!source) { if (!source) {
return; return;
} }
const activeCheck = activeSourceChecks.get(sourceId); const activeCheck = activeSourceChecks.get(sourceId);
if (activeCheck) { if (activeCheck) {
return activeCheck; return activeCheck;
} }
const task = (async () => { const task = (async () => {
setApiStatusState((current) => ({ setApiStatusState((current) => ({
...current, ...current,
@@ -237,10 +201,8 @@ export async function checkApiStatus(sourceId: string): Promise<void> {
[sourceId]: "checking", [sourceId]: "checking",
}, },
})); }));
try { try {
const status = await checkSourceStatus(source); const status = await checkSourceStatus(source);
setApiStatusState((current) => ({ setApiStatusState((current) => ({
...current, ...current,
statuses: { statuses: {
@@ -260,7 +222,6 @@ export async function checkApiStatus(sourceId: string): Promise<void> {
activeSourceChecks.delete(sourceId); activeSourceChecks.delete(sourceId);
} }
})(); })();
activeSourceChecks.set(sourceId, task); activeSourceChecks.set(sourceId, task);
return task; return task;
} }