vikramvasudevan commited on
Commit
338ea9e
·
verified ·
1 Parent(s): b72660d

Upload folder using huggingface_hub

Browse files
modules/audio/service.py CHANGED
@@ -1,9 +1,13 @@
 
1
  from modules.audio.model import AudioRequest, AudioType
 
 
2
  from modules.dropbox.audio import get_audio_urls, get_global_indices_with_audio
3
  from config import SanatanConfig
4
  from db import SanatanDatabase
5
  from typing import List
6
 
 
7
  async def svc_get_audio_urls(req: AudioRequest):
8
  config = SanatanConfig().get_scripture_by_name(req.scripture_name)
9
  audio_storage = config.get("audio_storage", "dropbox")
@@ -14,23 +18,28 @@ async def svc_get_audio_urls(req: AudioRequest):
14
  data = db.fetch_document_by_index(
15
  collection_name=config["collection_name"], index=req.global_index
16
  )
17
- url = data.get("audio", data.get("audio_url", "" ))
18
  ## Temporary fix for bhagavat gita audio being moved in the source.
19
- if(req.scripture_name == "bhagavat_gita"):
20
- url = url.replace("https://cdn.vivekavani.com/wp-content/","https://vivekavani.com/wp-content/")
 
 
 
21
  urls = {"recitation": url}
22
  return urls
23
 
24
 
25
- async def svc_get_indices_with_audio(scripture_name: str, audio_type: AudioType) -> List[int]:
 
 
26
  """
27
  Service function to get all global indices for a scripture
28
  that have audio files of the specified type.
29
-
30
  Args:
31
  scripture_name: Name of the scripture.
32
  audio_type: AudioType enum value.
33
-
34
  Returns:
35
  List[int]: Sorted list of global indices.
36
  """
@@ -53,3 +62,19 @@ async def svc_get_indices_with_audio(scripture_name: str, audio_type: AudioType)
53
  indices.sort()
54
 
55
  return indices
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import HTTPException
2
  from modules.audio.model import AudioRequest, AudioType
3
+ from modules.dropbox.art import get_audible_art_url
4
+ from modules.dropbox.audibles import get_audible_audio_url
5
  from modules.dropbox.audio import get_audio_urls, get_global_indices_with_audio
6
  from config import SanatanConfig
7
  from db import SanatanDatabase
8
  from typing import List
9
 
10
+
11
  async def svc_get_audio_urls(req: AudioRequest):
12
  config = SanatanConfig().get_scripture_by_name(req.scripture_name)
13
  audio_storage = config.get("audio_storage", "dropbox")
 
18
  data = db.fetch_document_by_index(
19
  collection_name=config["collection_name"], index=req.global_index
20
  )
21
+ url = data.get("audio", data.get("audio_url", ""))
22
  ## Temporary fix for bhagavat gita audio being moved in the source.
23
+ if req.scripture_name == "bhagavat_gita":
24
+ url = url.replace(
25
+ "https://cdn.vivekavani.com/wp-content/",
26
+ "https://vivekavani.com/wp-content/",
27
+ )
28
  urls = {"recitation": url}
29
  return urls
30
 
31
 
32
+ async def svc_get_indices_with_audio(
33
+ scripture_name: str, audio_type: AudioType
34
+ ) -> List[int]:
35
  """
36
  Service function to get all global indices for a scripture
37
  that have audio files of the specified type.
38
+
39
  Args:
40
  scripture_name: Name of the scripture.
41
  audio_type: AudioType enum value.
42
+
43
  Returns:
44
  List[int]: Sorted list of global indices.
45
  """
 
62
  indices.sort()
63
 
64
  return indices
