From 4fccb7abae2cfc966331a815cdcfee74b7f9fc57 Mon Sep 17 00:00:00 2001 From: Audiolib Date: Tue, 26 May 2026 19:30:07 +0200 Subject: [PATCH] Add connectivity check to diagnose why all metadata APIs return 0 hits All four sources returning 0 results is a strong signal of a network problem (DNS, firewall, proxy, Docker isolation) rather than four independent matching bugs. New endpoint GET /api/items/match/connectivity: - Pings Google, MusicBrainz, OpenLibrary, GoogleBooks, DNB - Returns per-target HTTP status, byte count, latency, error - Surfaces any HTTP_PROXY / HTTPS_PROXY env vars that httpx would use UI: New "Connectivity-Check" button in BookDetail. Result panel shows each target green (HTTP 200) or red (error/timeout), so the user can immediately see whether the backend has outbound internet access at all, or whether it's a per-API issue. Co-Authored-By: Claude Sonnet 4.6 --- backend/app/routers/matching.py | 54 +++++++++++++++++++++++++++++++ frontend/src/api/items.ts | 3 ++ frontend/src/pages/BookDetail.tsx | 47 ++++++++++++++++++++++++++- 3 files changed, 103 insertions(+), 1 deletion(-) diff --git a/backend/app/routers/matching.py b/backend/app/routers/matching.py index a81cc24..f7dbc92 100644 --- a/backend/app/routers/matching.py +++ b/backend/app/routers/matching.py @@ -52,6 +52,60 @@ async def search_match( return {"results": results} +@router.get("/match/connectivity") +async def check_connectivity( + current_user: User = Depends(get_current_user), +): + """Testet ob das Backend die externen Metadaten-APIs erreichen kann.""" + import httpx + import time + + targets = [ + ("Google", "https://www.google.com"), + ("MusicBrainz", "https://musicbrainz.org/ws/2/release?query=test&fmt=json&limit=1"), + ("OpenLibrary", "https://openlibrary.org/search.json?title=test&limit=1"), + ("GoogleBooks", "https://www.googleapis.com/books/v1/volumes?q=test&maxResults=1"), + ("DNB", "https://services.dnb.de/sru/dnb?version=1.1&operation=searchRetrieve&query=tit%3Dtest&maximumRecords=1"), + ] + headers = {"User-Agent": "audiolib/1.0"} + + results = [] + async with httpx.AsyncClient(headers=headers, timeout=15, follow_redirects=True) as client: + for name, url in targets: + t0 = time.time() + try: + r = await client.get(url) + results.append({ + "name": name, + "url": url, + "ok": True, + "status": r.status_code, + "bytes": len(r.content), + "ms": int((time.time() - t0) * 1000), + "body_snippet": (r.text[:150] if r.status_code != 200 else None), + }) + except Exception as e: + results.append({ + "name": name, + "url": url, + "ok": False, + "error": f"{type(e).__name__}: {e}", + "ms": int((time.time() - t0) * 1000), + }) + + # Auch Env-Variablen die httpx beeinflussen + import os + proxy_env = { + k: v for k, v in os.environ.items() + if k.upper() in ("HTTP_PROXY", "HTTPS_PROXY", "NO_PROXY", "ALL_PROXY") + } + + return { + "results": results, + "proxy_env": proxy_env or "keine", + } + + @router.get("/match/debug") async def debug_match( title: str, diff --git a/frontend/src/api/items.ts b/frontend/src/api/items.ts index 0613b12..88a46cc 100644 --- a/frontend/src/api/items.ts +++ b/frontend/src/api/items.ts @@ -30,6 +30,9 @@ export const triggerMatch = (id: string) => export const debugMatch = (title: string, author?: string) => api.get(`/api/items/match/debug`, { params: { title, author } }).then((r) => r.data) +export const checkConnectivity = () => + api.get(`/api/items/match/connectivity`).then((r) => r.data) + export const extractCover = (id: string) => api.post(`/api/items/${id}/extract-cover`).then((r) => r.data) diff --git a/frontend/src/pages/BookDetail.tsx b/frontend/src/pages/BookDetail.tsx index e94c7bc..92d70e8 100644 --- a/frontend/src/pages/BookDetail.tsx +++ b/frontend/src/pages/BookDetail.tsx @@ -4,7 +4,7 @@ import { Play, ArrowLeft, RefreshCw, Search, Check, Loader2, Trash2, X } from 'lucide-react' -import { getItem, updateItem, triggerMatch, searchMatch, applyMatch, debugMatch, extractCover, coverUrl } from '../api/items' +import { getItem, updateItem, triggerMatch, searchMatch, applyMatch, debugMatch, checkConnectivity, extractCover, coverUrl } from '../api/items' import { getMe, createBookmark, deleteBookmark } from '../api/me' import { usePlayerStore } from '../store/playerStore' import CoverImage from '../components/common/CoverImage' @@ -23,6 +23,8 @@ export default function BookDetail() { const [showMatchPanel, setShowMatchPanel] = useState(false) const [debugData, setDebugData] = useState(null) const [debugLoading, setDebugLoading] = useState(false) + const [connData, setConnData] = useState(null) + const [connLoading, setConnLoading] = useState(false) const { play, item: currentItem, currentTime } = usePlayerStore() useEffect(() => { @@ -91,6 +93,16 @@ export default function BookDetail() { } } + const handleConnectivity = async () => { + setConnLoading(true) + try { + const data = await checkConnectivity() + setConnData(data) + } finally { + setConnLoading(false) + } + } + const handleExtractCover = async () => { if (!id) return const res = await extractCover(id) @@ -206,10 +218,43 @@ export default function BookDetail() { > Cover aus Datei + + {connData && ( +
+
+

Externe Verbindungen

+ +
+ {connData.proxy_env && connData.proxy_env !== 'keine' && ( +

+ Proxy-Variablen aktiv: {JSON.stringify(connData.proxy_env)} +

+ )} + {connData.results?.map((r: any) => ( +
+ {r.name} + + {r.ok ? `HTTP ${r.status} · ${r.bytes}B · ${r.ms}ms` : `${r.error} (${r.ms}ms)`} + +
+ ))} +
+ )} + {debugData && (