Эх сурвалжийг харах

Fix: Download and cache book cover images locally

Root cause: Audiobookshelf requires authentication for cover images,
but browsers cannot send auth headers when loading <img> tags. The
previous proxy approach still didn't work due to session handling issues.

Solution: Download and cache strategy
1. During sync, download each book's cover from Audiobookshelf API
2. Save covers locally to app/static/covers/ as WebP images
3. Store local paths in database (e.g., /static/covers/{book_id}.webp)
4. Serve covers as regular static files - no auth needed!

Implementation:
- Added download_cover_image() function that fetches from /api/items/{book_id}/cover
- Updated sync to download covers for all books
- Simplified frontend to use cover_url directly (no proxy needed)
- Downloaded all 100 existing book covers (2.7MB total)
- Removed obsolete proxy endpoint

Benefits:
- No authentication issues
- Faster page loads (served as static files)
- Offline availability (cached locally)
- No repeated API calls to Audiobookshelf

Fixes #2

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Brad Lance 3 сар өмнө
parent
commit
47774cb200
100 өөрчлөгдсөн 53 нэмэгдсэн , 46 устгасан
  1. 52 38
      app/main.py
  2. 1 8
      app/services/stats.py
  3. BIN
      app/static/covers/00b7bf38-26ca-41a5-bd87-68e37a91fedb.webp
  4. BIN
      app/static/covers/010ad511-227a-4388-8335-f7d1100a562b.webp
  5. BIN
      app/static/covers/03534add-6803-47df-9208-542f54d7e6c6.webp
  6. BIN
      app/static/covers/04a17c3a-8416-4353-998c-2d6e82a944a2.webp
  7. BIN
      app/static/covers/06dde181-80eb-421b-96fe-b563888373ce.webp
  8. BIN
      app/static/covers/0b4b8c56-c4c4-4c74-86d5-989657b8a193.webp
  9. BIN
      app/static/covers/0ed593eb-17c7-4e2e-9bb0-c8f63e0fa30d.webp
  10. BIN
      app/static/covers/17e3bf2f-e989-4f4e-b0c1-f93e82f73866.webp
  11. BIN
      app/static/covers/1a3d8f0c-4032-4e06-8157-b99b278f5cf1.webp
  12. BIN
      app/static/covers/1cfdab86-c5e9-412d-af05-2e7f79445ca3.webp
  13. BIN
      app/static/covers/20badc34-eb89-4126-b1f6-ba93a1c59c78.webp
  14. BIN
      app/static/covers/2548d670-aff8-4a29-9051-c937287e3384.webp
  15. BIN
      app/static/covers/2a1b7a9b-19ae-4c84-8c9c-3f6ad5ebe304.webp
  16. BIN
      app/static/covers/2a9ed507-a8df-4098-a62a-0235b69e2e38.webp
  17. BIN
      app/static/covers/2b245e72-5b11-4a3e-8e8b-9e391fc3643c.webp
  18. BIN
      app/static/covers/2ba4cceb-1021-4fb8-b11b-c819949f4549.webp
  19. BIN
      app/static/covers/2fdb10c2-b57f-4649-921b-286dc1a4d815.webp
  20. BIN
      app/static/covers/32f511aa-0a6b-4add-ba93-9cd9a305ac01.webp
  21. BIN
      app/static/covers/35268b2d-a1b2-46bd-ad0b-57198e8b8dde.webp
  22. BIN
      app/static/covers/3c14f2c2-1e53-4fb7-b111-9e6606431121.webp
  23. BIN
      app/static/covers/3d51b698-87b4-48a2-bf3b-73e09b7b91ac.webp
  24. BIN
      app/static/covers/43aaf7b0-b332-40e7-8a75-846b97dbc3b8.webp
  25. BIN
      app/static/covers/48811290-687b-49ec-a1a3-da4a083a6ea8.webp
  26. BIN
      app/static/covers/4ad9d924-620a-4f0a-a1cd-35a0eb47fe28.webp
  27. BIN
      app/static/covers/4efbcef2-4b66-4bb1-a053-dc0ce0f7c493.webp
  28. BIN
      app/static/covers/5062c24c-3136-472c-a5b8-b38c0cad8530.webp
  29. BIN
      app/static/covers/50b6003c-30f5-4090-b050-b4f441116903.webp
  30. BIN
      app/static/covers/54d537a2-c5de-47c5-83f9-34f1595c7235.webp
  31. BIN
      app/static/covers/56b278ab-38c4-4bd5-9404-cc4d92ef76e3.webp
  32. BIN
      app/static/covers/66cd339a-115d-4661-b0e0-50e4b4642c95.webp
  33. BIN
      app/static/covers/6923da32-04be-42bc-a0b5-ad2718dd8ae8.webp
  34. BIN
      app/static/covers/6a0d5526-42d3-48ed-975b-e94b56464d49.webp
  35. BIN
      app/static/covers/6b08e58e-5700-4f80-96b4-8ad227250818.webp
  36. BIN
      app/static/covers/6d788203-5c15-4a47-bd3a-1fae67145474.webp
  37. BIN
      app/static/covers/6f15e0db-c879-46ba-a9ed-6bd6dd46c3c4.webp
  38. BIN
      app/static/covers/7176e32a-ae5a-4950-a56e-bbf10ae8957b.webp
  39. BIN
      app/static/covers/758e71fc-4905-495c-ba12-af17294c5a98.webp
  40. BIN
      app/static/covers/7679f523-ac33-4eb4-ad25-73d2187864e0.webp
  41. BIN
      app/static/covers/7735e8e4-f25e-4fbe-82ab-279ce70b5c81.webp
  42. BIN
      app/static/covers/799fce78-68f4-4bf1-80af-9542356ec033.webp
  43. BIN
      app/static/covers/7a5d21f1-23f2-4fd0-bc12-a69c7ea2d366.webp
  44. BIN
      app/static/covers/7b9fe41f-f05a-4304-a99f-09f23ff438db.webp
  45. BIN
      app/static/covers/7ffc1816-c696-4652-b5a5-ef3323d25001.webp
  46. BIN
      app/static/covers/8029a8a4-8941-49a1-b5e0-867616e05b0f.webp
  47. BIN
      app/static/covers/80a1aefb-0b51-48ca-afdb-1170c80f5149.webp
  48. BIN
      app/static/covers/892b5757-c3b8-4c77-b8df-8fdf041659f6.webp
  49. BIN
      app/static/covers/897a56b0-f1a2-4109-af96-39f8473ed2dc.webp
  50. BIN
      app/static/covers/8ad5bf11-c185-4f6e-acdf-073b40130e1b.webp
  51. BIN
      app/static/covers/8d25d2be-b0cd-45f0-93b8-a64335535b30.webp
  52. BIN
      app/static/covers/8dc8aa73-b6a3-49fa-bd1a-768c7d14dfa4.webp
  53. BIN
      app/static/covers/90c43af9-f195-43dd-9ad2-954c41900334.webp
  54. BIN
      app/static/covers/9493a51e-027b-4856-aa83-dcc20b220aef.webp
  55. BIN
      app/static/covers/9560e149-c486-467f-bd6f-9f90253fc9cf.webp
  56. BIN
      app/static/covers/97aea7d2-c0db-496b-91ad-afb19aa31c56.webp
  57. BIN
      app/static/covers/9bb63467-2f56-49c3-845c-27f7d81bcfa4.webp
  58. BIN
      app/static/covers/9bdb5d76-e52a-4e1d-9aeb-24542aad39fe.webp
  59. BIN
      app/static/covers/9dd25506-7afb-4250-8fef-53fc7644dbfe.webp
  60. BIN
      app/static/covers/a1e85de5-e9d4-4774-97e2-c25d609ac4f5.webp
  61. BIN
      app/static/covers/a49982d9-f5a6-4c9e-86c4-6bed6a22b072.webp
  62. BIN
      app/static/covers/a4df1978-9e74-4b0e-978f-54d298431852.webp
  63. BIN
      app/static/covers/ab0f6976-0b92-4c43-9819-30efd99de5b7.webp
  64. BIN
      app/static/covers/acffcfff-474a-4bed-bc65-c6e648ab64ac.webp
  65. BIN
      app/static/covers/adf49fac-937a-477e-9aaf-b1a098823d57.webp
  66. BIN
      app/static/covers/b0ef5926-e251-4ad5-a1c3-ca391ce93999.webp
  67. BIN
      app/static/covers/b1e1f37c-09ba-4799-b318-bfa61e3a087c.webp
  68. BIN
      app/static/covers/b2180c26-f0f1-4433-ad4f-6def20a304f1.webp
  69. BIN
      app/static/covers/b2831da8-4801-4934-9057-f6bd6ef2a440.webp
  70. BIN
      app/static/covers/b2b5fc26-4294-4fb5-abdf-03dba699976e.webp
  71. BIN
      app/static/covers/b3768473-75fa-4ef8-8d73-6a973ef20cd2.webp
  72. BIN
      app/static/covers/b3cab588-fa09-4505-8e56-c7aa9397c6af.webp
  73. BIN
      app/static/covers/b63ee5cd-8bbf-47e0-8544-c4c9f75575b1.webp
  74. BIN
      app/static/covers/b6961989-7584-462e-ae60-fb4c042481ed.webp
  75. BIN
      app/static/covers/bbd1e370-215a-46ba-b176-85b11d35b27e.webp
  76. BIN
      app/static/covers/bc522697-a0f9-48db-8bd3-803aee2cfeee.webp
  77. BIN
      app/static/covers/bd3b955c-f3e5-498d-a282-8eda321ad6b7.webp
  78. BIN
      app/static/covers/be2e5389-e25d-4f57-b9d0-6d323f8c2395.webp
  79. BIN
      app/static/covers/bffacbac-f9e5-4e5d-bedb-1a84a8f81c15.webp
  80. BIN
      app/static/covers/c834972d-6d4e-4740-916d-874f3ff93690.webp
  81. BIN
      app/static/covers/ca3ad7fc-d3ea-410f-a004-d0d604604057.webp
  82. BIN
      app/static/covers/cc473431-b69c-4327-bad3-57229bd7e855.webp
  83. BIN
      app/static/covers/cf19355d-a2f2-4f2f-bf91-f8606dfc807b.webp
  84. BIN
      app/static/covers/d130bdba-a29a-4b3a-8e25-bbb1546e91bc.webp
  85. BIN
      app/static/covers/d36d01ee-3509-4e50-86fa-afd3235cd0a9.webp
  86. BIN
      app/static/covers/d634f420-71cd-498d-bf73-4e3842e35cdf.webp
  87. BIN
      app/static/covers/d80128d6-6d4c-4635-8494-f72dabc8c046.webp
  88. BIN
      app/static/covers/d9a5d894-64e0-4a4e-b200-1ee3f44b1d1d.webp
  89. BIN
      app/static/covers/e090c90b-b9d2-4286-9e91-8824762f8649.webp
  90. BIN
      app/static/covers/e265e037-affc-4826-b843-e711d44bca8a.webp
  91. BIN
      app/static/covers/e336d4c8-72ce-493f-aa91-2b583438ed2d.webp
  92. BIN
      app/static/covers/e372c69e-394f-4a41-8396-fadee2d765e7.webp
  93. BIN
      app/static/covers/e6a07378-9f5a-4ccf-9610-944defeaedef.webp
  94. BIN
      app/static/covers/e6e1a224-b540-4909-9bae-724517b055e4.webp
  95. BIN
      app/static/covers/e7030443-d0d8-4977-88f4-c037e042f208.webp
  96. BIN
      app/static/covers/ec9ac84d-59f1-421f-a179-953c0ffae8a6.webp
  97. BIN
      app/static/covers/ed7c3316-bbfd-4e70-a777-487e17541f9d.webp
  98. BIN
      app/static/covers/f2960b6d-78b0-48a7-9219-1a4bcf394be4.webp
  99. BIN
      app/static/covers/f7def97f-9f22-4174-b34c-44076ef451d4.webp
  100. BIN
      app/static/covers/fa9e97b8-51d1-453c-ab46-4892b4c36ecf.webp