65
+
66
+
67
+ async def svc_get_audible_audio_url(path: str):
68
+ if not path.startswith("_audibles/audio/"):
69
+ raise HTTPException(status_code=400, detail="Invalid audible path")
70
+
71
+ url = await get_audible_audio_url(path)
72
+ return url
73
+
74
+
75
+ async def svc_get_audible_art_url(path: str):
76
+ if not path.startswith("_audibles/art/"):
77
+ raise HTTPException(status_code=400, detail="Invalid audible path")
78
+
79
+ url = await get_audible_art_url(path)
80
+ return url
modules/dropbox/art.py ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ import json
3
+ import logging
4
+ from datetime import datetime, timedelta, timezone
5
+ from typing import List, Optional
6
+ import dropbox
7
+ from modules.dropbox.client import dbx
8
+ from fastapi import HTTPException
9
+
10
+ # Logger
11
+ logger = logging.getLogger(__name__)
12
+ logger.setLevel(logging.INFO)
13
+
14
+
15
+ # cache = { audible_path: {"url": ..., "expiry": ...} }
16
+ audible_art_cache: dict[str, dict] = {}
17
+ AUDIBLE_ART_CACHE_TTL = timedelta(hours=3, minutes=30)
18
+
19
+ async def get_audible_art_url(art_path: str):
20
+ """
21
+ Returns a temporary Dropbox download URL for an audible art file.
22
+ Uses in-memory caching to avoid regenerating links too frequently.
23
+ """
24
+
25
+ if not art_path:
26
+ raise HTTPException(status_code=400, detail="art_path is required")
27
+
28
+ # Normalize path (ensure leading slash)
29
+ dropbox_path = (
30
+ art_path if art_path.startswith("/") else f"/{art_path}"
31
+ )
32
+
33
+ now = datetime.now(timezone.utc)
34
+
35
+ # 1️⃣ Check cache
36
+ cached = audible_art_cache.get(dropbox_path)
37
+ if cached and cached["expiry"] > now:
38
+ return {"art_url": cached["url"]}
39
+
40
+ # 2️⃣ Generate fresh Dropbox temp link
41
+ try:
42
+ temp_link = dbx.files_get_temporary_link(dropbox_path).link
43
+ except dropbox.exceptions.ApiError:
44
+ raise HTTPException(status_code=404, detail="Audible art not found")
45
+
46
+ # 3️⃣ Cache it
47
+ audible_art_cache[dropbox_path] = {
48
+ "url": temp_link,
49
+ "expiry": now + AUDIBLE_ART_CACHE_TTL,
50
+ }
51
+
52
+ return {"art_url": temp_link}
53
+
54
+
55
+ async def cleanup_audible_art_cache(interval_seconds: int = 600):
56
+ while True:
57
+ now = datetime.now(timezone.utc)
58
+ expired = [
59
+ k for k, v in audible_art_cache.items()
60
+ if v["expiry"] <= now
61
+ ]
62
+ for k in expired:
63
+ del audible_art_cache[k]
64
+ await asyncio.sleep(interval_seconds)
modules/dropbox/audibles.py ADDED
@@ -0,0 +1,192 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ import json
3
+ import logging
4
+ from datetime import datetime, timedelta, timezone
5
+ from typing import List, Optional
6
+ import dropbox
7
+ from modules.dropbox.client import dbx
8
+ from fastapi import HTTPException
9
+
10
+ # Logger
11
+ logger = logging.getLogger(__name__)
12
+ logger.setLevel(logging.INFO)
13
+
14
+ # Cache: key = folder_path, value = {"timestamp": datetime, "data": List[dict]}
15
+ _audible_cache: dict[str, dict] = {}
16
+ CACHE_TTL = timedelta(hours=1)
17
+ FOLDER_PATH = "/_audibles"
18
+
19
+ async def fetch_audibles_from_dropbox() -> List[dict]:
20
+ """
21
+ Fetch all audible JSONs for a scripture from Dropbox with caching.
22
+ Expects files in "/_audibles/".
23
+ """
24
+ loop = asyncio.get_running_loop()
25
+ folder_path = FOLDER_PATH
26
+
27
+ # Check cache
28
+ cache_entry = _audible_cache.get(folder_path)
29
+ if cache_entry:
30
+ age = datetime.now() - cache_entry["timestamp"]
31
+ if age < CACHE_TTL:
32
+ logger.info(f"Using cached audibles for '{folder_path}' (age={age})")
33
+ return cache_entry["data"]
34
+
35
+ logger.info(f"Fetching audibles from Dropbox folder '{folder_path}'")
36
+ audibles: List[dict] = []
37
+
38
+ try:
39
+ # List folder contents (synchronously in executor)
40
+ res = await loop.run_in_executor(None, dbx.files_list_folder, folder_path)
41
+ for entry in res.entries:
42
+ if isinstance(entry, dropbox.files.FileMetadata) and entry.name.lower().endswith(".json"):
43
+ metadata, fres = await loop.run_in_executor(
44
+ None, dbx.files_download, f"{folder_path}/{entry.name}"
45
+ )
46
+ data = fres.content.decode("utf-8")
47
+ audibles.append(json.loads(data))
48
+
49
+ # Update cache
50
+ _audible_cache[folder_path] = {"timestamp": datetime.now(), "data": audibles}
51
+ logger.info(f"Cached {len(audibles)} audibles for '{folder_path}'")
52
+ return audibles
53
+
54
+ except Exception as e:
55
+ logger.error(f"Error fetching audibles from '{folder_path}'", exc_info=e)
56
+ # fallback to cached data if available
57
+ if cache_entry:
58
+ logger.warning(f"Returning stale cached audibles for '{folder_path}'")
59
+ return cache_entry["data"]
60
+ else:
61
+ logger.warning(f"No cached audibles available for '{folder_path}'")
62
+ return []
63
+
64
+
65
+ async def get_audible_summaries(page: int = 1, per_page: int = 10):
66
+ """
67
+ Returns paginated summaries: id, topic_name, artwork_url.
68
+ Sorted by topic_name.
69
+ """
70
+ all_audibles = await fetch_audibles_from_dropbox()
71
+
72
+ # Build summaries
73
+ summaries = [
74
+ {
75
+ "id": d.get("id"),
76
+ "topic_name": d.get("topic_name"),
77
+ "artwork_url": d.get("artwork_url"),
78
+ }
79
+ for d in all_audibles
80
+ ]
81
+
82
+ summaries.sort(key=lambda x: (x.get("topic_name") or "").lower())
83
+
84
+ # Pagination
85
+ total_items = len(summaries)
86
+ total_pages = (total_items + per_page - 1) // per_page
87
+ if page < 1 or page > total_pages:
88
+ logger.warning(f"Invalid page {page}. Must be between 1 and {total_pages}")
89
+ return {"page": page, "per_page": per_page, "total_pages": total_pages, "total_items": total_items, "data": []}
90
+
91
+ start = (page - 1) * per_page
92
+ end = start + per_page
93
+ paginated = summaries[start:end]
94
+
95
+ print("audible data = ",paginated)
96
+
97
+ return {
98
+ "page": page,
99
+ "per_page": per_page,
100
+ "total_pages": total_pages,
101
+ "total_items": total_items,
102
+ "data": paginated,
103
+ }
104
+
105
+
106
+ async def get_audible_by_id(topic_id: int) -> Optional[dict]:
107
+ """
108
+ Fetch a single audible JSON by topic_id from Dropbox.
109
+ Uses in-memory caching per file.
110
+ """
111
+ loop = asyncio.get_running_loop()
112
+ file_path = f"{FOLDER_PATH}/{topic_id}.json"
113
+
114
+ # Check cache
115
+ cache_entry = _audible_cache.get(file_path)
116
+ if cache_entry:
117
+ age = datetime.now() - cache_entry["timestamp"]
118
+ if age < CACHE_TTL:
119
+ logger.info(f"Using cached audible for topic {topic_id} (age={age})")
120
+ return cache_entry["data"]
121
+
122
+ try:
123
+ logger.info(f"Fetching audible {topic_id} from Dropbox: {file_path}")
124
+ metadata, res = await loop.run_in_executor(None, dbx.files_download, file_path)
125
+ data = res.content.decode("utf-8")
126
+ audible = json.loads(data)
127
+
128
+ # Update cache
129
+ _audible_cache[file_path] = {"timestamp": datetime.now(), "data": audible}
130
+ return audible
131
+
132
+ except dropbox.exceptions.HttpError as e:
133
+ logger.error(f"Dropbox file not found: {file_path}", exc_info=e)
134
+ return None
135
+ except Exception as e:
136
+ logger.error(f"Error fetching audible {topic_id}", exc_info=e)
137
+ # fallback to cached data if available
138
+ if cache_entry:
139
+ logger.warning(f"Returning stale cached audible for topic {topic_id}")
140
+ return cache_entry["data"]
141
+ return None
142
+
143
+ # cache = { audible_path: {"url": ..., "expiry": ...} }
144
+ audible_audio_cache: dict[str, dict] = {}
145
+ AUDIBLE_CACHE_TTL = timedelta(hours=3, minutes=30)
146
+
147
+ async def get_audible_audio_url(audio_path: str):
148
+ """
149
+ Returns a temporary Dropbox download URL for an audible audio file.
150
+ Uses in-memory caching to avoid regenerating links too frequently.
151
+ """
152
+
153
+ if not audio_path:
154
+ raise HTTPException(status_code=400, detail="audio_path is required")
155
+
156
+ # Normalize path (ensure leading slash)
157
+ dropbox_path = (
158
+ audio_path if audio_path.startswith("/") else f"/{audio_path}"
159
+ )
160
+
161
+ now = datetime.now(timezone.utc)
162
+
163
+ # 1️⃣ Check cache
164
+ cached = audible_audio_cache.get(dropbox_path)
165
+ if cached and cached["expiry"] > now:
166
+ return {"audio_url": cached["url"]}
167
+
168
+ # 2️⃣ Generate fresh Dropbox temp link
169
+ try:
170
+ temp_link = dbx.files_get_temporary_link(dropbox_path).link
171
+ except dropbox.exceptions.ApiError:
172
+ raise HTTPException(status_code=404, detail="Audible audio not found")
173
+
174
+ # 3️⃣ Cache it
175
+ audible_audio_cache[dropbox_path] = {
176
+ "url": temp_link,
177
+ "expiry": now + AUDIBLE_CACHE_TTL,
178
+ }
179
+
180
+ return {"audio_url": temp_link}
181
+
182
+
183
+ async def cleanup_audible_audio_cache(interval_seconds: int = 600):
184
+ while True:
185
+ now = datetime.now(timezone.utc)
186
+ expired = [
187
+ k for k, v in audible_audio_cache.items()
188
+ if v["expiry"] <= now
189
+ ]
190
+ for k in expired:
191
+ del audible_audio_cache[k]
192
+ await asyncio.sleep(interval_seconds)
server.py CHANGED
@@ -13,8 +13,9 @@ from config import SanatanConfig
13
  from db import SanatanDatabase
