.refine check status
This commit is contained in:
@@ -23,10 +23,34 @@ type App struct {
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
const checkOperationTimeout = 10 * time.Second
|
||||
|
||||
func NewApp() *App {
|
||||
return &App{}
|
||||
}
|
||||
|
||||
type timedResult[T any] struct {
|
||||
value T
|
||||
err error
|
||||
}
|
||||
|
||||
func runWithTimeout[T any](timeout time.Duration, fn func() (T, error)) (T, error) {
|
||||
resultCh := make(chan timedResult[T], 1)
|
||||
|
||||
go func() {
|
||||
value, err := fn()
|
||||
resultCh <- timedResult[T]{value: value, err: err}
|
||||
}()
|
||||
|
||||
select {
|
||||
case result := <-resultCh:
|
||||
return result.value, result.err
|
||||
case <-time.After(timeout):
|
||||
var zero T
|
||||
return zero, fmt.Errorf("operation timed out after %s", timeout)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) getFirstArtist(artistString string) string {
|
||||
if artistString == "" {
|
||||
return ""
|
||||
@@ -757,6 +781,7 @@ func (a *App) ExportFailedDownloads() (string, error) {
|
||||
}
|
||||
|
||||
func (a *App) CheckAPIStatus(apiType string, apiURL string) bool {
|
||||
isOnline, err := runWithTimeout(checkOperationTimeout, func() (bool, error) {
|
||||
var checkURL string
|
||||
if apiType == "tidal" {
|
||||
checkURL = fmt.Sprintf("%s/track/?id=441821360&quality=HI_RES_LOSSLESS", apiURL)
|
||||
@@ -770,10 +795,10 @@ func (a *App) CheckAPIStatus(apiType string, apiURL string) bool {
|
||||
checkURL = apiURL
|
||||
}
|
||||
|
||||
client := &http.Client{Timeout: 15 * time.Second}
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
req, err := http.NewRequest("GET", checkURL, nil)
|
||||
if err != nil {
|
||||
return false
|
||||
return false, err
|
||||
}
|
||||
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36")
|
||||
|
||||
@@ -786,12 +811,12 @@ func (a *App) CheckAPIStatus(apiType string, apiURL string) bool {
|
||||
body, readErr := io.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
if readErr == nil && strings.Contains(string(body), `"amazonMusic":"up"`) {
|
||||
return true
|
||||
return true, nil
|
||||
}
|
||||
} else {
|
||||
resp.Body.Close()
|
||||
if statusCode == 200 {
|
||||
return true
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -799,9 +824,16 @@ func (a *App) CheckAPIStatus(apiType string, apiURL string) bool {
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Printf("CheckAPIStatus timeout/error for %s (%s): %v\n", apiType, apiURL, err)
|
||||
return false
|
||||
}
|
||||
|
||||
return isOnline
|
||||
}
|
||||
|
||||
func (a *App) Quit() {
|
||||
|
||||
panic("quit")
|
||||
@@ -1085,6 +1117,7 @@ func (a *App) CheckTrackAvailability(spotifyTrackID string) (string, error) {
|
||||
return "", fmt.Errorf("spotify track ID is required")
|
||||
}
|
||||
|
||||
return runWithTimeout(checkOperationTimeout, func() (string, error) {
|
||||
client := backend.NewSongLinkClient()
|
||||
availability, err := client.CheckTrackAvailability(spotifyTrackID)
|
||||
if err != nil {
|
||||
@@ -1097,6 +1130,7 @@ func (a *App) CheckTrackAvailability(spotifyTrackID string) (string, error) {
|
||||
}
|
||||
|
||||
return string(jsonData), nil
|
||||
})
|
||||
}
|
||||
|
||||
func (a *App) IsFFmpegInstalled() (bool, error) {
|
||||
|
||||
@@ -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