File size: 4,852 Bytes
064643c
e74e470
064643c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e74e470
064643c
 
e74e470
064643c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e74e470
 
064643c
 
 
 
 
 
 
 
 
e74e470
 
064643c
e74e470
064643c
 
e74e470
064643c
 
 
 
 
 
 
 
 
 
 
 
e74e470
 
064643c
e74e470
064643c
e74e470
064643c
 
e74e470
 
 
 
 
 
 
 
 
 
064643c
e74e470
 
 
064643c
e74e470
064643c
 
e74e470
064643c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e74e470
064643c
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
import asyncio
import json
import logging
from datetime import datetime, timedelta
from typing import List, Optional
import dropbox
from modules.dropbox.client import dbx

# Logger
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)

# Cache: key = folder_path, value = {"timestamp": datetime, "data": List[dict]}
_discourse_cache: dict[str, dict] = {}
CACHE_TTL = timedelta(hours=1)
FOLDER_PATH = "/_discourses"

async def fetch_discourses_from_dropbox() -> List[dict]:
    """
    Fetch all discourse JSONs for a scripture from Dropbox with caching.
    Expects files in "/_discourses/".
    """
    loop = asyncio.get_running_loop()
    folder_path = FOLDER_PATH

    # Check cache
    cache_entry = _discourse_cache.get(folder_path)
    if cache_entry:
        age = datetime.now() - cache_entry["timestamp"]
        if age < CACHE_TTL:
            logger.info(f"Using cached discourses for '{folder_path}' (age={age})")
            return cache_entry["data"]

    logger.info(f"Fetching discourses from Dropbox folder '{folder_path}'")
    discourses: 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")
                discourses.append(json.loads(data))

        # Update cache
        _discourse_cache[folder_path] = {"timestamp": datetime.now(), "data": discourses}
        logger.info(f"Cached {len(discourses)} discourses for '{folder_path}'")
        return discourses

    except Exception as e:
        logger.error(f"Error fetching discourses from '{folder_path}'", exc_info=e)
        # fallback to cached data if available
        if cache_entry:
            logger.warning(f"Returning stale cached discourses for '{folder_path}'")
            return cache_entry["data"]
        else:
            logger.warning(f"No cached discourses available for '{folder_path}'")
            return []


async def get_discourse_summaries(page: int = 1, per_page: int = 10):
    """
    Returns paginated summaries: id, topic_name, thumbnail_url.
    Sorted by topic_name.
    """
    all_discourses = await fetch_discourses_from_dropbox()

    # Build summaries
    summaries = [
        {
            "id": d.get("id"),
            "topic_name": d.get("topic_name"),
            "thumbnail_url": d.get("thumbnail_url"),
        }
        for d in all_discourses
    ]

    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]

    return {
        "page": page,
        "per_page": per_page,
        "total_pages": total_pages,
        "total_items": total_items,
        "data": paginated,
    }


async def get_discourse_by_id(topic_id: int) -> Optional[dict]:
    """
    Fetch a single discourse 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 = _discourse_cache.get(file_path)
    if cache_entry:
        age = datetime.now() - cache_entry["timestamp"]
        if age < CACHE_TTL:
            logger.info(f"Using cached discourse for topic {topic_id} (age={age})")
            return cache_entry["data"]

    try:
        logger.info(f"Fetching discourse {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")
        discourse = json.loads(data)

        # Update cache
        _discourse_cache[file_path] = {"timestamp": datetime.now(), "data": discourse}
        return discourse

    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 discourse {topic_id}", exc_info=e)
        # fallback to cached data if available
        if cache_entry:
            logger.warning(f"Returning stale cached discourse for topic {topic_id}")
            return cache_entry["data"]
        return None