diff --git a/app.go b/app.go index 179548d..f12ca60 100644 --- a/app.go +++ b/app.go @@ -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,49 +781,57 @@ func (a *App) ExportFailedDownloads() (string, error) { } func (a *App) CheckAPIStatus(apiType string, apiURL string) bool { - var checkURL string - if apiType == "tidal" { - checkURL = fmt.Sprintf("%s/track/?id=441821360&quality=HI_RES_LOSSLESS", apiURL) - } else if apiType == "qobuz" { - checkURL = fmt.Sprintf("%s/api/stream?trackId=360735657&format_id=27", apiURL) - } else if apiType == "qbz" { - checkURL = fmt.Sprintf("%s/api/track/360735657?quality=27", apiURL) - } else if apiType == "amazon" { - checkURL = fmt.Sprintf("%s/status", apiURL) - } else { - checkURL = apiURL - } + 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) + } else if apiType == "qobuz" { + checkURL = fmt.Sprintf("%s/api/stream?trackId=360735657&format_id=27", apiURL) + } else if apiType == "qbz" { + checkURL = fmt.Sprintf("%s/api/track/360735657?quality=27", apiURL) + } else if apiType == "amazon" { + checkURL = fmt.Sprintf("%s/status", apiURL) + } else { + checkURL = apiURL + } - client := &http.Client{Timeout: 15 * time.Second} - req, err := http.NewRequest("GET", checkURL, nil) - if err != nil { - return false - } - 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") + client := &http.Client{Timeout: 10 * time.Second} + req, err := http.NewRequest("GET", checkURL, nil) + if err != nil { + 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") - maxRetries := 3 - for i := 0; i < maxRetries; i++ { - resp, err := client.Do(req) - if err == nil { - statusCode := resp.StatusCode - if apiType == "amazon" && statusCode == 200 { - body, readErr := io.ReadAll(resp.Body) - resp.Body.Close() - if readErr == nil && strings.Contains(string(body), `"amazonMusic":"up"`) { - return true - } - } else { - resp.Body.Close() - if statusCode == 200 { - return true + maxRetries := 3 + for i := 0; i < maxRetries; i++ { + resp, err := client.Do(req) + if err == nil { + statusCode := resp.StatusCode + if apiType == "amazon" && statusCode == 200 { + body, readErr := io.ReadAll(resp.Body) + resp.Body.Close() + if readErr == nil && strings.Contains(string(body), `"amazonMusic":"up"`) { + return true, nil + } + } else { + resp.Body.Close() + if statusCode == 200 { + return true, nil + } } } + if i < maxRetries-1 { + time.Sleep(1 * time.Second) + } } - if i < maxRetries-1 { - 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 false + + return isOnline } func (a *App) Quit() { @@ -1085,18 +1117,20 @@ func (a *App) CheckTrackAvailability(spotifyTrackID string) (string, error) { return "", fmt.Errorf("spotify track ID is required") } - client := backend.NewSongLinkClient() - availability, err := client.CheckTrackAvailability(spotifyTrackID) - if err != nil { - return "", err - } + return runWithTimeout(checkOperationTimeout, func() (string, error) { + client := backend.NewSongLinkClient() + availability, err := client.CheckTrackAvailability(spotifyTrackID) + if err != nil { + return "", err + } - jsonData, err := json.Marshal(availability) - if err != nil { - return "", fmt.Errorf("failed to encode response: %v", err) - } + jsonData, err := json.Marshal(availability) + if err != nil { + return "", fmt.Errorf("failed to encode response: %v", err) + } - return string(jsonData), nil + return string(jsonData), nil + }) } func (a *App) IsFFmpegInstalled() (bool, error) { diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 104fb33..bff17ac 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 { 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); diff --git a/frontend/src/components/AboutPage.tsx b/frontend/src/components/AboutPage.tsx index bb374eb..dc11811 100644 --- a/frontend/src/components/AboutPage.tsx +++ b/frontend/src/components/AboutPage.tsx @@ -273,8 +273,7 @@ export function AboutPage() { Note
- 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.
)} diff --git a/frontend/src/components/ApiStatusTab.tsx b/frontend/src/components/ApiStatusTab.tsx index 707a0d9..902f939 100644 --- a/frontend/src/components/ApiStatusTab.tsx +++ b/frontend/src/components/ApiStatusTab.tsx @@ -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