Ying Jiang commited on
Commit
c3265d8
·
1 Parent(s): 5028126

check point 2

Browse files
README_API.md CHANGED
@@ -11,26 +11,6 @@ pip install fastapi "uvicorn[standard]"
11
 
12
  Or install all deps: `pip install -r requirements.txt`
13
 
14
- ### Environment (reading lists / Supabase auth)
15
-
16
- | Variable | Purpose |
17
- |----------|---------|
18
- | `DATABASE_URL` | PostgreSQL for app data |
19
- | **Either** `SUPABASE_JWT_SECRET` **or** remote verification below | Verify the user’s `access_token` |
20
-
21
- **If your project no longer exposes a legacy JWT secret** (dashboard says to use signing keys / publishable keys), skip `SUPABASE_JWT_SECRET` and set:
22
-
23
- | Variable | Where to copy |
24
- |----------|----------------|
25
- | `SUPABASE_URL` | Same as frontend `EXPO_PUBLIC_SUPABASE_URL` (e.g. `https://xxxx.supabase.co`) |
26
- | `SUPABASE_ANON_KEY` | Same as frontend `EXPO_PUBLIC_SUPABASE_ANON_KEY` (anon / publishable key) |
27
-
28
- The API then calls `GET {SUPABASE_URL}/auth/v1/user` with the client’s Bearer token to validate it (no local JWT secret needed).
29
-
30
- If `SUPABASE_JWT_SECRET` is set, it is used first (offline verification; faster).
31
-
32
- See `backend/.env.example`.
33
-
34
  uvicorn is ASGI server, in production will need something like to use to start
35
 
36
  ```bash
 
11
 
12
  Or install all deps: `pip install -r requirements.txt`
13
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  uvicorn is ASGI server, in production will need something like to use to start
15
 
16
  ```bash
alembic/versions/20260329_reading_list_item_external_ids.py DELETED
@@ -1,152 +0,0 @@
1
- """reading_list_item: store provider + external manga id, drop FK to public.manga
2
-
3
- Revision ID: b7e2a1c0d9f8
4
- Revises: 34d55d0d8469
5
- Create Date: 2026-03-29
6
-
7
- """
8
- from alembic import op
9
- import sqlalchemy as sa
10
- from sqlalchemy import text
11
-
12
-
13
- revision = "b7e2a1c0d9f8"
14
- down_revision = "34d55d0d8469"
15
- branch_labels = None
16
- depends_on = None
17
-
18
-
19
- def upgrade() -> None:
20
- op.add_column(
21
- "reading_list_item",
22
- sa.Column("provider_id", sa.String(), nullable=True),
23
- )
24
- op.add_column(
25
- "reading_list_item",
26
- sa.Column("external_manga_id", sa.String(), nullable=True),
27
- )
28
- op.add_column(
29
- "reading_list_item",
30
- sa.Column("manga_title", sa.String(length=500), nullable=True),
31
- )
32
-
33
- conn = op.get_bind()
34
- conn.execute(
35
- text(
36
- """
37
- UPDATE reading_list_item AS rli
38
- SET
39
- provider_id = ms.provider_id,
40
- external_manga_id = ms.external_manga_id,
41
- manga_title = m.manga_title
42
- FROM manga AS m
43
- INNER JOIN manga_source AS ms
44
- ON ms.manga_id = m.id
45
- AND ms.provider_id = 'mangadex'
46
- WHERE rli.manga_id = m.id
47
- """
48
- )
49
- )
50
- conn.execute(text("DELETE FROM reading_list_item WHERE external_manga_id IS NULL"))
51
-
52
- op.drop_constraint(
53
- "reading_list_item_manga_id_fkey",
54
- "reading_list_item",
55
- type_="foreignkey",
56
- )
57
- op.drop_constraint("uq_rli_list_manga", "reading_list_item", type_="unique")
58
- op.drop_index(op.f("ix_reading_list_item_manga_id"), table_name="reading_list_item")
59
- op.drop_column("reading_list_item", "manga_id")
60
-
61
- op.alter_column(
62
- "reading_list_item",
63
- "provider_id",
64
- existing_type=sa.String(),
65
- nullable=False,
66
- )
67
- op.alter_column(
68
- "reading_list_item",
69
- "external_manga_id",
70
- existing_type=sa.String(),
71
- nullable=False,
72
- )
73
- op.alter_column(
74
- "reading_list_item",
75
- "manga_title",
76
- existing_type=sa.String(length=500),
77
- nullable=False,
78
- )
79
-
80
- op.create_index(
81
- op.f("ix_reading_list_item_external_manga_id"),
82
- "reading_list_item",
83
- ["external_manga_id"],
84
- unique=False,
85
- )
86
- op.create_index(
87
- op.f("ix_reading_list_item_provider_id"),
88
- "reading_list_item",
89
- ["provider_id"],
90
- unique=False,
91
- )
92
- op.create_unique_constraint(
93
- "uq_rli_list_provider_external",
94
- "reading_list_item",
95
- ["reading_list_id", "provider_id", "external_manga_id"],
96
- )
97
-
98
-
99
- def downgrade() -> None:
100
- op.drop_constraint(
101
- "uq_rli_list_provider_external", "reading_list_item", type_="unique"
102
- )
103
- op.drop_index(
104
- op.f("ix_reading_list_item_provider_id"), table_name="reading_list_item"
105
- )
106
- op.drop_index(
107
- op.f("ix_reading_list_item_external_manga_id"),
108
- table_name="reading_list_item",
109
- )
110
-
111
- op.add_column(
112
- "reading_list_item",
113
- sa.Column("manga_id", sa.Integer(), nullable=True),
114
- )
115
-
116
- conn = op.get_bind()
117
- conn.execute(
118
- text(
119
- """
120
- UPDATE reading_list_item AS rli
121
- SET manga_id = ms.manga_id
122
- FROM manga_source AS ms
123
- WHERE rli.provider_id = ms.provider_id
124
- AND rli.external_manga_id = ms.external_manga_id
125
- """
126
- )
127
- )
128
- conn.execute(text("DELETE FROM reading_list_item WHERE manga_id IS NULL"))
129
-
130
- op.alter_column(
131
- "reading_list_item", "manga_id", existing_type=sa.Integer(), nullable=False
132
- )
133
- op.create_foreign_key(
134
- "reading_list_item_manga_id_fkey",
135
- "reading_list_item",
136
- "manga",
137
- ["manga_id"],
138
- ["id"],
139
- )
140
- op.create_index(
141
- op.f("ix_reading_list_item_manga_id"),
142
- "reading_list_item",
143
- ["manga_id"],
144
- unique=False,
145
- )
146
- op.create_unique_constraint(
147
- "uq_rli_list_manga", "reading_list_item", ["reading_list_id", "manga_id"]
148
- )
149
-
150
- op.drop_column("reading_list_item", "manga_title")
151
- op.drop_column("reading_list_item", "external_manga_id")
152
- op.drop_column("reading_list_item", "provider_id")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
api.py CHANGED
@@ -258,6 +258,14 @@ async def proxy_manga_cover_art(manga_id: str, file_name: str, size: int = 256):
258
  print(url)