14
  from metadata import MetadataWhereClause
15
  from modules.audio.model import AudioRequest, AudioType
16
- from modules.audio.service import svc_get_audio_urls, svc_get_indices_with_audio
17
  from modules.config.categories import get_scripture_categories
 
18
  from modules.dropbox.discources import get_discourse_by_id, get_discourse_summaries
19
  from modules.firebase.messaging import FcmRequest, fcm_service
20
  from modules.languages.get_v2 import handle_fetch_languages_v2
@@ -634,4 +635,50 @@ async def send_broadcast_message(req: BroadcastRequest):
634
  body=req.body,
635
  data=req.data
636
  )
637
- return response
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  from db import SanatanDatabase
14
  from metadata import MetadataWhereClause
15
  from modules.audio.model import AudioRequest, AudioType
16
+ from modules.audio.service import svc_get_audible_art_url, svc_get_audible_audio_url, svc_get_audio_urls, svc_get_indices_with_audio
17
  from modules.config.categories import get_scripture_categories
18
+ from modules.dropbox.audibles import get_audible_by_id, get_audible_summaries
19
  from modules.dropbox.discources import get_discourse_by_id, get_discourse_summaries
20
  from modules.firebase.messaging import FcmRequest, fcm_service
21
  from modules.languages.get_v2 import handle_fetch_languages_v2
 
