Spaces:
Running
on
CPU Upgrade
Running
on
CPU Upgrade
| import asyncio | |
| import json | |
| import logging | |
| from datetime import datetime, timedelta, timezone | |
| from typing import List, Optional | |
| import dropbox | |
| from modules.dropbox.client import dbx | |
| from fastapi import HTTPException | |
| # Logger | |
| logger = logging.getLogger(__name__) | |
| logger.setLevel(logging.INFO) | |
| # Cache: key = folder_path, value = {"timestamp": datetime, "data": List[dict]} | |
| _audible_cache: dict[str, dict] = {} | |
| CACHE_TTL = timedelta(hours=1) | |
| FOLDER_PATH = "/_audibles" | |
| async def fetch_audibles_from_dropbox() -> List[dict]: | |
| """ | |
| Fetch all audible JSONs for a scripture from Dropbox with caching. | |
| Expects files in "/_audibles/". | |
| """ | |
| loop = asyncio.get_running_loop() | |
| folder_path = FOLDER_PATH | |
| # Check cache | |
| cache_entry = _audible_cache.get(folder_path) | |
| if cache_entry: | |
| age = datetime.now() - cache_entry["timestamp"] | |
| if age < CACHE_TTL: | |
| logger.info(f"Using cached audibles for '{folder_path}' (age={age})") | |
| return cache_entry["data"] | |
| logger.info(f"Fetching audibles from Dropbox folder '{folder_path}'") | |
| audibles: List[dict] = [] | |
| try: | |
| # List folder contents (synchronously in executor) | |
| res = await loop.run_in_executor(None, dbx.files_list_folder, folder_path) | |
| for entry in res.entries: | |
| if isinstance(entry, dropbox.files.FileMetadata) and entry.name.lower().endswith(".json"): | |
| metadata, fres = await loop.run_in_executor( | |
| None, dbx.files_download, f"{folder_path}/{entry.name}" | |
| ) | |
| data = fres.content.decode("utf-8") | |
| audibles.append(json.loads(data)) | |
| # Update cache | |
| _audible_cache[folder_path] = {"timestamp": datetime.now(), "data": audibles} | |
| logger.info(f"Cached {len(audibles)} audibles for '{folder_path}'") | |
| return audibles | |
| except Exception as e: | |
| logger.error(f"Error fetching audibles from '{folder_path}'", exc_info=e) | |
| # fallback to cached data if available | |
| if cache_entry: | |
| logger.warning(f"Returning stale cached audibles for '{folder_path}'") | |
| return cache_entry["data"] | |
| else: | |
| logger.warning(f"No cached audibles available for '{folder_path}'") | |
| return [] | |
| async def get_audible_summaries(page: int = 1, per_page: int = 10): | |
| """ | |
| Returns paginated summaries: id, topic_name, artwork_url. | |
| Sorted by topic_name. | |
| """ | |
| all_audibles = await fetch_audibles_from_dropbox() | |
| # Build summaries | |
| summaries = [ | |
| { | |
| "id": d.get("id"), | |
| "topic_name": d.get("topic_name"), | |
| "artwork_url": d.get("artwork_url"), | |
| } | |
| for d in all_audibles | |
| ] | |
| summaries.sort(key=lambda x: (x.get("topic_name") or "").lower()) | |
| # Pagination | |
| total_items = len(summaries) | |
| total_pages = (total_items + per_page - 1) // per_page | |
| if page < 1 or page > total_pages: | |
| logger.warning(f"Invalid page {page}. Must be between 1 and {total_pages}") | |
| return {"page": page, "per_page": per_page, "total_pages": total_pages, "total_items": total_items, "data": []} | |
| start = (page - 1) * per_page | |
| end = start + per_page | |
| paginated = summaries[start:end] | |
| print("audible data = ",paginated) | |
| return { | |
| "page": page, | |
| "per_page": per_page, | |
| "total_pages": total_pages, | |
| "total_items": total_items, | |
| "data": paginated, | |
| } | |
| async def get_audible_by_id(topic_id: int) -> Optional[dict]: | |
| """ | |
| Fetch a single audible JSON by topic_id from Dropbox. | |
| Uses in-memory caching per file. | |
| """ | |
| loop = asyncio.get_running_loop() | |
| file_path = f"{FOLDER_PATH}/{topic_id}.json" | |
| # Check cache | |
| cache_entry = _audible_cache.get(file_path) | |
| if cache_entry: | |
| age = datetime.now() - cache_entry["timestamp"] | |
| if age < CACHE_TTL: | |
| logger.info(f"Using cached audible for topic {topic_id} (age={age})") | |
| return cache_entry["data"] | |
| try: | |
| logger.info(f"Fetching audible {topic_id} from Dropbox: {file_path}") | |
| metadata, res = await loop.run_in_executor(None, dbx.files_download, file_path) | |
| data = res.content.decode("utf-8") | |
| audible = json.loads(data) | |
| # Update cache | |
| _audible_cache[file_path] = {"timestamp": datetime.now(), "data": audible} | |
| return audible | |
| except dropbox.exceptions.HttpError as e: | |
| logger.error(f"Dropbox file not found: {file_path}", exc_info=e) | |
| return None | |
| except Exception as e: | |
| logger.error(f"Error fetching audible {topic_id}", exc_info=e) | |
| # fallback to cached data if available | |
| if cache_entry: | |
| logger.warning(f"Returning stale cached audible for topic {topic_id}") | |
| return cache_entry["data"] | |
| return None | |
| # cache = { audible_path: {"url": ..., "expiry": ...} } | |
| audible_audio_cache: dict[str, dict] = {} | |
| AUDIBLE_CACHE_TTL = timedelta(hours=3, minutes=30) | |
| async def get_audible_audio_url(audio_path: str): | |
| """ | |
| Returns a temporary Dropbox download URL for an audible audio file. | |
| Uses in-memory caching to avoid regenerating links too frequently. | |
| """ | |
| if not audio_path: | |
| raise HTTPException(status_code=400, detail="audio_path is required") | |
| # Normalize path (ensure leading slash) | |
| dropbox_path = ( | |
| audio_path if audio_path.startswith("/") else f"/{audio_path}" | |
| ) | |
| now = datetime.now(timezone.utc) | |
| # 1️⃣ Check cache | |
| cached = audible_audio_cache.get(dropbox_path) | |
| if cached and cached["expiry"] > now: | |
| return {"audio_url": cached["url"]} | |
| # 2️⃣ Generate fresh Dropbox temp link | |
| try: | |
| temp_link = dbx.files_get_temporary_link(dropbox_path).link | |
| except dropbox.exceptions.ApiError: | |
| raise HTTPException(status_code=404, detail="Audible audio not found") | |
| # 3️⃣ Cache it | |
| audible_audio_cache[dropbox_path] = { | |
| "url": temp_link, | |
| "expiry": now + AUDIBLE_CACHE_TTL, | |
| } | |
| return {"audio_url": temp_link} | |
| async def cleanup_audible_audio_cache(interval_seconds: int = 600): | |
| while True: | |
| now = datetime.now(timezone.utc) | |
| expired = [ | |
| k for k, v in audible_audio_cache.items() | |
| if v["expiry"] <= now | |
| ] | |
| for k in expired: | |
| del audible_audio_cache[k] | |
| await asyncio.sleep(interval_seconds) | |