import json import time from adapters.hdhive_adapter import normalize_resource, search_resource from adapters.tmdb_adapter import get_media_detail, search_media from error_handling import AppServiceError, normalize_exception from storage import find_media_item RESOURCE_CACHE_TTL_SECONDS = 15 * 60 _RESOURCE_CACHE = {} def _extract_hdhive_items(hdhive_result): payload = (hdhive_result or {}).get("data") # HDHive openapi uses envelope: { success, data, ... } if isinstance(payload, dict) and isinstance(payload.get("data"), list): return payload.get("data") or [] if isinstance(payload, list): return payload if isinstance(payload, dict): return payload.get("items") or [] return [] def _resource_cache_key(media_type, tmdb_id): return f"{media_type}:{tmdb_id}" def _get_cached_resources(media_type, tmdb_id): key = _resource_cache_key(media_type, tmdb_id) cached = _RESOURCE_CACHE.get(key) if not cached: return None if cached.get("expireAt", 0) < time.time(): _RESOURCE_CACHE.pop(key, None) return None return cached.get("resources") or [] def _set_cached_resources(media_type, tmdb_id, resources): key = _resource_cache_key(media_type, tmdb_id) _RESOURCE_CACHE[key] = { "expireAt": time.time() + RESOURCE_CACHE_TTL_SECONDS, "resources": resources or [], } def _extract_hdhive_raw_items(hdhive_raw): payload = hdhive_raw if isinstance(payload, str): try: payload = json.loads(payload) except Exception: return [] if isinstance(payload, dict) and isinstance(payload.get("data"), list): return payload.get("data") or [] if isinstance(payload, dict) and isinstance(payload.get("items"), list): return payload.get("items") or [] if isinstance(payload, list): return payload return [] def _fallback_resources_from_db(tmdb_id, media_type): item = find_media_item(tmdb_id) if not item: return [] if item.get("type") != media_type: return [] raw_items = _extract_hdhive_raw_items(item.get("hdhive_raw")) resources = [] for raw_item in raw_items: normalized = normalize_resource(raw_item, {}) normalized["unlockError"] = "当前 HDHive 接口限流,已回退展示缓存资源(未实时解锁)" resources.append(normalized) return resources def search_media_by_keyword(query, media_type): result = search_media(query, media_type) raw_items = result.get("items") or [] items = [] for item in raw_items: items.append( { "id": item.get("id"), "type": media_type, "title": item.get("title") or item.get("name"), "overview": item.get("overview") or "", "posterPath": item.get("poster_path") or "", "releaseDate": item.get("release_date") or item.get("first_air_date") or "", "voteAverage": item.get("vote_average"), } ) return {"items": items} def get_media_resources(media_type, tmdb_id): detail = get_media_detail(tmdb_id, media_type) cached_resources = _get_cached_resources(media_type, tmdb_id) if cached_resources: return { "media": detail.get("normalized"), "resources": cached_resources, "resourceNotice": "命中本地缓存,已减少上游请求压力", } try: hdhive = search_resource(media_type, tmdb_id) except Exception as error: normalized_error = normalize_exception(error) if normalized_error.provider == "hdhive" and normalized_error.category == "rate_limit": fallback_resources = _fallback_resources_from_db(tmdb_id, media_type) if fallback_resources: _set_cached_resources(media_type, tmdb_id, fallback_resources) return { "media": detail.get("normalized"), "resources": fallback_resources, "resourceNotice": "HDHive 当前限流,已回退到历史缓存资源", } raise search_data = _extract_hdhive_items(hdhive) resources = [] for item in search_data: normalized = normalize_resource(item, {}) normalized["unlockError"] = "详情页不自动解锁,点击“使用该资源入库”时才会请求解锁并扣积分" resources.append(normalized) _set_cached_resources(media_type, tmdb_id, resources) return { "media": detail.get("normalized"), "resources": resources, } def validate_media_query(query, media_type): if not query: raise AppServiceError( "query is required", category="validation", code="INVALID_INPUT", status=400, provider="api", ) validate_media_type(media_type) def validate_media_type(media_type): if media_type not in ("movie", "tv"): raise AppServiceError( "type must be movie or tv", category="validation", code="INVALID_INPUT", status=400, provider="api", )