.refine check status
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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>)}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user