Fix cache title hydration
This commit is contained in:
@@ -1 +1 @@
|
|||||||
2501262041
|
2601261358
|
||||||
|
|||||||
@@ -76,6 +76,40 @@ def _extract_title_year_from_payload(payload_json: Optional[str]) -> tuple[Optio
|
|||||||
return _normalize_title_value(title), _normalize_year_value(year)
|
return _normalize_title_value(title), _normalize_year_value(year)
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_tmdb_from_payload(payload_json: Optional[str]) -> tuple[Optional[int], Optional[str]]:
|
||||||
|
if not payload_json:
|
||||||
|
return None, None
|
||||||
|
try:
|
||||||
|
payload = json.loads(payload_json)
|
||||||
|
except (TypeError, json.JSONDecodeError):
|
||||||
|
return None, None
|
||||||
|
if not isinstance(payload, dict):
|
||||||
|
return None, None
|
||||||
|
media = payload.get("media") or {}
|
||||||
|
if not isinstance(media, dict):
|
||||||
|
media = {}
|
||||||
|
tmdb_id = (
|
||||||
|
media.get("tmdbId")
|
||||||
|
or payload.get("tmdbId")
|
||||||
|
or payload.get("tmdb_id")
|
||||||
|
or media.get("externalServiceId")
|
||||||
|
or payload.get("externalServiceId")
|
||||||
|
)
|
||||||
|
media_type = (
|
||||||
|
media.get("mediaType")
|
||||||
|
or payload.get("mediaType")
|
||||||
|
or payload.get("media_type")
|
||||||
|
or payload.get("type")
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
tmdb_id = int(tmdb_id) if tmdb_id is not None else None
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
tmdb_id = None
|
||||||
|
if isinstance(media_type, str):
|
||||||
|
media_type = media_type.strip().lower() or None
|
||||||
|
return tmdb_id, media_type
|
||||||
|
|
||||||
|
|
||||||
def init_db() -> None:
|
def init_db() -> None:
|
||||||
with _connect() as conn:
|
with _connect() as conn:
|
||||||
conn.execute(
|
conn.execute(
|
||||||
@@ -874,6 +908,34 @@ def get_request_cache_overview(limit: int = 50) -> list[Dict[str, Any]]:
|
|||||||
return results
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
def get_request_cache_missing_titles(limit: int = 200) -> list[Dict[str, Any]]:
|
||||||
|
limit = max(1, min(limit, 500))
|
||||||
|
with _connect() as conn:
|
||||||
|
rows = conn.execute(
|
||||||
|
"""
|
||||||
|
SELECT request_id, payload_json
|
||||||
|
FROM requests_cache
|
||||||
|
WHERE title IS NULL OR TRIM(title) = '' OR LOWER(title) = 'untitled'
|
||||||
|
ORDER BY updated_at DESC, request_id DESC
|
||||||
|
LIMIT ?
|
||||||
|
""",
|
||||||
|
(limit,),
|
||||||
|
).fetchall()
|
||||||
|
results: list[Dict[str, Any]] = []
|
||||||
|
for row in rows:
|
||||||
|
payload_json = row[1]
|
||||||
|
tmdb_id, media_type = _extract_tmdb_from_payload(payload_json)
|
||||||
|
results.append(
|
||||||
|
{
|
||||||
|
"request_id": row[0],
|
||||||
|
"payload_json": payload_json,
|
||||||
|
"tmdb_id": tmdb_id,
|
||||||
|
"media_type": media_type,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
def get_request_cache_count() -> int:
|
def get_request_cache_count() -> int:
|
||||||
with _connect() as conn:
|
with _connect() as conn:
|
||||||
row = conn.execute("SELECT COUNT(*) FROM requests_cache").fetchone()
|
row = conn.execute("SELECT COUNT(*) FROM requests_cache").fetchone()
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ from ..db import (
|
|||||||
delete_setting,
|
delete_setting,
|
||||||
get_all_users,
|
get_all_users,
|
||||||
get_request_cache_overview,
|
get_request_cache_overview,
|
||||||
|
get_request_cache_missing_titles,
|
||||||
get_settings_overrides,
|
get_settings_overrides,
|
||||||
get_user_by_username,
|
get_user_by_username,
|
||||||
set_setting,
|
set_setting,
|
||||||
@@ -100,6 +101,38 @@ def _normalize_root_folders(folders: Any) -> List[Dict[str, Any]]:
|
|||||||
return results
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
async def _hydrate_cache_titles_from_jellyseerr(limit: int) -> int:
|
||||||
|
runtime = get_runtime_settings()
|
||||||
|
client = JellyseerrClient(runtime.jellyseerr_base_url, runtime.jellyseerr_api_key)
|
||||||
|
if not client.configured():
|
||||||
|
return 0
|
||||||
|
missing = get_request_cache_missing_titles(limit)
|
||||||
|
if not missing:
|
||||||
|
return 0
|
||||||
|
hydrated = 0
|
||||||
|
for row in missing:
|
||||||
|
tmdb_id = row.get("tmdb_id")
|
||||||
|
media_type = row.get("media_type")
|
||||||
|
request_id = row.get("request_id")
|
||||||
|
if not tmdb_id or not media_type or not request_id:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
title, year = await requests_router._hydrate_title_from_tmdb(
|
||||||
|
client, media_type, tmdb_id
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
logger.warning(
|
||||||
|
"Requests cache title hydrate failed: request_id=%s tmdb_id=%s",
|
||||||
|
request_id,
|
||||||
|
tmdb_id,
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
if title:
|
||||||
|
update_request_cache_title(request_id, title, year)
|
||||||
|
hydrated += 1
|
||||||
|
return hydrated
|
||||||
|
|
||||||
|
|
||||||
def _normalize_quality_profiles(profiles: Any) -> List[Dict[str, Any]]:
|
def _normalize_quality_profiles(profiles: Any) -> List[Dict[str, Any]]:
|
||||||
if not isinstance(profiles, list):
|
if not isinstance(profiles, list):
|
||||||
return []
|
return []
|
||||||
@@ -286,6 +319,9 @@ async def requests_cache(limit: int = 50) -> Dict[str, Any]:
|
|||||||
repaired = repair_request_cache_titles()
|
repaired = repair_request_cache_titles()
|
||||||
if repaired:
|
if repaired:
|
||||||
logger.info("Requests cache titles repaired via settings view: %s", repaired)
|
logger.info("Requests cache titles repaired via settings view: %s", repaired)
|
||||||
|
hydrated = await _hydrate_cache_titles_from_jellyseerr(limit)
|
||||||
|
if hydrated:
|
||||||
|
logger.info("Requests cache titles hydrated via Jellyseerr: %s", hydrated)
|
||||||
rows = get_request_cache_overview(limit)
|
rows = get_request_cache_overview(limit)
|
||||||
return {"rows": rows}
|
return {"rows": rows}
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ export default function UsersPage() {
|
|||||||
const [users, setUsers] = useState<AdminUser[]>([])
|
const [users, setUsers] = useState<AdminUser[]>([])
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [jellyfinSyncStatus, setJellyfinSyncStatus] = useState<string | null>(null)
|
||||||
|
const [jellyfinSyncBusy, setJellyfinSyncBusy] = useState(false)
|
||||||
|
|
||||||
const loadUsers = async () => {
|
const loadUsers = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -103,6 +105,28 @@ export default function UsersPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const syncJellyfinUsers = async () => {
|
||||||
|
setJellyfinSyncStatus(null)
|
||||||
|
setJellyfinSyncBusy(true)
|
||||||
|
try {
|
||||||
|
const baseUrl = getApiBase()
|
||||||
|
const response = await authFetch(`${baseUrl}/admin/jellyfin/users/sync`, {
|
||||||
|
method: 'POST',
|
||||||
|
})
|
||||||
|
if (!response.ok) {
|
||||||
|
const text = await response.text()
|
||||||
|
throw new Error(text || 'Sync failed')
|
||||||
|
}
|
||||||
|
const data = await response.json()
|
||||||
|
setJellyfinSyncStatus(`Synced ${data?.imported ?? 0} Jellyfin users.`)
|
||||||
|
await loadUsers()
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
setJellyfinSyncStatus('Could not sync Jellyfin users.')
|
||||||
|
} finally {
|
||||||
|
setJellyfinSyncBusy(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!getToken()) {
|
if (!getToken()) {
|
||||||
@@ -121,13 +145,19 @@ export default function UsersPage() {
|
|||||||
title="Users"
|
title="Users"
|
||||||
subtitle="Manage who can use Magent."
|
subtitle="Manage who can use Magent."
|
||||||
actions={
|
actions={
|
||||||
|
<>
|
||||||
<button type="button" onClick={loadUsers}>
|
<button type="button" onClick={loadUsers}>
|
||||||
Reload list
|
Reload list
|
||||||
</button>
|
</button>
|
||||||
|
<button type="button" onClick={syncJellyfinUsers} disabled={jellyfinSyncBusy}>
|
||||||
|
{jellyfinSyncBusy ? 'Syncing Jellyfin users...' : 'Sync Jellyfin users'}
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<section className="admin-section">
|
<section className="admin-section">
|
||||||
{error && <div className="error-banner">{error}</div>}
|
{error && <div className="error-banner">{error}</div>}
|
||||||
|
{jellyfinSyncStatus && <div className="status-banner">{jellyfinSyncStatus}</div>}
|
||||||
{users.length === 0 ? (
|
{users.length === 0 ? (
|
||||||
<div className="status-banner">No users found yet.</div>
|
<div className="status-banner">No users found yet.</div>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
Reference in New Issue
Block a user