259
  return await proxy.get_manga_page_stream(url)
260
 
 
 
 
 
 
 
 
 
261
  @router.get("/api/manga/search")
262
  async def get_popular_manga(
263
  title: str = "",
 
258
  print(url)
259
  return await proxy.get_manga_page_stream(url)
260
 
261
+
262
+ @router.get("/api/manga/{manga_id}/cover")
263
+ async def get_manga_cover_json(manga_id: str):
264
+ """MangaDex manga UUID → 256px cover URL for list UIs (no DB storage)."""
265
+ cover_url = await mangadex_service.get_manga_cover_url_256(manga_id)
266
+ return {"cover_url": cover_url}
267
+
268
+
269
  @router.get("/api/manga/search")
270
  async def get_popular_manga(
271
  title: str = "",
services/mangadex_service.py CHANGED
@@ -5,6 +5,40 @@ import httpx
5
 
6
  BASE_URL = "https://api.mangadex.org"
7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
  async def search_manga(title: str, limit: int =20, offset: int = 0, order_by: str = "followedCount", order_direction: str = "desc", cover_art: bool = True):
9
  """
10
  Todo: filters by tags (include, exclude)
 
5
 
6
  BASE_URL = "https://api.mangadex.org"
7
 
8
+
9
+ def _cover_url_from_manga_payload(data: dict, manga_id: str) -> str | None:
10
+ """Build 256px cover URL from MangaDex /manga/{id} JSON (needs cover_art relationship)."""
11
+ rels = data.get("relationships") or []
12
+ for rel in rels:
13
+ if rel.get("type") != "cover_art":
14
+ continue
15
+ attrs = rel.get("attributes") or {}
16
+ fn = attrs.get("fileName")
17
+ if isinstance(fn, str) and fn.strip():
18
+ return f"https://uploads.mangadex.org/covers/{manga_id}/{fn}.256.jpg"
19
+ return None
20
+
21
+
22
+ async def get_manga_cover_url_256(manga_id: str) -> str | None:
23
+ """GET /manga/{id} with cover_art included; returns CDN URL or None."""
24
+ url = f"{BASE_URL}/manga/{manga_id}"
25
+ params = {"includes[]": ["cover_art"]}
26
+ async with httpx.AsyncClient() as client:
27
+ try:
28
+ response = await client.get(url, params=params, timeout=10.0)
29
+ response.raise_for_status()
30
+ payload = response.json()
31
+ except httpx.HTTPStatusError:
32
+ return None
33
+ except Exception as e:
34
+ print(f"get_manga_cover_url_256: {e}")
35
+ return None
36
+ data = payload.get("data")
37
+ if not isinstance(data, dict):
38
+ return None
39
+ return _cover_url_from_manga_payload(data, manga_id)
40
+
41
+
42
  async def search_manga(title: str, limit: int =20, offset: int = 0, order_by: str = "followedCount", order_direction: str = "desc", cover_art: bool = True):
43
  """
44
  Todo: filters by tags (include, exclude)