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 <noreply@anthropic.com>
This commit is contained in:
@@ -52,6 +52,60 @@ async def search_match(
|
|||||||
return {"results": results}
|
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")
|
@router.get("/match/debug")
|
||||||
async def debug_match(
|
async def debug_match(
|
||||||
title: str,
|
title: str,
|
||||||
|
|||||||
@@ -30,6 +30,9 @@ export const triggerMatch = (id: string) =>
|
|||||||
export const debugMatch = (title: string, author?: string) =>
|
export const debugMatch = (title: string, author?: string) =>
|
||||||
api.get(`/api/items/match/debug`, { params: { title, author } }).then((r) => r.data)
|
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) =>
|
export const extractCover = (id: string) =>
|
||||||
api.post(`/api/items/${id}/extract-cover`).then((r) => r.data)
|
api.post(`/api/items/${id}/extract-cover`).then((r) => r.data)
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {
|
|||||||
Play, ArrowLeft, RefreshCw, Search, Check,
|
Play, ArrowLeft, RefreshCw, Search, Check,
|
||||||
Loader2, Trash2, X
|
Loader2, Trash2, X
|
||||||
} from 'lucide-react'
|
} 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 { getMe, createBookmark, deleteBookmark } from '../api/me'
|
||||||
import { usePlayerStore } from '../store/playerStore'
|
import { usePlayerStore } from '../store/playerStore'
|
||||||
import CoverImage from '../components/common/CoverImage'
|
import CoverImage from '../components/common/CoverImage'
|
||||||
@@ -23,6 +23,8 @@ export default function BookDetail() {
|
|||||||
const [showMatchPanel, setShowMatchPanel] = useState(false)
|
const [showMatchPanel, setShowMatchPanel] = useState(false)
|
||||||
const [debugData, setDebugData] = useState<any>(null)
|
const [debugData, setDebugData] = useState<any>(null)
|
||||||
const [debugLoading, setDebugLoading] = useState(false)
|
const [debugLoading, setDebugLoading] = useState(false)
|
||||||
|
const [connData, setConnData] = useState<any>(null)
|
||||||
|
const [connLoading, setConnLoading] = useState(false)
|
||||||
const { play, item: currentItem, currentTime } = usePlayerStore()
|
const { play, item: currentItem, currentTime } = usePlayerStore()
|
||||||
|
|
||||||
useEffect(() => {
|
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 () => {
|
const handleExtractCover = async () => {
|
||||||
if (!id) return
|
if (!id) return
|
||||||
const res = await extractCover(id)
|
const res = await extractCover(id)
|
||||||
@@ -206,10 +218,43 @@ export default function BookDetail() {
|
|||||||
>
|
>
|
||||||
Cover aus Datei
|
Cover aus Datei
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleConnectivity}
|
||||||
|
disabled={connLoading}
|
||||||
|
className="flex items-center gap-2 bg-card border border-divider px-4 py-2 rounded-lg text-sm text-muted hover:text-ink hover:bg-card/80 disabled:opacity-50 transition-colors"
|
||||||
|
title="Prüft ob Backend die externen Metadaten-APIs erreicht"
|
||||||
|
>
|
||||||
|
{connLoading ? <Loader2 size={14} className="animate-spin" /> : null}
|
||||||
|
Connectivity-Check
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{connData && (
|
||||||
|
<div className="mb-6 bg-surface border border-divider rounded-xl p-4">
|
||||||
|
<div className="flex items-center justify-between mb-3">
|
||||||
|
<h3 className="text-ink" style={{ fontSize: '13px', fontWeight: 600 }}>Externe Verbindungen</h3>
|
||||||
|
<button onClick={() => setConnData(null)} className="text-muted hover:text-ink">
|
||||||
|
<X size={14} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{connData.proxy_env && connData.proxy_env !== 'keine' && (
|
||||||
|
<p className="text-yellow-400 mb-2" style={{ fontSize: '11px' }}>
|
||||||
|
Proxy-Variablen aktiv: <span className="font-mono">{JSON.stringify(connData.proxy_env)}</span>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{connData.results?.map((r: any) => (
|
||||||
|
<div key={r.name} className="flex items-center justify-between py-1.5 border-t border-divider" style={{ fontSize: '12px' }}>
|
||||||
|
<span className="text-ink font-medium">{r.name}</span>
|
||||||
|
<span className={r.ok && r.status === 200 ? 'text-primary' : 'text-red-400'} style={{ fontSize: '11px' }}>
|
||||||
|
{r.ok ? `HTTP ${r.status} · ${r.bytes}B · ${r.ms}ms` : `${r.error} (${r.ms}ms)`}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{debugData && (
|
{debugData && (
|
||||||
<div className="mb-6 bg-surface border border-divider rounded-xl p-4">
|
<div className="mb-6 bg-surface border border-divider rounded-xl p-4">
|
||||||
<div className="flex items-center justify-between mb-3">
|
<div className="flex items-center justify-between mb-3">
|
||||||
|
|||||||
Reference in New Issue
Block a user