mdlaw 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mdlaw-1.0.0/LICENSE +21 -0
- mdlaw-1.0.0/PKG-INFO +610 -0
- mdlaw-1.0.0/README.md +582 -0
- mdlaw-1.0.0/mdlaw.egg-info/PKG-INFO +610 -0
- mdlaw-1.0.0/mdlaw.egg-info/SOURCES.txt +11 -0
- mdlaw-1.0.0/mdlaw.egg-info/dependency_links.txt +1 -0
- mdlaw-1.0.0/mdlaw.egg-info/entry_points.txt +2 -0
- mdlaw-1.0.0/mdlaw.egg-info/requires.txt +9 -0
- mdlaw-1.0.0/mdlaw.egg-info/top_level.txt +1 -0
- mdlaw-1.0.0/mdlaw.py +801 -0
- mdlaw-1.0.0/pyproject.toml +39 -0
- mdlaw-1.0.0/setup.cfg +4 -0
- mdlaw-1.0.0/tests/test_mdlaw.py +95 -0
mdlaw-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 pandamoon21
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
mdlaw-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,610 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mdlaw
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Blazing-fast unofficial MyDramaList API wrapper (FastAPI + httpx, official MDL Android app API)
|
|
5
|
+
Author: pandamoon21
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: mydramalist,mdl,drama,k-drama,api,wrapper,fastapi
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
17
|
+
Requires-Python: >=3.10
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
Requires-Dist: fastapi>=0.110
|
|
21
|
+
Requires-Dist: uvicorn[standard]>=0.29
|
|
22
|
+
Requires-Dist: httpx>=0.27
|
|
23
|
+
Provides-Extra: mysql
|
|
24
|
+
Requires-Dist: pymysql>=1.1; extra == "mysql"
|
|
25
|
+
Provides-Extra: postgres
|
|
26
|
+
Requires-Dist: psycopg[binary]>=3.1; extra == "postgres"
|
|
27
|
+
Dynamic: license-file
|
|
28
|
+
|
|
29
|
+
# mdlaw
|
|
30
|
+
|
|
31
|
+
**Blazing-fast unofficial API for [MyDramaList](https://mydramalist.com)**, powered by the **official MDL Android app API** (`app-api.mydramalist.com/v1`).
|
|
32
|
+
|
|
33
|
+
Built from reverse-engineering `com.mydramalist.app` v2.3.18 (Flutter/Dio): the app's request header scheme, auth flow, and endpoint inventory were recovered and live-validated before this wrapper was written.
|
|
34
|
+
|
|
35
|
+
> ⚠️ **Unofficial.** Not affiliated with MyDramaList. For research & personal use. Stop using if an official API ships.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## ✨ Why mdlaw?
|
|
40
|
+
|
|
41
|
+
| | Web scrapers | **mdlaw** |
|
|
42
|
+
|---|---|---|
|
|
43
|
+
| Data source | HTML scraping (`mydramalist.com`) | **Official app JSON API** |
|
|
44
|
+
| Speed | Slow — parses HTML per request | **~1 ms** after cache warm-up |
|
|
45
|
+
| Fragility | Breaks when site markup changes | Stable — same API the app uses |
|
|
46
|
+
| WAF risk | High | Throttled + cached outbound |
|
|
47
|
+
|
|
48
|
+
The Android app talks to a JSON API behind Cloudflare. `mdlaw` replicates the exact request headers the app sends (validated: without them the API returns **403**), so it gets **real structured data** — not scraped HTML.
|
|
49
|
+
|
|
50
|
+
## 📊 Real-world benchmark (measured, not estimated)
|
|
51
|
+
|
|
52
|
+
Same machine, same network, live hits (median of 3 runs):
|
|
53
|
+
|
|
54
|
+
| Test | Latency | Payload |
|
|
55
|
+
|---|---|---|
|
|
56
|
+
| **mdlaw** `GET /api/v1/genres` (cached) | **2.9 ms** | 35 genres · 1.7 KB JSON |
|
|
57
|
+
| **mdlaw** `GET /api/v1/titles/1/reviews` (cold — hits upstream) | **298 ms** | 5 reviews JSON |
|
|
58
|
+
| **Web scraper** fetch `mydramalist.com/` + parse HTML | **395 ms** | 112 KB HTML |
|
|
59
|
+
|
|
60
|
+
**Result: mdlaw (cached) is ~136× faster** than a typical web scraper, and even a *cold* mdlaw hit (real upstream call) beats HTML scraping. On top of that, scrapers return HTML that you still have to parse, while mdlaw hands you clean JSON.
|
|
61
|
+
|
|
62
|
+
> Benchmark: 2026-09-01, `python3` + `urllib` on localhost → mydramalist.com. Your numbers will vary with network, but the order of magnitude is the point.
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## 🤖 AI-agent instructions
|
|
67
|
+
|
|
68
|
+
`AGENTS.md` is a purpose-built instruction file for AI coding agents (and new
|
|
69
|
+
developers): how to install, verify, extend, and deploy `mdlaw` without
|
|
70
|
+
tripping on the WAF/throttle/cache rules.
|
|
71
|
+
|
|
72
|
+
- **File**: [`AGENTS.md`](./AGENTS.md)
|
|
73
|
+
- It covers: quick start, verification commands, env var reference, how the
|
|
74
|
+
throttling/cache/auth works, conventions & pitfalls (never remove the
|
|
75
|
+
throttle, keep tests offline, don't leak credentials), and deploy steps.
|
|
76
|
+
|
|
77
|
+
Point any agent (or your future self) at `AGENTS.md` first — it's the fastest
|
|
78
|
+
path to a working setup.
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## 🚀 Quickstart
|
|
83
|
+
|
|
84
|
+
**Option 1 — pip (PyPI):**
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
pip install mdlaw
|
|
88
|
+
export MDL_API_KEY=<your-key> # REQUIRED — no key embedded in the public build
|
|
89
|
+
mdlaw # serves http://0.0.0.0:8000 (Swagger at /docs)
|
|
90
|
+
mdlaw self # offline self-check (needs MDL_API_KEY)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
**Option 2 — from source:**
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
# 1. Clone & enter
|
|
97
|
+
git clone <your-repo-url> mdlaw && cd mdlaw
|
|
98
|
+
|
|
99
|
+
# 2. Create venv & install
|
|
100
|
+
python3 -m venv .venv && . .venv/bin/activate
|
|
101
|
+
pip install -r requirements.txt
|
|
102
|
+
|
|
103
|
+
# 3. Run
|
|
104
|
+
uvicorn mdlaw:app --port 8000
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Verify:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
curl http://localhost:8000/api/v1/health
|
|
111
|
+
# {"status":"ok","service":"mdlaw"}
|
|
112
|
+
|
|
113
|
+
curl http://localhost:8000/api/v1/genres | python3 -m json.tool
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Interactive docs (Swagger UI): **http://localhost:8000/docs**
|
|
117
|
+
|
|
118
|
+
## 📚 API docs & playground
|
|
119
|
+
|
|
120
|
+
`mdlaw` ships with the industry-standard docs out of the box (FastAPI built-ins — no extra setup):
|
|
121
|
+
|
|
122
|
+
| URL | What it is |
|
|
123
|
+
|---|---|
|
|
124
|
+
| **`/`** | Redirects to Swagger UI |
|
|
125
|
+
| **`/docs`** | **Swagger UI — interactive playground.** "Try it out" on any endpoint, fill params, execute live, see the JSON response + latency. |
|
|
126
|
+
| **`/redoc`** | ReDoc — clean, readable reference docs |
|
|
127
|
+
| **`/openapi.json`** | Machine-readable OpenAPI 3.1 spec (for codegen / clients) |
|
|
128
|
+
|
|
129
|
+
The Swagger playground is pre-configured for quick use: *Try it out* is open by default, request duration is displayed, and the schemas section is collapsed so endpoints are front and center.
|
|
130
|
+
|
|
131
|
+
## 🖥 Deploy locally
|
|
132
|
+
|
|
133
|
+
Three options, same code.
|
|
134
|
+
|
|
135
|
+
### Option 1 — Run directly (dev)
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
python3 -m venv .venv && . .venv/bin/activate
|
|
139
|
+
pip install -r requirements.txt
|
|
140
|
+
uvicorn mdlaw:app --host 0.0.0.0 --port 8000
|
|
141
|
+
# → http://localhost:8000/docs
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Option 2 — Docker
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
docker build -t mdlaw .
|
|
148
|
+
docker run -p 8000:8000 mdlaw
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Option 3 — Docker Compose (production-ish)
|
|
152
|
+
|
|
153
|
+
```yaml
|
|
154
|
+
# docker-compose.yml
|
|
155
|
+
services:
|
|
156
|
+
mdlaw:
|
|
157
|
+
build: .
|
|
158
|
+
ports:
|
|
159
|
+
- "8000:8000"
|
|
160
|
+
environment:
|
|
161
|
+
- MDL_API_KEY=${MDL_API_KEY} # optional
|
|
162
|
+
restart: unless-stopped
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
docker compose up -d
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## 📡 Endpoints
|
|
172
|
+
|
|
173
|
+
All responses are the **raw upstream JSON** (fastest path, zero transformation).
|
|
174
|
+
|
|
175
|
+
| Method | Route | Upstream | Cache TTL | Auth |
|
|
176
|
+
|---|---|---|---|---|
|
|
177
|
+
| GET | `/api/v1/health` | — | — | — |
|
|
178
|
+
| GET | `/api/v1/dashboard` | — *(local)* | — | — |
|
|
179
|
+
| GET | `/api/v1/status` | — *(alias dashboard)* | — | — |
|
|
180
|
+
| GET | `/api/v1/auth/status` | — *(local)* | — | — |
|
|
181
|
+
| GET | `/api/v1/cache/stats` | — *(local)* | — | — |
|
|
182
|
+
| POST | `/api/v1/auth/login` | `POST /auth/login` *(force re-login)* | — | env |
|
|
183
|
+
| POST | `/api/v1/auth/refresh` | `POST /auth/refresh` *(force refresh)* | — | env |
|
|
184
|
+
| GET | `/api/v1/genres` | `GET /genres` | 1h | — |
|
|
185
|
+
| GET | `/api/v1/languages` | `GET /languages/supported?v=2` | 1h | — |
|
|
186
|
+
| GET | `/api/v1/titles/{id}` | `GET /titles/{id}?expand=1` | 5m | ✅ |
|
|
187
|
+
| GET | `/api/v1/titles/{id}/reviews` | `GET /titles/{id}/reviews` | 5m | — |
|
|
188
|
+
| GET | `/api/v1/titles/{id}/comments` | `GET /titles/{id}/comments` | 5m | — |
|
|
189
|
+
| GET | `/api/v1/titles/{id}/credits` | `GET /titles/{id}/credits` | 5m | — |
|
|
190
|
+
| GET | `/api/v1/titles/{id}/recommendations` | `GET /titles/{id}/recommendations` | 10m | ✅ |
|
|
191
|
+
| POST | `/api/v1/search?q={q}` | `POST /search` (body `{"q", "synopsis"}`) | 5m | ✅ |
|
|
192
|
+
| POST | `/api/v1/search/people?q={q}` | `POST /search/people` (body `{"q"}`) | 5m | ✅ |
|
|
193
|
+
| GET | `/api/v1/watchlist` | `GET /sync/mylist/watchlist` | 1m | ✅ |
|
|
194
|
+
| GET | `/api/v1/watchlist/{status}` | `GET /sync/mylist/{status}` | 1m | ✅ |
|
|
195
|
+
| GET | `/api/v1/me` | `GET /users/me` | 1m | ✅ |
|
|
196
|
+
| GET | `/api/v1/calendar` | `POST /calendar/episodes` | 1h | — |
|
|
197
|
+
| GET | `/api/v1/articles/featured?page={page}` | `GET /articles/featured` | 10m | — |
|
|
198
|
+
| GET | `/api/v1/lists/featured?limit={limit}` | `GET /lists/featured` | 10m | — |
|
|
199
|
+
| GET | `/api/v1/lists/popular?limit={limit}` | `GET /lists/popular_voting_lists` | 10m | — |
|
|
200
|
+
| GET | `/api/v1/people/leaderboard?period={alltime\|weekly\|monthly}` | `GET /people/leaderboard` | 10m | — |
|
|
201
|
+
| GET | `/api/v1/people/{id}` | `GET /people/{id}` | 24h | — |
|
|
202
|
+
| GET | `/api/v1/payment/plans` | `GET /payment/plans` | 1h | — |
|
|
203
|
+
| GET | `/api/v1/payment/coins` | `GET /payment/coins` | 1h | — |
|
|
204
|
+
|
|
205
|
+
> **Auth ✅** = needs `MDL_USERNAME` + `MDL_PASSWORD` (see [🔐 Account](#-account)). Without them these return `400` with a clear message.
|
|
206
|
+
|
|
207
|
+
### Example responses (captured live)
|
|
208
|
+
|
|
209
|
+
**`GET /api/v1/genres`** → `200` · `Cache-Control: public, max-age=3600`
|
|
210
|
+
|
|
211
|
+
```json
|
|
212
|
+
[
|
|
213
|
+
{ "id": 1, "name": "Action", "slug": "action" },
|
|
214
|
+
{ "id": 5, "name": "Adventure", "slug": "adventure" },
|
|
215
|
+
{ "id": 13, "name": "Business", "slug": "business" }
|
|
216
|
+
]
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
**`GET /api/v1/people/6997`** → `200`
|
|
220
|
+
|
|
221
|
+
```json
|
|
222
|
+
{
|
|
223
|
+
"id": 6997,
|
|
224
|
+
"name": "Shin Myung Jin",
|
|
225
|
+
"first_name": "Myung Jin",
|
|
226
|
+
"family_name": "Shin",
|
|
227
|
+
"biography": "A South Korean actor. Starred in the movie December (2014).",
|
|
228
|
+
"permalink": "https://mydramalist.com/people/6997-shin-myung-jin",
|
|
229
|
+
"nationality": "South Korean",
|
|
230
|
+
"ranked": 99999,
|
|
231
|
+
"images": {
|
|
232
|
+
"thumb": "https://i.mydramalist.com/QDldg_5t.jpg",
|
|
233
|
+
"medium": "https://i.mydramalist.com/QDldg_5m.jpg",
|
|
234
|
+
"poster": "https://i.mydramalist.com/QDldg_5c.jpg"
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
**`GET /api/v1/titles/1/reviews`** → `200`
|
|
240
|
+
|
|
241
|
+
```json
|
|
242
|
+
[
|
|
243
|
+
{
|
|
244
|
+
"id": 3561,
|
|
245
|
+
"ratings": { "story": 10, "acting": 10, "music": 10, "rewatch": 10, "overall": 10 },
|
|
246
|
+
"headline": "As I watched through the whole...",
|
|
247
|
+
"upvotes": 0,
|
|
248
|
+
"total_votes": 0,
|
|
249
|
+
"spoiler": false,
|
|
250
|
+
"lang_iso": "en"
|
|
251
|
+
}
|
|
252
|
+
]
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
**`GET /api/v1/calendar`** → `200` — airing calendar
|
|
256
|
+
|
|
257
|
+
```json
|
|
258
|
+
{
|
|
259
|
+
"items": [
|
|
260
|
+
{
|
|
261
|
+
"id": 4349807, "rid": 695673, "episode_number": 19,
|
|
262
|
+
"released_at": 1788141600, "duration": 19,
|
|
263
|
+
"permalink": "/695673-wu-zuo-nu-fu-ma/episode/19"
|
|
264
|
+
}
|
|
265
|
+
],
|
|
266
|
+
"relationships": []
|
|
267
|
+
}
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
### Errors
|
|
271
|
+
|
|
272
|
+
| Case | Status | Body |
|
|
273
|
+
|---|---|---|
|
|
274
|
+
| Upstream 4xx (e.g. auth-gated) | `400`/`401`/`403` | `{"error": true, "code": <status>, "detail": <upstream body>}` |
|
|
275
|
+
| Upstream timeout | `504` | `{"error": true, "code": 504, "detail": "upstream timeout"}` |
|
|
276
|
+
| Upstream unreachable | `502` | `{"error": true, "code": 502, "detail": "upstream error: ..."}` |
|
|
277
|
+
| Invalid query param | `400` | `{"error": true, "code": 400, "detail": "period must be alltime\|weekly\|monthly"}` |
|
|
278
|
+
| Bad path / type | `404` / `422` | FastAPI default |
|
|
279
|
+
|
|
280
|
+
---
|
|
281
|
+
|
|
282
|
+
## ⚡ Performance
|
|
283
|
+
|
|
284
|
+
- **Async + connection pooling** — one `httpx.AsyncClient` reuses TCP/TLS connections.
|
|
285
|
+
- **TTL cache in-memory** — repeat requests never hit upstream. Measured:
|
|
286
|
+
- Cold (first hit): **~0.5 s**
|
|
287
|
+
- Warm (cached): **~1 ms**
|
|
288
|
+
- **`Cache-Control` header** on every response → CDN/proxy caching works too.
|
|
289
|
+
- **Outbound throttle** (2 concurrent, 0.5 s min interval) — keeps us under MDL's WAF rate limit.
|
|
290
|
+
|
|
291
|
+
> **Note:** the in-memory cache is single-process. Swap to a SQL backend or Redis if you run more than one instance.
|
|
292
|
+
|
|
293
|
+
---
|
|
294
|
+
|
|
295
|
+
## 💾 Response cache (DB)
|
|
296
|
+
|
|
297
|
+
By default `mdlaw` caches responses **in memory** (TTL). For persistence across restarts / multiple instances, switch to a **SQL database**:
|
|
298
|
+
|
|
299
|
+
| Env | Value | Notes |
|
|
300
|
+
|---|---|---|
|
|
301
|
+
| `MDL_CACHE_BACKEND` | `memory` (default) · `sqlite` · `mysql` · `postgres` | `memory` needs no setup |
|
|
302
|
+
| `MDL_CACHE_DB_URL` | e.g. `sqlite:///mdlaw_cache.db` · `mysql://user:pass@host/db` · `postgresql://user:pass@host/db` | required when backend ≠ memory |
|
|
303
|
+
|
|
304
|
+
```bash
|
|
305
|
+
# SQLite (zero setup — stdlib driver)
|
|
306
|
+
export MDL_CACHE_BACKEND=sqlite
|
|
307
|
+
export MDL_CACHE_DB_URL=sqlite:///mdlaw_cache.db
|
|
308
|
+
uvicorn mdlaw:app --port 8000
|
|
309
|
+
|
|
310
|
+
# MySQL
|
|
311
|
+
pip install pymysql
|
|
312
|
+
export MDL_CACHE_BACKEND=mysql MDL_CACHE_DB_URL=mysql://user:pass@localhost:3306/mdlaw
|
|
313
|
+
uvicorn mdlaw:app --port 8000
|
|
314
|
+
|
|
315
|
+
# PostgreSQL
|
|
316
|
+
pip install "psycopg[binary]"
|
|
317
|
+
export MDL_CACHE_BACKEND=postgres MDL_CACHE_DB_URL=postgresql://user:pass@localhost:5432/mdlaw
|
|
318
|
+
uvicorn mdlaw:app --port 8000
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
The cache table (`mdlaw_cache`) stores each response as JSON + a **sha256 hash**.
|
|
322
|
+
|
|
323
|
+
### 🔄 How response changes are detected
|
|
324
|
+
|
|
325
|
+
MDL doesn't push changes — so `mdlaw` uses the standard **TTL + hash comparison** pattern:
|
|
326
|
+
|
|
327
|
+
1. Response is stored with its sha256 hash.
|
|
328
|
+
2. When the TTL expires, the upstream is re-fetched.
|
|
329
|
+
3. The new hash is compared to the stored one:
|
|
330
|
+
- **Different** → the row is updated and flagged `changed=1` (with `updated_at`).
|
|
331
|
+
- **Same** → the row is refreshed (TTL extended), `changed=0`.
|
|
332
|
+
4. So you always know *when a response actually changed* — not just when it was re-fetched.
|
|
333
|
+
|
|
334
|
+
Inspect live:
|
|
335
|
+
|
|
336
|
+
```bash
|
|
337
|
+
curl http://localhost:8000/api/v1/cache/stats
|
|
338
|
+
# {"backend": "sqlite", "hits": 1, "misses": 1, "hit_rate": 0.5, "entries": 1}
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
> **Note:** change detection is only meaningful for a persistent backend (sqlite/mysql/postgres). In-memory cache resets on restart, so nothing to compare against.
|
|
342
|
+
|
|
343
|
+
---
|
|
344
|
+
|
|
345
|
+
## ⚙️ Configuration requirements
|
|
346
|
+
|
|
347
|
+
Everything `mdlaw` needs is set via environment variables. Here's the full picture at a glance:
|
|
348
|
+
|
|
349
|
+
| Variable | Required? | What it does | Default |
|
|
350
|
+
|---|---|---|---|
|
|
351
|
+
| `MDL_API_KEY` | **Yes** | The MDL app API key (not in source — you provide it) | — |
|
|
352
|
+
| `MDL_USERNAME` | No | Enables auth-gated endpoints (title detail, search, watchlist, `/me`) | disabled |
|
|
353
|
+
| `MDL_PASSWORD` | No | Password for the account above (paired with `MDL_USERNAME`) | disabled |
|
|
354
|
+
| `MDL_CACHE_BACKEND` | No | `memory` · `sqlite` · `mysql` · `postgres` | `memory` |
|
|
355
|
+
| `MDL_CACHE_DB_URL` | Only if backend ≠ memory | DSN, e.g. `sqlite:///mdlaw_cache.db` | — |
|
|
356
|
+
|
|
357
|
+
### 🔐 API key (required)
|
|
358
|
+
|
|
359
|
+
The MDL Android app talks to `app-api.mydramalist.com` with a hardcoded
|
|
360
|
+
`mdl-api-key` header. This public build does **not** embed that key — you must
|
|
361
|
+
provide it yourself (extract from the APK, or get it from a collaborator
|
|
362
|
+
who has access to the app key). The server **refuses to start** without `MDL_API_KEY`:
|
|
363
|
+
|
|
364
|
+
```bash
|
|
365
|
+
export MDL_API_KEY=<your-key>
|
|
366
|
+
uvicorn mdlaw:app --port 8000
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
### 👤 Account (optional)
|
|
370
|
+
|
|
371
|
+
Set both to unlock auth-gated endpoints:
|
|
372
|
+
|
|
373
|
+
```bash
|
|
374
|
+
export MDL_USERNAME=your_username
|
|
375
|
+
export MDL_PASSWORD=your_password
|
|
376
|
+
uvicorn mdlaw:app --port 8000
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
Without them, auth-gated endpoints return a clear `400` ("requires MDL_USERNAME and MDL_PASSWORD env vars").
|
|
380
|
+
|
|
381
|
+
### 💾 Cache (optional)
|
|
382
|
+
|
|
383
|
+
```bash
|
|
384
|
+
# default (in-memory) — no setup
|
|
385
|
+
uvicorn mdlaw:app --port 8000
|
|
386
|
+
|
|
387
|
+
# persistent (SQLite, zero dependencies)
|
|
388
|
+
export MDL_CACHE_BACKEND=sqlite MDL_CACHE_DB_URL=sqlite:///mdlaw_cache.db
|
|
389
|
+
uvicorn mdlaw:app --port 8000
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
---
|
|
393
|
+
|
|
394
|
+
## 🔐 Account (auth) — how it works
|
|
395
|
+
|
|
396
|
+
1. `POST /auth/login?device_id=<uuid>` with `{username, password: md5(password)}` → `{token, refresh_token, user}`.
|
|
397
|
+
2. Token is a JWT; `exp` is decoded to know when to refresh.
|
|
398
|
+
3. On `401` (or `invalid_grant`), `POST /auth/refresh` is called; if that fails, it re-logs in.
|
|
399
|
+
|
|
400
|
+
Check status:
|
|
401
|
+
|
|
402
|
+
```bash
|
|
403
|
+
curl http://localhost:8000/api/v1/auth/status
|
|
404
|
+
# {"configured": true, "logged_in": true, "user": {...}, "expires_at": ..., "refreshes": 0, ...}
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
> ⚠️ **2FA is not supported.** If your account has 2FA enabled, either disable it or use an app password. The wrapper will return `428` ("2FA required") instead of hanging.
|
|
408
|
+
|
|
409
|
+
---
|
|
410
|
+
|
|
411
|
+
## 🚢 Deploy
|
|
412
|
+
|
|
413
|
+
All options below. This is the **public** build: `MDL_API_KEY` is **required**
|
|
414
|
+
everywhere — the app refuses to start without it.
|
|
415
|
+
|
|
416
|
+
| Env | Public build |
|
|
417
|
+
|---|---|
|
|
418
|
+
| `MDL_API_KEY` | **required** |
|
|
419
|
+
| `MDL_USERNAME` + `MDL_PASSWORD` | optional (auth-gated endpoints) |
|
|
420
|
+
| `MDL_CACHE_BACKEND` + `MDL_CACHE_DB_URL` | optional (persistent cache) |
|
|
421
|
+
|
|
422
|
+
### Option A — Fly.io (recommended)
|
|
423
|
+
|
|
424
|
+
```bash
|
|
425
|
+
# 1. One-time: create the app (reads fly.toml)
|
|
426
|
+
fly launch --no-deploy
|
|
427
|
+
|
|
428
|
+
# 2. Set secrets (MDL_API_KEY REQUIRED)
|
|
429
|
+
fly secrets set MDL_API_KEY=<your-key>
|
|
430
|
+
fly secrets set MDL_USERNAME=<your-user> MDL_PASSWORD=<your-pass> # optional
|
|
431
|
+
fly secrets set MDL_CACHE_BACKEND=sqlite MDL_CACHE_DB_URL=sqlite:////data/mdlaw_cache.db # optional
|
|
432
|
+
|
|
433
|
+
# 3. Deploy
|
|
434
|
+
fly deploy
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
`fly.toml` is preconfigured: port 8000, region `sin`, **1 machine always
|
|
438
|
+
running** (no cold starts), HTTPS forced.
|
|
439
|
+
|
|
440
|
+
> **Persistent cache on Fly.io**: the default in-memory cache resets on every
|
|
441
|
+
> deploy/restart. Use the SQLite backend with a volume if you want it to
|
|
442
|
+
> survive restarts:
|
|
443
|
+
>
|
|
444
|
+
> ```bash
|
|
445
|
+
> fly volumes create mdlaw_data --size 1
|
|
446
|
+
> fly secrets set MDL_CACHE_BACKEND=sqlite MDL_CACHE_DB_URL=sqlite:////data/mdlaw_cache.db
|
|
447
|
+
> # then add to fly.toml:
|
|
448
|
+
> # [mounts]
|
|
449
|
+
> # source = "mdlaw_data"
|
|
450
|
+
> # destination = "/data"
|
|
451
|
+
> fly deploy
|
|
452
|
+
> ```
|
|
453
|
+
|
|
454
|
+
### Option B — Vercel (serverless)
|
|
455
|
+
|
|
456
|
+
Works, **but know the trade-offs**:
|
|
457
|
+
|
|
458
|
+
```bash
|
|
459
|
+
npx vercel
|
|
460
|
+
# when prompted, set env vars in the Vercel dashboard:
|
|
461
|
+
# Settings → Environment Variables → add MDL_API_KEY (REQUIRED),
|
|
462
|
+
# MDL_USERNAME, MDL_PASSWORD, MDL_CACHE_BACKEND, MDL_CACHE_DB_URL
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
Two files are already included (`api/index.py` + `vercel.json`). Caveats:
|
|
466
|
+
|
|
467
|
+
- **Cold starts**: Python serverless functions boot per request — first hit after idle is slow (~2–5 s).
|
|
468
|
+
- **Cache is per-instance & ephemeral**: the TTL cache resets between cold starts, so MDL's WAF sees more upstream traffic than on a persistent host. Still throttled (2 concurrent, 0.5 s interval), so it holds up, but it's not "blazing" on first hits.
|
|
469
|
+
- **SQL cache not recommended on Vercel**: serverless has no persistent filesystem — use an external MySQL/Postgres if you want persistence.
|
|
470
|
+
- Fine for a demo / low-traffic personal API. For consistent speed, use **Fly.io** (persistent instance, warm cache).
|
|
471
|
+
|
|
472
|
+
### Option C — Docker
|
|
473
|
+
|
|
474
|
+
```bash
|
|
475
|
+
docker build -t mdlaw .
|
|
476
|
+
|
|
477
|
+
# MDL_API_KEY REQUIRED
|
|
478
|
+
docker run -p 8000:8000 \
|
|
479
|
+
-e MDL_API_KEY=<your-key> \
|
|
480
|
+
-e MDL_USERNAME=<your-user> -e MDL_PASSWORD=<your-pass> \
|
|
481
|
+
-e MDL_CACHE_BACKEND=sqlite -e MDL_CACHE_DB_URL=sqlite:////data/mdlaw_cache.db \
|
|
482
|
+
-v mdlaw_data:/data \
|
|
483
|
+
mdlaw
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
### Option D — Docker Compose (production)
|
|
487
|
+
|
|
488
|
+
```yaml
|
|
489
|
+
# docker-compose.yml
|
|
490
|
+
services:
|
|
491
|
+
mdlaw:
|
|
492
|
+
build: .
|
|
493
|
+
ports:
|
|
494
|
+
- "8000:8000"
|
|
495
|
+
environment:
|
|
496
|
+
MDL_API_KEY: ${MDL_API_KEY} # required
|
|
497
|
+
MDL_USERNAME: ${MDL_USERNAME} # optional
|
|
498
|
+
MDL_PASSWORD: ${MDL_PASSWORD} # optional
|
|
499
|
+
MDL_CACHE_BACKEND: ${MDL_CACHE_BACKEND:-memory}
|
|
500
|
+
MDL_CACHE_DB_URL: ${MDL_CACHE_DB_URL:-}
|
|
501
|
+
volumes:
|
|
502
|
+
- mdlaw_data:/data
|
|
503
|
+
restart: unless-stopped
|
|
504
|
+
|
|
505
|
+
volumes:
|
|
506
|
+
mdlaw_data:
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
```bash
|
|
510
|
+
# .env (same folder, gitignored)
|
|
511
|
+
MDL_API_KEY=your-key
|
|
512
|
+
MDL_USERNAME=your-user
|
|
513
|
+
MDL_PASSWORD=your-pass
|
|
514
|
+
MDL_CACHE_BACKEND=sqlite
|
|
515
|
+
MDL_CACHE_DB_URL=sqlite:////data/mdlaw_cache.db
|
|
516
|
+
|
|
517
|
+
docker compose up -d
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
### Option E — VPS / systemd
|
|
521
|
+
|
|
522
|
+
Any host that can run Python 3.12.
|
|
523
|
+
|
|
524
|
+
```bash
|
|
525
|
+
# /etc/systemd/system/mdlaw.service
|
|
526
|
+
[Unit]
|
|
527
|
+
Description=mdlaw API
|
|
528
|
+
After=network.target
|
|
529
|
+
|
|
530
|
+
[Service]
|
|
531
|
+
WorkingDirectory=/opt/mdlaw
|
|
532
|
+
ExecStart=/opt/mdlaw/.venv/bin/uvicorn mdlaw:app --host 0.0.0.0 --port 8000
|
|
533
|
+
EnvironmentFile=/opt/mdlaw/.env
|
|
534
|
+
Restart=always
|
|
535
|
+
User=mdlaw
|
|
536
|
+
|
|
537
|
+
[Install]
|
|
538
|
+
WantedBy=multi-user.target
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
```bash
|
|
542
|
+
sudo systemctl daemon-reload
|
|
543
|
+
sudo systemctl enable --now mdlaw
|
|
544
|
+
# logs: journalctl -u mdlaw -f
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
---
|
|
548
|
+
|
|
549
|
+
## 🧪 Tests
|
|
550
|
+
|
|
551
|
+
Offline — no network needed:
|
|
552
|
+
|
|
553
|
+
```bash
|
|
554
|
+
python -m pytest tests/ -q
|
|
555
|
+
# 8 passed
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
Covers: key decode, md5 formula, header scheme, TTL cache expiry, SQL cache change detection, route count, auth-offline, and leaderboard validation.
|
|
559
|
+
|
|
560
|
+
---
|
|
561
|
+
|
|
562
|
+
## 📦 Project layout
|
|
563
|
+
|
|
564
|
+
```
|
|
565
|
+
mdlaw/
|
|
566
|
+
├── mdlaw.py # the whole API: config, client, cache (memory/SQL), auth, 27 routes
|
|
567
|
+
├── pyproject.toml # PyPI packaging (pip install mdlaw → console script)
|
|
568
|
+
├── LICENSE # MIT
|
|
569
|
+
├── AGENTS.md # AI-agent / dev instructions (install, verify, extend, pitfalls)
|
|
570
|
+
├── CHANGELOG.md # version history (Keep a Changelog convention)
|
|
571
|
+
├── api/index.py # Vercel serverless entrypoint
|
|
572
|
+
├── vercel.json # Vercel config (rewrites, function limits)
|
|
573
|
+
├── requirements.txt # fastapi, uvicorn[standard], httpx
|
|
574
|
+
├── Dockerfile # python:3.12-slim, non-root
|
|
575
|
+
├── docker-compose.yml # one-command production deploy (with healthcheck)
|
|
576
|
+
├── fly.toml # Fly.io config (port 8000, region sin, always-on)
|
|
577
|
+
├── pytest.ini # pythonpath for tests
|
|
578
|
+
├── .env.example # API key + account + cache backend placeholders
|
|
579
|
+
├── tests/
|
|
580
|
+
│ └── test_mdlaw.py # 8 offline checks
|
|
581
|
+
└── README.md
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
---
|
|
585
|
+
|
|
586
|
+
## ⚠️ Known limits
|
|
587
|
+
|
|
588
|
+
- **2FA accounts** are not supported by the auto-login (returns `428`). Disable 2FA or use an app password.
|
|
589
|
+
- **Anonymous guest mode does not exist** — MDL's Firebase project has anonymous auth disabled. Account features need `MDL_USERNAME`/`MDL_PASSWORD`.
|
|
590
|
+
- **One account per instance** — the wrapper holds a single token for all requests.
|
|
591
|
+
- **Rate limits**: the MDL WAF soft-blocks bursts. `mdlaw` throttles outbound, but don't point heavy crawlers at it.
|
|
592
|
+
- **Version drift**: if MDL updates the app's header scheme / key, endpoints may start returning 403. Update `mdlaw.py` constants (single place).
|
|
593
|
+
|
|
594
|
+
---
|
|
595
|
+
|
|
596
|
+
## 🛠 Development
|
|
597
|
+
|
|
598
|
+
```bash
|
|
599
|
+
# self-check (offline)
|
|
600
|
+
python mdlaw.py self
|
|
601
|
+
|
|
602
|
+
# run dev server with auto-reload
|
|
603
|
+
uvicorn mdlaw:app --reload --port 8000
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
---
|
|
607
|
+
|
|
608
|
+
## 📜 License & disclaimer
|
|
609
|
+
|
|
610
|
+
Unofficial, for research/education. Not affiliated with MyDramaList. All data © MyDramaList. Use at your own risk; stop if an official API ships.
|