From a24ca370eb413b333dc948693e5f709be209fe4c Mon Sep 17 00:00:00 2001 From: afkarxyz Date: Sun, 19 Apr 2026 22:35:03 +0700 Subject: [PATCH] .refine status check --- frontend/src/App.tsx | 2 + frontend/src/assets/icons/am.png | Bin 0 -> 9483 bytes frontend/src/assets/icons/dzr.png | Bin 0 -> 23621 bytes frontend/src/components/ApiStatusTab.tsx | 84 +++++++-- frontend/src/components/PlatformIcons.tsx | 8 + frontend/src/hooks/useApiStatus.ts | 4 +- frontend/src/lib/api-status.ts | 207 +++++++++++++++++++--- 7 files changed, 262 insertions(+), 43 deletions(-) create mode 100644 frontend/src/assets/icons/am.png create mode 100644 frontend/src/assets/icons/dzr.png diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index df1ca54..e5288d0 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -32,6 +32,7 @@ import { useMetadata } from "@/hooks/useMetadata"; import { useLyrics } from "@/hooks/useLyrics"; import { useCover } from "@/hooks/useCover"; import { useAvailability } from "@/hooks/useAvailability"; +import { ensureSpotiFLACNextStatusCheckStarted } from "@/lib/api-status"; import { useDownloadQueueDialog } from "@/hooks/useDownloadQueueDialog"; import { useDownloadProgress } from "@/hooks/useDownloadProgress"; import { buildPlaylistFolderName } from "@/lib/playlist"; @@ -197,6 +198,7 @@ function App() { }; mediaQuery.addEventListener("change", handleChange); checkForUpdates(); + ensureSpotiFLACNextStatusCheckStarted(); void loadHistory(); return () => { mediaQuery.removeEventListener("change", handleChange); diff --git a/frontend/src/assets/icons/am.png b/frontend/src/assets/icons/am.png new file mode 100644 index 0000000000000000000000000000000000000000..5040e0e2368630b36b657436b2590e945e89d136 GIT binary patch literal 9483 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 P2~bzoQL1`u6Y;+QY2~>o literal 0 HcmV?d00001 diff --git a/frontend/src/assets/icons/dzr.png b/frontend/src/assets/icons/dzr.png new file mode 100644 index 0000000000000000000000000000000000000000..c31e8becc995dfebd15be91c7abf351fd21276c4 GIT binary patch literal 23621 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 literal 0 HcmV?d00001 diff --git a/frontend/src/components/ApiStatusTab.tsx b/frontend/src/components/ApiStatusTab.tsx index 0617a15..730a55e 100644 --- a/frontend/src/components/ApiStatusTab.tsx +++ b/frontend/src/components/ApiStatusTab.tsx @@ -1,34 +1,82 @@ import { Button } from "@/components/ui/button"; import { SearchCheck, CheckCircle2, XCircle, Loader2 } from "lucide-react"; -import { TidalIcon, QobuzIcon, AmazonIcon, MusicBrainzIcon } from "./PlatformIcons"; +import { TidalIcon, QobuzIcon, AmazonIcon, MusicBrainzIcon, AppleMusicIcon, DeezerIcon } from "./PlatformIcons"; import { useApiStatus } from "@/hooks/useApiStatus"; +import { SPOTIFLAC_NEXT_SOURCES } from "@/lib/api-status"; + +function renderStatusIcon(status: "checking" | "online" | "offline" | "idle") { + if (status === "online") { + return ; + } + if (status === "offline") { + return ; + } + return null; +} + +function renderPlatformIcon(type: string) { + if (type === "tidal") { + return ; + } + if (type === "amazon") { + return ; + } + if (type === "musicbrainz") { + return ; + } + if (type === "deezer") { + return ; + } + if (type === "apple") { + return ; + } + return ; +} + export function ApiStatusTab() { - const { sources, statuses, isCheckingAll, checkAll } = useApiStatus(); + const { sources, statuses, nextStatuses, checkingSources, checkOne } = useApiStatus(); return (
-
- +
+

SpotiFLAC Services

+ +
+ {sources.map((source) => { + const status = statuses[source.id] || "idle"; + const isChecking = checkingSources[source.id] === true; + return (
+
+
+ {renderPlatformIcon(source.type)} +

{source.name}

+
+
{renderStatusIcon(status)}
+
+ +
); + })} +
-
- {sources.map((source) => { - const status = statuses[source.id] || "idle"; +
+ +
+

SpotiFLAC Next Services

+ +
+ {SPOTIFLAC_NEXT_SOURCES.map((source) => { + const status = nextStatuses[source.id] || "idle"; return (
- {source.type === "tidal" ? : source.type === "amazon" ? : source.type === "musicbrainz" ? : } + {renderPlatformIcon(source.id)}

{source.name}

- -
- {status === "checking" && } - {status === "online" && } - {status === "offline" && } - {status === "idle" &&
} -
+
{renderStatusIcon(status)}
); })} +
); } diff --git a/frontend/src/components/PlatformIcons.tsx b/frontend/src/components/PlatformIcons.tsx index e944837..388e1c6 100644 --- a/frontend/src/components/PlatformIcons.tsx +++ b/frontend/src/components/PlatformIcons.tsx @@ -1,4 +1,6 @@ import amazonMusicIcon from "../assets/icons/amzn.png"; +import appleMusicIcon from "../assets/icons/am.png"; +import deezerIcon from "../assets/icons/dzr.png"; import lrclibIcon from "../assets/icons/lrclib.png"; import musicBrainzDarkIcon from "../assets/icons/musicbrainz_d.png"; import musicBrainzLightIcon from "../assets/icons/musicbrainz_l.png"; @@ -81,6 +83,12 @@ export function QobuzIcon({ className = "w-4 h-4" }: PlatformIconProps) { export function AmazonIcon({ className = "w-4 h-4" }: PlatformIconProps) { return ; } +export function AppleMusicIcon({ className = "w-4 h-4" }: PlatformIconProps) { + return ; +} +export function DeezerIcon({ className = "w-4 h-4" }: PlatformIconProps) { + return ; +} export function LrclibIcon({ className = "w-4 h-4" }: PlatformIconProps) { return ; } diff --git a/frontend/src/hooks/useApiStatus.ts b/frontend/src/hooks/useApiStatus.ts index bff3601..b311a45 100644 --- a/frontend/src/hooks/useApiStatus.ts +++ b/frontend/src/hooks/useApiStatus.ts @@ -1,5 +1,5 @@ import { useEffect, useState } from "react"; -import { API_SOURCES, checkAllApiStatuses, getApiStatusState, subscribeApiStatus, } from "@/lib/api-status"; +import { API_SOURCES, checkApiStatus, getApiStatusState, subscribeApiStatus, } from "@/lib/api-status"; export function useApiStatus() { const [state, setState] = useState(getApiStatusState); useEffect(() => { @@ -10,6 +10,6 @@ export function useApiStatus() { return { ...state, sources: API_SOURCES, - checkAll: () => checkAllApiStatuses(false), + checkOne: (sourceId: string) => checkApiStatus(sourceId), }; } diff --git a/frontend/src/lib/api-status.ts b/frontend/src/lib/api-status.ts index 15dd0b7..c89b4a4 100644 --- a/frontend/src/lib/api-status.ts +++ b/frontend/src/lib/api-status.ts @@ -10,6 +10,24 @@ export interface ApiSource { url: string; } +interface SpotiFLACNextSource { + id: string; + name: string; +} + +type SpotiFLACNextStatusResponse = { + tidal?: string; + qobuz_a?: string; + qobuz_b?: string; + qobuz_c?: string; + deezer_a?: string; + deezer_b?: string; + amazon_a?: string; + amazon_b?: string; + amazon_c?: string; + apple?: string; +}; + export const API_SOURCES: ApiSource[] = [ { id: "tidal", type: "tidal", name: "Tidal", url: "" }, { id: "qobuz", type: "qobuz", name: "Qobuz", url: "" }, @@ -17,17 +35,32 @@ export const API_SOURCES: ApiSource[] = [ { id: "musicbrainz", type: "musicbrainz", name: "MusicBrainz", url: "https://musicbrainz.org" }, ]; +export const SPOTIFLAC_NEXT_SOURCES: SpotiFLACNextSource[] = [ + { id: "tidal", name: "Tidal" }, + { id: "qobuz", name: "Qobuz" }, + { id: "amazon", name: "Amazon Music" }, + { id: "deezer", name: "Deezer" }, + { id: "apple", name: "Apple Music" }, +]; + +const SPOTIFLAC_NEXT_STATUS_URL = "https://status.spotbye.qzz.io/status"; +const SPOTIFLAC_NEXT_MAX_ATTEMPTS = 3; +const SPOTIFLAC_NEXT_RETRY_DELAY_MS = 1200; + type ApiStatusState = { - isCheckingAll: boolean; + checkingSources: Record; statuses: Record; + nextStatuses: Record; }; let apiStatusState: ApiStatusState = { - isCheckingAll: false, + checkingSources: {}, statuses: {}, + nextStatuses: {}, }; -let activeCheckAll: Promise | null = null; +let activeCheckNextOnly: Promise | null = null; +const activeSourceChecks = new Map>(); const listeners = new Set<() => void>(); function emitApiStatusChange() { @@ -51,6 +84,67 @@ async function checkSourceStatus(source: ApiSource): Promise { } } +function statusFromNextValue(value: string | undefined): ApiCheckStatus { + return value === "up" ? "online" : "offline"; +} + +function anyNextVariantUp(values: Array): ApiCheckStatus { + return values.some((value) => value === "up") ? "online" : "offline"; +} + +function delay(ms: number): Promise { + return new Promise((resolve) => window.setTimeout(resolve, ms)); +} + +function getSafeNextStatusesFallback(currentStatuses: Record): Record { + return SPOTIFLAC_NEXT_SOURCES.reduce>((acc, source) => { + const current = currentStatuses[source.id]; + acc[source.id] = current === "online" || current === "offline" ? current : "idle"; + return acc; + }, {}); +} + +async function fetchSpotiFLACNextStatusesOnce(): Promise> { + const response = await withTimeout(fetch(SPOTIFLAC_NEXT_STATUS_URL, { + method: "GET", + cache: "no-store", + headers: { + Accept: "application/json", + }, + }), CHECK_TIMEOUT_MS, "SpotiFLAC Next status check timed out after 10 seconds"); + + if (!response.ok) { + throw new Error(`SpotiFLAC Next status returned ${response.status}`); + } + + const payload = (await response.json()) as SpotiFLACNextStatusResponse; + return { + tidal: statusFromNextValue(payload.tidal), + qobuz: anyNextVariantUp([payload.qobuz_a, payload.qobuz_b, payload.qobuz_c]), + deezer: anyNextVariantUp([payload.deezer_a, payload.deezer_b]), + amazon: anyNextVariantUp([payload.amazon_a, payload.amazon_b, payload.amazon_c]), + apple: statusFromNextValue(payload.apple), + }; +} + +async function checkSpotiFLACNextStatuses(): Promise> { + let lastError: unknown = null; + + for (let attempt = 1; attempt <= SPOTIFLAC_NEXT_MAX_ATTEMPTS; attempt++) { + try { + return await fetchSpotiFLACNextStatusesOnce(); + } + catch (error) { + lastError = error; + if (attempt < SPOTIFLAC_NEXT_MAX_ATTEMPTS) { + await delay(SPOTIFLAC_NEXT_RETRY_DELAY_MS * attempt); + } + } + } + + throw lastError instanceof Error ? lastError : new Error("SpotiFLAC Next status check failed"); +} + export function getApiStatusState(): ApiStatusState { return apiStatusState; } @@ -62,44 +156,111 @@ export function subscribeApiStatus(listener: () => void): () => void { }; } -export async function checkAllApiStatuses(_forceRefresh: boolean = false): Promise { - if (activeCheckAll) { - return activeCheckAll; +function hasSpotiFLACNextResults(): boolean { + return SPOTIFLAC_NEXT_SOURCES.some((source) => { + const status = apiStatusState.nextStatuses[source.id]; + return status === "online" || status === "offline"; + }); +} + +export async function checkSpotiFLACNextStatusesOnly(): Promise { + if (activeCheckNextOnly) { + return activeCheckNextOnly; } - activeCheckAll = (async () => { - const checkingStatuses = Object.fromEntries(API_SOURCES.map((source) => [source.id, "checking" as ApiCheckStatus])); + activeCheckNextOnly = (async () => { + const checkingNextStatuses = Object.fromEntries(SPOTIFLAC_NEXT_SOURCES.map((source) => [source.id, "checking" as ApiCheckStatus])); setApiStatusState((current) => ({ ...current, - isCheckingAll: true, - statuses: { - ...current.statuses, - ...checkingStatuses, + nextStatuses: { + ...current.nextStatuses, + ...checkingNextStatuses, }, })); try { - const results = await Promise.all(API_SOURCES.map(async (source) => ({ - id: source.id, - status: await checkSourceStatus(source), - }))); + setApiStatusState((current) => ({ + ...current, + nextStatuses: { ...current.nextStatuses }, + })); + + const nextStatuses = await checkSpotiFLACNextStatuses(); setApiStatusState((current) => ({ ...current, - statuses: results.reduce>((acc, result) => { - acc[result.id] = result.status; - return acc; - }, { ...current.statuses }), + nextStatuses: { + ...current.nextStatuses, + ...nextStatuses, + }, + })); + } + catch { + setApiStatusState((current) => ({ + ...current, + nextStatuses: getSafeNextStatusesFallback(current.nextStatuses), + })); + } + finally { + activeCheckNextOnly = null; + } + })(); + + return activeCheckNextOnly; +} + +export function ensureSpotiFLACNextStatusCheckStarted(): void { + if (!activeCheckNextOnly && !hasSpotiFLACNextResults()) { + void checkSpotiFLACNextStatusesOnly(); + } +} + +export async function checkApiStatus(sourceId: string): Promise { + const source = API_SOURCES.find((item) => item.id === sourceId); + if (!source) { + return; + } + + const activeCheck = activeSourceChecks.get(sourceId); + if (activeCheck) { + return activeCheck; + } + + const task = (async () => { + setApiStatusState((current) => ({ + ...current, + checkingSources: { + ...current.checkingSources, + [sourceId]: true, + }, + statuses: { + ...current.statuses, + [sourceId]: "checking", + }, + })); + + try { + const status = await checkSourceStatus(source); + + setApiStatusState((current) => ({ + ...current, + statuses: { + ...current.statuses, + [sourceId]: status, + }, })); } finally { setApiStatusState((current) => ({ ...current, - isCheckingAll: false, + checkingSources: { + ...current.checkingSources, + [sourceId]: false, + }, })); - activeCheckAll = null; + activeSourceChecks.delete(sourceId); } })(); - return activeCheckAll; + activeSourceChecks.set(sourceId, task); + return task; }