+ 52 - 38
app/main.py

@@ -8,6 +8,9 @@ from contextlib import asynccontextmanager
 import json
 from datetime import datetime
 from typing import Optional
+import httpx
+import os
+from pathlib import Path
 
 from app.database import init_db, get_db
 from app.models import Book, ListeningSession, Recommendation, User, AppSettings
@@ -228,6 +231,49 @@ async def logout(response: Response):
 # ==================== API Routes ====================
 
 
+async def download_cover_image(abs_client, book_id: str) -> Optional[str]:
+    """
+    Download cover image from Audiobookshelf and save locally.
+
+    Args:
+        abs_client: Audiobookshelf client with authentication
+        book_id: Library item ID
+
+    Returns:
+        Local path to saved cover (e.g., /static/covers/book_id.jpg) or None
+    """
+    if not book_id:
+        return None
+
+    # Create covers directory if it doesn't exist
+    covers_dir = Path("app/static/covers")
+    covers_dir.mkdir(parents=True, exist_ok=True)
+
+    # Use Audiobookshelf API cover endpoint
+    cover_url = f"{abs_client.base_url}/api/items/{book_id}/cover"
+
+    async with httpx.AsyncClient() as client:
+        try:
+            response = await client.get(cover_url, headers=abs_client.headers, follow_redirects=True)
+            response.raise_for_status()
+
+            # Determine extension from content-type
+            content_type = response.headers.get("content-type", "image/jpeg")
+            ext = ".webp" if "webp" in content_type else ".jpg"
+            local_filename = f"{book_id}{ext}"
+            local_path = covers_dir / local_filename
+
+            # Save to local file
+            with open(local_path, "wb") as f:
+                f.write(response.content)
+
+            # Return path relative to static directory
+            return f"/static/covers/{local_filename}"
+        except Exception as e:
+            print(f"Failed to download cover for {book_id}: {e}")
+            return None
+
+
 @app.get("/api/sync")
 async def sync_with_audiobookshelf(
     db: AsyncSession = Depends(get_db),
@@ -270,6 +316,9 @@ async def sync_with_audiobookshelf(
             result = await db.execute(select(Book).where(Book.id == book_id))
             book = result.scalar_one_or_none()
 
+            # Download cover image and get local path
+            local_cover_url = await download_cover_image(abs_client, book_id)
+
             # Create or update book
             if not book:
                 book = Book(
@@ -281,14 +330,15 @@ async def sync_with_audiobookshelf(
                     genres=json.dumps(metadata.get("genres", [])),
                     tags=json.dumps(media.get("tags", [])),
                     duration=media.get("duration", 0),
-                    cover_url=media.get("coverPath")  # Store relative path
+                    cover_url=local_cover_url  # Store local path
                 )
                 db.add(book)
             else:
                 # Update existing book
                 book.title = metadata.get("title", book.title)
                 book.author = metadata.get("authorName", book.author)
-                book.cover_url = media.get("coverPath")  # Store relative path
+                if local_cover_url:  # Only update if download succeeded
+                    book.cover_url = local_cover_url
                 book.updated_at = datetime.now()
 
             # Update or create listening session
@@ -732,39 +782,3 @@ async def delete_user(
 async def health_check():
     """Health check endpoint."""
     return {"status": "healthy"}
-
-
-@app.get("/api/cover/{book_id}")
-async def get_book_cover(
-    book_id: str,
-    db: AsyncSession = Depends(get_db),
-    user: User = Depends(get_current_user)
-):
-    """Proxy book cover images from Audiobookshelf with authentication."""
-    import httpx
-
-    # Get book from database
-    result = await db.execute(select(Book).where(Book.id == book_id))
-    book = result.scalar_one_or_none()
-
-    if not book or not book.cover_url:
-        raise HTTPException(status_code=404, detail="Cover not found")
-
-    # Get user's ABS client
-    abs_client = get_abs_client(user)
-
-    # Fetch cover from Audiobookshelf with auth
-    cover_url = f"{abs_client.base_url}{book.cover_url}"
-
-    async with httpx.AsyncClient() as client:
-        try:
-            response = await client.get(cover_url, headers=abs_client.headers)
-            response.raise_for_status()
-
-            # Return image with appropriate content type
-            return Response(
-                content=response.content,
-                media_type=response.headers.get("content-type", "image/jpeg")
-            )
-        except httpx.HTTPError:
-            raise HTTPException(status_code=404, detail="Cover image not found")

+ 1 - 8
app/services/stats.py

@@ -247,13 +247,6 @@ class ReadingStatsService:
                 duration_hours = (session.finished_at - session.started_at).total_seconds() / 3600
                 listening_duration = round(duration_hours, 1)
 
-            # Construct full cover URL if abs_url is available
-            cover_url = book.cover_url
-            if cover_url and self.abs_url:
-                # If cover_url is a relative path, prepend abs_url
-                if not cover_url.startswith(('http://', 'https://')):
-                    cover_url = f"{self.abs_url.rstrip('/')}{cover_url if cover_url.startswith('/') else '/' + cover_url}"
-
             recent.append({
                 "book_id": book.id,
                 "title": book.title,
@@ -261,7 +254,7 @@ class ReadingStatsService:
                 "finished_at": session.finished_at.isoformat(),
                 "rating": session.rating,
                 "listening_duration": listening_duration,
-                "cover_url": cover_url
+                "cover_url": book.cover_url  # Now stores local path like /static/covers/book_id.jpg
             })
 
         return recent

BIN
app/static/covers/00b7bf38-26ca-41a5-bd87-68e37a91fedb.webp


BIN
app/static/covers/010ad511-227a-4388-8335-f7d1100a562b.webp


BIN
app/static/covers/03534add-6803-47df-9208-542f54d7e6c6.webp


BIN
app/static/covers/04a17c3a-8416-4353-998c-2d6e82a944a2.webp


BIN
app/static/covers/06dde181-80eb-421b-96fe-b563888373ce.webp


BIN
app/static/covers/0b4b8c56-c4c4-4c74-86d5-989657b8a193.webp


BIN
app/static/covers/0ed593eb-17c7-4e2e-9bb0-c8f63e0fa30d.webp


BIN
app/static/covers/17e3bf2f-e989-4f4e-b0c1-f93e82f73866.webp


BIN
app/static/covers/1a3d8f0c-4032-4e06-8157-b99b278f5cf1.webp


BIN
app/static/covers/1cfdab86-c5e9-412d-af05-2e7f79445ca3.webp


BIN
app/static/covers/20badc34-eb89-4126-b1f6-ba93a1c59c78.webp


BIN
app/static/covers/2548d670-aff8-4a29-9051-c937287e3384.webp


BIN
app/static/covers/2a1b7a9b-19ae-4c84-8c9c-3f6ad5ebe304.webp


BIN
app/static/covers/2a9ed507-a8df-4098-a62a-0235b69e2e38.webp


BIN
app/static/covers/2b245e72-5b11-4a3e-8e8b-9e391fc3643c.webp


BIN
app/static/covers/2ba4cceb-1021-4fb8-b11b-c819949f4549.webp


BIN
app/static/covers/2fdb10c2-b57f-4649-921b-286dc1a4d815.webp


BIN
app/static/covers/32f511aa-0a6b-4add-ba93-9cd9a305ac01.webp


BIN
app/static/covers/35268b2d-a1b2-46bd-ad0b-57198e8b8dde.webp


BIN
app/static/covers/3c14f2c2-1e53-4fb7-b111-9e6606431121.webp


BIN
app/static/covers/3d51b698-87b4-48a2-bf3b-73e09b7b91ac.webp


BIN
app/static/covers/43aaf7b0-b332-40e7-8a75-846b97dbc3b8.webp


BIN
app/static/covers/48811290-687b-49ec-a1a3-da4a083a6ea8.webp


BIN
app/static/covers/4ad9d924-620a-4f0a-a1cd-35a0eb47fe28.webp


BIN
app/static/covers/4efbcef2-4b66-4bb1-a053-dc0ce0f7c493.webp


BIN
app/static/covers/5062c24c-3136-472c-a5b8-b38c0cad8530.webp


BIN
app/static/covers/50b6003c-30f5-4090-b050-b4f441116903.webp


BIN
app/static/covers/54d537a2-c5de-47c5-83f9-34f1595c7235.webp


BIN
app/static/covers/56b278ab-38c4-4bd5-9404-cc4d92ef76e3.webp


BIN
app/static/covers/66cd339a-115d-4661-b0e0-50e4b4642c95.webp


BIN
app/static/covers/6923da32-04be-42bc-a0b5-ad2718dd8ae8.webp


BIN
app/static/covers/6a0d5526-42d3-48ed-975b-e94b56464d49.webp


BIN
app/static/covers/6b08e58e-5700-4f80-96b4-8ad227250818.webp


BIN
app/static/covers/6d788203-5c15-4a47-bd3a-1fae67145474.webp


BIN
app/static/covers/6f15e0db-c879-46ba-a9ed-6bd6dd46c3c4.webp


BIN
app/static/covers/7176e32a-ae5a-4950-a56e-bbf10ae8957b.webp


BIN
app/static/covers/758e71fc-4905-495c-ba12-af17294c5a98.webp


BIN
app/static/covers/7679f523-ac33-4eb4-ad25-73d2187864e0.webp


BIN
app/static/covers/7735e8e4-f25e-4fbe-82ab-279ce70b5c81.webp


BIN
app/static/covers/799fce78-68f4-4bf1-80af-9542356ec033.webp


BIN
app/static/covers/7a5d21f1-23f2-4fd0-bc12-a69c7ea2d366.webp


BIN
app/static/covers/7b9fe41f-f05a-4304-a99f-09f23ff438db.webp


BIN
app/static/covers/7ffc1816-c696-4652-b5a5-ef3323d25001.webp


BIN
app/static/covers/8029a8a4-8941-49a1-b5e0-867616e05b0f.webp


BIN
app/static/covers/80a1aefb-0b51-48ca-afdb-1170c80f5149.webp


BIN
app/static/covers/892b5757-c3b8-4c77-b8df-8fdf041659f6.webp


BIN
app/static/covers/897a56b0-f1a2-4109-af96-39f8473ed2dc.webp


BIN
app/static/covers/8ad5bf11-c185-4f6e-acdf-073b40130e1b.webp


BIN
app/static/covers/8d25d2be-b0cd-45f0-93b8-a64335535b30.webp


BIN
app/static/covers/8dc8aa73-b6a3-49fa-bd1a-768c7d14dfa4.webp


BIN
app/static/covers/90c43af9-f195-43dd-9ad2-954c41900334.webp


BIN
app/static/covers/9493a51e-027b-4856-aa83-dcc20b220aef.webp


BIN
app/static/covers/9560e149-c486-467f-bd6f-9f90253fc9cf.webp


BIN
app/static/covers/97aea7d2-c0db-496b-91ad-afb19aa31c56.webp


BIN
app/static/covers/9bb63467-2f56-49c3-845c-27f7d81bcfa4.webp


BIN
app/static/covers/9bdb5d76-e52a-4e1d-9aeb-24542aad39fe.webp


BIN
app/static/covers/9dd25506-7afb-4250-8fef-53fc7644dbfe.webp


BIN
app/static/covers/a1e85de5-e9d4-4774-97e2-c25d609ac4f5.webp


BIN
app/static/covers/a49982d9-f5a6-4c9e-86c4-6bed6a22b072.webp


BIN
app/static/covers/a4df1978-9e74-4b0e-978f-54d298431852.webp


BIN
app/static/covers/ab0f6976-0b92-4c43-9819-30efd99de5b7.webp


BIN
app/static/covers/acffcfff-474a-4bed-bc65-c6e648ab64ac.webp


BIN
app/static/covers/adf49fac-937a-477e-9aaf-b1a098823d57.webp


BIN
app/static/covers/b0ef5926-e251-4ad5-a1c3-ca391ce93999.webp


BIN
app/static/covers/b1e1f37c-09ba-4799-b318-bfa61e3a087c.webp


BIN
app/static/covers/b2180c26-f0f1-4433-ad4f-6def20a304f1.webp


BIN
app/static/covers/b2831da8-4801-4934-9057-f6bd6ef2a440.webp


BIN
app/static/covers/b2b5fc26-4294-4fb5-abdf-03dba699976e.webp


BIN
app/static/covers/b3768473-75fa-4ef8-8d73-6a973ef20cd2.webp


BIN
app/static/covers/b3cab588-fa09-4505-8e56-c7aa9397c6af.webp


BIN
app/static/covers/b63ee5cd-8bbf-47e0-8544-c4c9f75575b1.webp


BIN
app/static/covers/b6961989-7584-462e-ae60-fb4c042481ed.webp


BIN
app/static/covers/bbd1e370-215a-46ba-b176-85b11d35b27e.webp


BIN
app/static/covers/bc522697-a0f9-48db-8bd3-803aee2cfeee.webp


BIN
app/static/covers/bd3b955c-f3e5-498d-a282-8eda321ad6b7.webp


BIN
app/static/covers/be2e5389-e25d-4f57-b9d0-6d323f8c2395.webp


BIN
app/static/covers/bffacbac-f9e5-4e5d-bedb-1a84a8f81c15.webp


BIN
app/static/covers/c834972d-6d4e-4740-916d-874f3ff93690.webp


BIN
app/static/covers/ca3ad7fc-d3ea-410f-a004-d0d604604057.webp


BIN
app/static/covers/cc473431-b69c-4327-bad3-57229bd7e855.webp


BIN
app/static/covers/cf19355d-a2f2-4f2f-bf91-f8606dfc807b.webp


BIN
app/static/covers/d130bdba-a29a-4b3a-8e25-bbb1546e91bc.webp


BIN
app/static/covers/d36d01ee-3509-4e50-86fa-afd3235cd0a9.webp


BIN
app/static/covers/d634f420-71cd-498d-bf73-4e3842e35cdf.webp


BIN
app/static/covers/d80128d6-6d4c-4635-8494-f72dabc8c046.webp


BIN
app/static/covers/d9a5d894-64e0-4a4e-b200-1ee3f44b1d1d.webp


BIN
app/static/covers/e090c90b-b9d2-4286-9e91-8824762f8649.webp


BIN
app/static/covers/e265e037-affc-4826-b843-e711d44bca8a.webp


BIN
app/static/covers/e336d4c8-72ce-493f-aa91-2b583438ed2d.webp


BIN
app/static/covers/e372c69e-394f-4a41-8396-fadee2d765e7.webp


BIN
app/static/covers/e6a07378-9f5a-4ccf-9610-944defeaedef.webp


BIN
app/static/covers/e6e1a224-b540-4909-9bae-724517b055e4.webp


BIN
app/static/covers/e7030443-d0d8-4977-88f4-c037e042f208.webp


BIN
app/static/covers/ec9ac84d-59f1-421f-a179-953c0ffae8a6.webp


BIN
app/static/covers/ed7c3316-bbfd-4e70-a777-487e17541f9d.webp


BIN
app/static/covers/f2960b6d-78b0-48a7-9219-1a4bcf394be4.webp


BIN
app/static/covers/f7def97f-9f22-4174-b34c-44076ef451d4.webp


BIN
app/static/covers/fa9e97b8-51d1-453c-ab46-4892b4c36ecf.webp


Энэ ялгаанд хэт олон файл өөрчлөгдсөн тул зарим файлыг харуулаагүй болно