.refine check status

This commit is contained in:
afkarxyz
2026-04-02 08:55:24 +07:00
parent 6066278fe6
commit 264b474903
8 changed files with 276 additions and 94 deletions
+2
View File
@@ -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 { ensureApiStatusCheckStarted } from "@/lib/api-status";
import { useDownloadQueueDialog } from "@/hooks/useDownloadQueueDialog";
import { useDownloadProgress } from "@/hooks/useDownloadProgress";
const HISTORY_KEY = "spotiflac_fetch_history";
@@ -179,6 +180,7 @@ function App() {
};
mediaQuery.addEventListener("change", handleChange);
checkForUpdates();
ensureApiStatusCheckStarted();
loadHistory();
const handleScroll = () => {
setShowScrollTop(window.scrollY > 300);
+1 -2
View File
@@ -273,8 +273,7 @@ export function AboutPage() {
Note
</div>
<p className="text-xs leading-relaxed text-sky-700 dark:text-sky-300">
SpotiFLAC Next is a separate project created as a thank-you
to everyone who has supported SpotiFLAC on Ko-fi.
This project is a thank-you to everyone who supported SpotiFLAC on Ko-fi.
</p>
</div>
</CardContent>)}
+5 -44
View File
@@ -1,59 +1,20 @@
import { useState, useEffect } from "react";
import { Button } from "@/components/ui/button";
import { RefreshCw, CheckCircle2, XCircle, Loader2 } from "lucide-react";
import { CheckAPIStatus } from "../../wailsjs/go/main/App";
import { TidalIcon, QobuzIcon, AmazonIcon } from "./PlatformIcons";
interface ApiSource {
id: string;
type: string;
name: string;
url: string;
}
const SOURCES: ApiSource[] = [
{ id: "tidal1", type: "tidal", name: "Tidal A", url: "https://hifi-one.spotisaver.net" },
{ id: "tidal2", type: "tidal", name: "Tidal B", url: "https://hifi-two.spotisaver.net" },
{ id: "tidal3", type: "tidal", name: "Tidal C", url: "https://eu-central.monochrome.tf" },
{ id: "tidal4", type: "tidal", name: "Tidal D", url: "https://us-west.monochrome.tf" },
{ id: "tidal5", type: "tidal", name: "Tidal E", url: "https://api.monochrome.tf" },
{ id: "tidal6", type: "tidal", name: "Tidal F", url: "https://monochrome-api.samidy.com" },
{ id: "tidal7", type: "tidal", name: "Tidal G", url: "https://tidal.kinoplus.online" },
{ id: "qobuz1", type: "qobuz", name: "Qobuz A", url: "https://dab.yeet.su" },
{ id: "qobuz2", type: "qobuz", name: "Qobuz B", url: "https://dabmusic.xyz" },
{ id: "qobuz3", type: "qbz", name: "Qobuz C", url: "https://qbz.afkarxyz.qzz.io" },
{ id: "amazon1", type: "amazon", name: "Amazon Music", url: "https://amzn.afkarxyz.qzz.io" },
];
import { useApiStatus } from "@/hooks/useApiStatus";
export function ApiStatusTab() {
const [statuses, setStatuses] = useState<Record<string, "checking" | "online" | "offline" | "idle">>({});
const [isCheckingAll, setIsCheckingAll] = useState(false);
const checkStatus = async (sourceId: string, apiType: string, url: string) => {
setStatuses(prev => ({ ...prev, [sourceId]: "checking" }));
try {
const isOnline = await CheckAPIStatus(apiType, url);
setStatuses(prev => ({ ...prev, [sourceId]: isOnline ? "online" : "offline" }));
}
catch (error) {
setStatuses(prev => ({ ...prev, [sourceId]: "offline" }));
}
};
const checkAll = async () => {
setIsCheckingAll(true);
const promises = SOURCES.map(s => checkStatus(s.id, s.type, s.url));
await Promise.allSettled(promises);
setIsCheckingAll(false);
};
useEffect(() => {
checkAll();
}, []);
const { sources, statuses, isCheckingAll, refreshAll } = useApiStatus();
return (<div className="space-y-6">
<div className="flex items-center justify-end">
<Button variant="outline" onClick={checkAll} disabled={isCheckingAll} className="gap-2">
<Button variant="outline" onClick={() => void refreshAll()} disabled={isCheckingAll} className="gap-2">
<RefreshCw className={`h-4 w-4 ${isCheckingAll ? "animate-spin" : ""}`}/>
Refresh All
</Button>
</div>
<div className="grid grid-cols-4 gap-4">
{SOURCES.map((source) => {
{sources.map((source) => {
const status = statuses[source.id] || "idle";
return (<div key={source.id} className="flex items-center justify-between p-4 border rounded-lg bg-card text-card-foreground shadow-sm">
<div className="flex items-center gap-3">
+25
View File
@@ -0,0 +1,25 @@
import { useEffect, useState } from "react";
import {
API_SOURCES,
checkAllApiStatuses,
ensureApiStatusCheckStarted,
getApiStatusState,
subscribeApiStatus,
} from "@/lib/api-status";
export function useApiStatus() {
const [state, setState] = useState(getApiStatusState);
useEffect(() => {
ensureApiStatusCheckStarted();
return subscribeApiStatus(() => {
setState(getApiStatusState());
});
}, []);
return {
...state,
sources: API_SOURCES,
refreshAll: checkAllApiStatuses,
};
}
+6 -1
View File
@@ -2,6 +2,7 @@ import { useState, useCallback } from "react";
import { CheckTrackAvailability } from "../../wailsjs/go/main/App";
import type { TrackAvailability } from "@/types/api";
import { logger } from "@/lib/logger";
import { CHECK_TIMEOUT_MS, withTimeout } from "@/lib/async-timeout";
export function useAvailability() {
const [checking, setChecking] = useState(false);
const [checkingTrackId, setCheckingTrackId] = useState<string | null>(null);
@@ -20,7 +21,11 @@ export function useAvailability() {
setError(null);
try {
logger.info(`Checking availability for track: ${spotifyId}`);
const response = await CheckTrackAvailability(spotifyId);
const response = await withTimeout(
CheckTrackAvailability(spotifyId),
CHECK_TIMEOUT_MS,
`Availability check timed out after 10 seconds for ${spotifyId}`,
);
const availability: TrackAvailability = JSON.parse(response);
setAvailabilityMap((prev) => {
const newMap = new Map(prev);
+133
View File
@@ -0,0 +1,133 @@
import { CheckAPIStatus } from "../../wailsjs/go/main/App";
import { CHECK_TIMEOUT_MS, withTimeout } from "@/lib/async-timeout";
export type ApiCheckStatus = "checking" | "online" | "offline" | "idle";
export interface ApiSource {
id: string;
type: string;
name: string;
url: string;
}
export const API_SOURCES: ApiSource[] = [
{ id: "tidal1", type: "tidal", name: "Tidal A", url: "https://hifi-one.spotisaver.net" },
{ id: "tidal2", type: "tidal", name: "Tidal B", url: "https://hifi-two.spotisaver.net" },
{ id: "tidal3", type: "tidal", name: "Tidal C", url: "https://eu-central.monochrome.tf" },
{ id: "tidal4", type: "tidal", name: "Tidal D", url: "https://us-west.monochrome.tf" },
{ id: "tidal5", type: "tidal", name: "Tidal E", url: "https://api.monochrome.tf" },
{ id: "tidal6", type: "tidal", name: "Tidal F", url: "https://monochrome-api.samidy.com" },
{ id: "tidal7", type: "tidal", name: "Tidal G", url: "https://tidal.kinoplus.online" },
{ id: "qobuz1", type: "qobuz", name: "Qobuz A", url: "https://dab.yeet.su" },
{ id: "qobuz2", type: "qobuz", name: "Qobuz B", url: "https://dabmusic.xyz" },
{ id: "qobuz3", type: "qbz", name: "Qobuz C", url: "https://qbz.afkarxyz.qzz.io" },
{ id: "amazon1", type: "amazon", name: "Amazon Music", url: "https://amzn.afkarxyz.qzz.io" },
];
type ApiStatusState = {
isCheckingAll: boolean;
statuses: Record<string, ApiCheckStatus>;
};
let apiStatusState: ApiStatusState = {
isCheckingAll: false,
statuses: {},
};
let activeCheckAll: Promise<void> | null = null;
const listeners = new Set<() => void>();
function emitApiStatusChange() {
for (const listener of listeners) {
listener();
}
}
function setApiStatusState(updater: (current: ApiStatusState) => ApiStatusState) {
apiStatusState = updater(apiStatusState);
emitApiStatusChange();
}
async function checkSingleApiStatus(source: ApiSource): Promise<void> {
setApiStatusState((current) => ({
...current,
statuses: {
...current.statuses,
[source.id]: "checking",
},
}));
try {
const isOnline = await withTimeout(
CheckAPIStatus(source.type, source.url),
CHECK_TIMEOUT_MS,
`API status check timed out after 10 seconds for ${source.url}`,
);
setApiStatusState((current) => ({
...current,
statuses: {
...current.statuses,
[source.id]: isOnline ? "online" : "offline",
},
}));
} catch {
setApiStatusState((current) => ({
...current,
statuses: {
...current.statuses,
[source.id]: "offline",
},
}));
}
}
export function getApiStatusState(): ApiStatusState {
return apiStatusState;
}
export function subscribeApiStatus(listener: () => void): () => void {
listeners.add(listener);
return () => {
listeners.delete(listener);
};
}
export function hasApiStatusResults(): boolean {
return API_SOURCES.some((source) => {
const status = apiStatusState.statuses[source.id];
return status === "online" || status === "offline";
});
}
export function ensureApiStatusCheckStarted(): void {
if (!activeCheckAll && !hasApiStatusResults()) {
void checkAllApiStatuses();
}
}
export async function checkAllApiStatuses(): Promise<void> {
if (activeCheckAll) {
return activeCheckAll;
}
activeCheckAll = (async () => {
setApiStatusState((current) => ({
...current,
isCheckingAll: true,
}));
try {
await Promise.allSettled(API_SOURCES.map((source) => checkSingleApiStatus(source)));
} finally {
setApiStatusState((current) => ({
...current,
isCheckingAll: false,
}));
activeCheckAll = null;
}
})();
return activeCheckAll;
}
+23
View File
@@ -0,0 +1,23 @@
export const CHECK_TIMEOUT_MS = 10 * 1000;
export function withTimeout<T>(
promise: Promise<T>,
timeoutMs: number = CHECK_TIMEOUT_MS,
message: string = `Operation timed out after ${Math.round(timeoutMs / 1000)} seconds`,
): Promise<T> {
return new Promise<T>((resolve, reject) => {
const timer = window.setTimeout(() => {
reject(new Error(message));
}, timeoutMs);
promise
.then((value) => {
window.clearTimeout(timer);
resolve(value);
})
.catch((error) => {
window.clearTimeout(timer);
reject(error);
});
});
}