635
  body=req.body,
636
  data=req.data
637
  )
638
+ return response
639
+
640
+ @router.get("/audible/list")
641
+ async def get_all_audibles(
642
+ page: int = Query(1, ge=1, description="Page number (1-indexed)"),
643
+ per_page: int = Query(10, ge=1, le=100, description="Number of items per page"),
644
+ ):
645
+ """
646
+ Returns a paginated list of audible topics.
647
+ Each topic includes:
648
+ - id
649
+ - topic_name
650
+ - artwork_url
651
+ """
652
+ result = await get_audible_summaries(page=page, per_page=per_page)
653
+ return result
654
+
655
+
656
+ @router.get("/audible/find/{topic_id}")
657
+ async def get_audible_detail(topic_id: int):
658
+ """
659
+ Returns the full details of a audible topic by its unique ID.
660
+ """
661
+ topic = await get_audible_by_id(topic_id)
662
+ if not topic:
663
+ raise HTTPException(status_code=404, detail="Audible topic not found")
664
+ return topic
665
+
666
+ @router.get("/audible/audio-url")
667
+ async def get_audible_audio_url(path: str):
668
+ """
669
+ Returns the audio url of the audible by path
670
+ """
671
+ url = await svc_get_audible_audio_url(path)
672
+ if not url:
673
+ raise HTTPException(status_code=404, detail="Audible audio not found")
674
+ return url
675
+
676
+ @router.get("/audible/art-url")
677
+ async def get_audible_art_url(path: str):
678
+ """
679
+ Returns the art url of the audible by path
680
+ """
681
+ url = await svc_get_audible_art_url(path)
682
+ if not url:
683
+ raise HTTPException(status_code=404, detail="Audible art not found")
684
+ return url