kuramanime-api 1.0.0
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.
- package/LICENSE +21 -0
- package/README.md +263 -0
- package/api/index.js +19 -0
- package/bin/cli.js +63 -0
- package/data/sources.json +10 -0
- package/package.json +54 -0
- package/src/config/index.js +19 -0
- package/src/config/sources.js +83 -0
- package/src/db/index.js +93 -0
- package/src/index.js +97 -0
- package/src/routes/anime.js +260 -0
- package/src/routes/status.js +100 -0
- package/src/scrapers/browser.js +136 -0
- package/src/scrapers/cheerio.js +256 -0
- package/src/services/browserFarm.js +106 -0
- package/src/services/sourceUpdater.js +125 -0
- package/vercel.json +21 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 mmoaa
|
|
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.
|
package/README.md
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
# Kuramanime API
|
|
2
|
+
|
|
3
|
+
REST API untuk scraping data anime dari Kuramanime — portal streaming anime sub Indo.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
# Coba langsung via curl — no install!
|
|
7
|
+
curl https://musician-optics-mental-carey.trycloudflare.com/api/anime?page=1
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
## 🎬 Demo
|
|
11
|
+
|
|
12
|
+
https://github.com/user-attachments/assets/files/kuramanime-api/docs/demo.mp4
|
|
13
|
+
|
|
14
|
+
[▶️ **Tonton demo**](docs/demo.mp4) — 26 detik showcase semua endpoint live via curl.
|
|
15
|
+
|
|
16
|
+
## ⚡ Quick Start
|
|
17
|
+
|
|
18
|
+
### Opsi 1: NPM Global Install
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install -g kuramanime-api
|
|
22
|
+
kuramanime-api
|
|
23
|
+
# → http://localhost:3000
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### Opsi 2: Self-host (clone repo)
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
git clone https://github.com/mocasus/kuramanime-api.git
|
|
30
|
+
cd kuramanime-api
|
|
31
|
+
npm install
|
|
32
|
+
npm start
|
|
33
|
+
# → http://localhost:3000
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Opsi 3: Vercel Deploy 🚀
|
|
37
|
+
|
|
38
|
+
[](https://vercel.com/new/clone?repository-url=https://github.com/mocasus/kuramanime-api)
|
|
39
|
+
|
|
40
|
+
1-click deploy — gratis, auto-scale, langsung dapat endpoint publik. Set env `BROWSER_FARM_URL` + `BROWSER_FARM_KEY`.
|
|
41
|
+
|
|
42
|
+
### Opsi 4: Pakai cURL (no install, langsung coba)
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
# List anime terbaru
|
|
46
|
+
curl "https://musician-optics-mental-carey.trycloudflare.com/api/anime?page=1"
|
|
47
|
+
|
|
48
|
+
# Search anime
|
|
49
|
+
curl "https://musician-optics-mental-carey.trycloudflare.com/api/search?q=naruto"
|
|
50
|
+
|
|
51
|
+
# Detail anime
|
|
52
|
+
curl "https://musician-optics-mental-carey.trycloudflare.com/api/anime/fuguushoku-kanteishi-ga-jitsu-wa-saikyou-datta"
|
|
53
|
+
|
|
54
|
+
# Episode + server streaming
|
|
55
|
+
curl "https://musician-optics-mental-carey.trycloudflare.com/api/anime/fuguushoku-kanteishi-ga-jitsu-wa-saikyou-datta/1"
|
|
56
|
+
|
|
57
|
+
# Cek status source
|
|
58
|
+
curl "https://musician-optics-mental-carey.trycloudflare.com/api/status"
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
> **Demo URL:** `musician-optics-mental-carey.trycloudflare.com` _(Cloudflare Tunnel — uptime tidak dijamin. Untuk production, self-host!)_
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
### Opsi 5: Docker
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
docker compose up -d
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Endpoints API
|
|
71
|
+
|
|
72
|
+
| Method | Endpoint | Deskripsi |
|
|
73
|
+
|--------|----------|-----------|
|
|
74
|
+
| `GET` | `/api/anime?page=1` | List anime terbaru (5/page) |
|
|
75
|
+
| `GET` | `/api/anime/:slug` | Detail anime + daftar episode |
|
|
76
|
+
| `GET` | `/api/anime/:slug/:episode` | Server streaming + video URL |
|
|
77
|
+
| `GET` | `/api/search?q=judul` | Search anime (min 2 karakter) |
|
|
78
|
+
| `GET` | `/api/status` | Cek status source (alive/down) |
|
|
79
|
+
| `GET` | `/api/sources` | List semua URL source aktif |
|
|
80
|
+
| `GET` | `/api/health` | Server health check |
|
|
81
|
+
| `POST` | `/api/sources/check` | Trigger manual source update |
|
|
82
|
+
| `POST` | `/api/cache/clear` | Clear semua cache |
|
|
83
|
+
|
|
84
|
+
### Contoh Response
|
|
85
|
+
|
|
86
|
+
<details>
|
|
87
|
+
<summary><b>GET /api/anime?page=1</b></summary>
|
|
88
|
+
|
|
89
|
+
```json
|
|
90
|
+
{
|
|
91
|
+
"success": true,
|
|
92
|
+
"page": 1,
|
|
93
|
+
"count": 5,
|
|
94
|
+
"data": [{
|
|
95
|
+
"id": 5000,
|
|
96
|
+
"slug": "super-no-ura-de-yani-suu-futari",
|
|
97
|
+
"title": "Super no Ura de Yani Suu Futari",
|
|
98
|
+
"image": "https://r2.nyomo.my.id/images/...",
|
|
99
|
+
"quality": "Loading...",
|
|
100
|
+
"score": "8.06"
|
|
101
|
+
}]
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
</details>
|
|
105
|
+
|
|
106
|
+
<details>
|
|
107
|
+
<summary><b>GET /api/search?q=one</b></summary>
|
|
108
|
+
|
|
109
|
+
```json
|
|
110
|
+
{
|
|
111
|
+
"success": true,
|
|
112
|
+
"query": "one",
|
|
113
|
+
"count": 18,
|
|
114
|
+
"data": [{
|
|
115
|
+
"id": 4508,
|
|
116
|
+
"slug": "omae-gotoki-ga-maou-ni-kateru-to-omouna...",
|
|
117
|
+
"title": "Omae Gotoki ga Maou ni Kateru to Omouna...",
|
|
118
|
+
"altTitles": "Winter 2026 • TV • HD"
|
|
119
|
+
}]
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
</details>
|
|
123
|
+
|
|
124
|
+
<details>
|
|
125
|
+
<summary><b>GET /api/anime/:slug (detail)</b></summary>
|
|
126
|
+
|
|
127
|
+
```json
|
|
128
|
+
{
|
|
129
|
+
"success": true,
|
|
130
|
+
"data": {
|
|
131
|
+
"id": 3380,
|
|
132
|
+
"title": "Fuguushoku \"Kanteishi\" ga Jitsu wa Saikyou Datta",
|
|
133
|
+
"synopsis": "Di dunia fantasi...",
|
|
134
|
+
"genres": ["Action", "Adventure", "Fantasy"],
|
|
135
|
+
"score": "6.34",
|
|
136
|
+
"metadata": {
|
|
137
|
+
"tipe": "TV",
|
|
138
|
+
"episode": "12",
|
|
139
|
+
"studio": "Okuruto Noboru"
|
|
140
|
+
},
|
|
141
|
+
"episodes": [
|
|
142
|
+
{ "number": 1, "url": "..." }
|
|
143
|
+
]
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
</details>
|
|
148
|
+
|
|
149
|
+
<details>
|
|
150
|
+
<summary><b>GET /api/anime/:slug/:episode</b></summary>
|
|
151
|
+
|
|
152
|
+
```json
|
|
153
|
+
{
|
|
154
|
+
"success": true,
|
|
155
|
+
"data": {
|
|
156
|
+
"episode": 1,
|
|
157
|
+
"servers": [
|
|
158
|
+
{ "id": "kuramadrive", "label": "Kuramadrive s1" },
|
|
159
|
+
{ "id": "mega", "label": "MEGA" }
|
|
160
|
+
],
|
|
161
|
+
"selectedServer": "kuramadrive"
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
</details>
|
|
166
|
+
|
|
167
|
+
## Fitur
|
|
168
|
+
|
|
169
|
+
- **9 REST Endpoints** — list, detail, episode, search, status, sources, health, source check, cache clear
|
|
170
|
+
- **Auto-fallback** — source utama timeout/403 → auto coba alternatif (v17, v18, v19...)
|
|
171
|
+
- **Auto-update Source** — tiap 6 jam cek + rotate URL source secara otomatis
|
|
172
|
+
- **Quicksearch** — pakai AJAX endpoint untuk hasil search instan & akurat
|
|
173
|
+
- **SQLite Cache** — cache response per endpoint (30m–2h TTL)
|
|
174
|
+
- **Rate Limiting** — 60 req/menit general, 20 search, 30 episode
|
|
175
|
+
- **Browser Farm** — ekstrak streaming URL via headless Playwright
|
|
176
|
+
- **Docker Ready** — Dockerfile + docker-compose.yml
|
|
177
|
+
|
|
178
|
+
## Konfigurasi
|
|
179
|
+
|
|
180
|
+
Copy `.env.example` → `.env`:
|
|
181
|
+
|
|
182
|
+
| Variable | Default | Deskripsi |
|
|
183
|
+
|----------|---------|-----------|
|
|
184
|
+
| `PORT` | `3000` | Port server |
|
|
185
|
+
| `CACHE_TTL_ANIME_LIST` | `1800` | Cache TTL list (detik) |
|
|
186
|
+
| `CACHE_TTL_ANIME_DETAIL` | `3600` | Cache TTL detail |
|
|
187
|
+
| `CACHE_TTL_EPISODE` | `7200` | Cache TTL episode |
|
|
188
|
+
| `BROWSER_FARM_URL` | - | URL Browser Farm service |
|
|
189
|
+
| `BROWSER_FARM_KEY` | - | API key Browser Farm |
|
|
190
|
+
| `SOURCE_AUTO_UPDATE_INTERVAL` | `6` | Interval cek source (jam) |
|
|
191
|
+
|
|
192
|
+
## Auto-Fallback & Auto-Update
|
|
193
|
+
|
|
194
|
+
Sistem otomatis:
|
|
195
|
+
1. Coba URL utama → alternatif → derived URLs (v17, v18, v19...)
|
|
196
|
+
2. Tiap 6 jam: cek health semua URL
|
|
197
|
+
3. Primary down → rotate ke alternatif yang hidup
|
|
198
|
+
4. Update `data/sources.json` otomatis
|
|
199
|
+
5. Semua log ke SQLite
|
|
200
|
+
|
|
201
|
+
Manual trigger: `POST /api/sources/check`
|
|
202
|
+
|
|
203
|
+
## Cache Strategy
|
|
204
|
+
|
|
205
|
+
| Endpoint | TTL |
|
|
206
|
+
|----------|-----|
|
|
207
|
+
| Anime list | 30 menit |
|
|
208
|
+
| Anime detail | 1 jam |
|
|
209
|
+
| Episode stream | 2 jam |
|
|
210
|
+
| Source status | 5 menit |
|
|
211
|
+
| Search | 30 menit |
|
|
212
|
+
|
|
213
|
+
Clear: `POST /api/cache/clear`
|
|
214
|
+
|
|
215
|
+
## Struktur Project
|
|
216
|
+
|
|
217
|
+
```
|
|
218
|
+
kuramanime-api/
|
|
219
|
+
├── src/
|
|
220
|
+
│ ├── index.js # Express app + middleware
|
|
221
|
+
│ ├── config/
|
|
222
|
+
│ │ ├── index.js # Env config
|
|
223
|
+
│ │ └── sources.js # URL source manager
|
|
224
|
+
│ ├── routes/
|
|
225
|
+
│ │ ├── anime.js # Anime endpoints
|
|
226
|
+
│ │ └── status.js # Status, sources, cache
|
|
227
|
+
│ ├── scrapers/
|
|
228
|
+
│ │ ├── cheerio.js # Static scraper (list, detail, search)
|
|
229
|
+
│ │ └── browser.js # Browser scraper (streaming URL)
|
|
230
|
+
│ ├── services/
|
|
231
|
+
│ │ ├── browserFarm.js # Browser Farm REST client
|
|
232
|
+
│ │ └── sourceUpdater.js # Auto source rotate
|
|
233
|
+
│ └── db/
|
|
234
|
+
│ └── index.js # SQLite (cache + source log)
|
|
235
|
+
├── data/
|
|
236
|
+
│ └── sources.json # URL source (runtime)
|
|
237
|
+
├── Dockerfile
|
|
238
|
+
├── docker-compose.yml
|
|
239
|
+
└── README.md
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
## Streaming Servers
|
|
243
|
+
|
|
244
|
+
| Server ID | Deskripsi |
|
|
245
|
+
|-----------|-----------|
|
|
246
|
+
| `kuramadrive` | Default (normal, iklan banner) |
|
|
247
|
+
| `filelions` | FileLions (kencang, iklan popup) |
|
|
248
|
+
| `filemoon` | FileMoon (kencang, iklan popup) |
|
|
249
|
+
| `mega` | MEGA (kencang, iklan banner) |
|
|
250
|
+
| `streamwish` | StreamWish (kencang, iklan popup) |
|
|
251
|
+
| `vidguard` | VidGuard (kencang, iklan popup) |
|
|
252
|
+
|
|
253
|
+
## Tech Stack
|
|
254
|
+
|
|
255
|
+
- **Node.js 20+** + **Express** — REST framework
|
|
256
|
+
- **Cheerio** — HTML parsing (list, detail, search)
|
|
257
|
+
- **Browser Farm** (Playwright) — dynamic JS scraping
|
|
258
|
+
- **better-sqlite3** — SQLite database
|
|
259
|
+
- **express-rate-limit** — rate limiting
|
|
260
|
+
|
|
261
|
+
## License
|
|
262
|
+
|
|
263
|
+
MIT © 2026 mmoaa
|
package/api/index.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vercel Serverless Entry Point
|
|
3
|
+
*
|
|
4
|
+
* Adaptasi Express app untuk Vercel serverless:
|
|
5
|
+
* - SQLite disabled (filesystem ephemeral)
|
|
6
|
+
* - node-cron disabled (no long-running process)
|
|
7
|
+
* - Rate limiting stays (per-instance, acceptable for low-traffic)
|
|
8
|
+
* - Source URLs loaded from data/sources.json (static)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// Override before requiring modules
|
|
12
|
+
process.env.VERCEL = '1';
|
|
13
|
+
process.env.NODE_ENV = 'production';
|
|
14
|
+
process.env.DISABLE_SQLITE = '1'; // Signal to db module
|
|
15
|
+
|
|
16
|
+
const app = require('../src/index');
|
|
17
|
+
|
|
18
|
+
// Vercel serverless export
|
|
19
|
+
module.exports = app;
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Kuramanime API CLI — one command to start the server.
|
|
4
|
+
* Usage:
|
|
5
|
+
* kuramanime-api → start server (port from PORT env or 3000)
|
|
6
|
+
* kuramanime-api --port 8080 → start on custom port
|
|
7
|
+
* kuramanime-api --help → show help
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const { spawn } = require('child_process');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
|
|
14
|
+
const args = process.argv.slice(2);
|
|
15
|
+
const help = args.includes('--help') || args.includes('-h');
|
|
16
|
+
|
|
17
|
+
if (help) {
|
|
18
|
+
console.log(`
|
|
19
|
+
📦 Kuramanime API v${require('../package.json').version}
|
|
20
|
+
|
|
21
|
+
kuramanime-api Start server on PORT env or 3000
|
|
22
|
+
kuramanime-api --port 8080 Start on custom port
|
|
23
|
+
kuramanime-api --help Show this help
|
|
24
|
+
|
|
25
|
+
Environment:
|
|
26
|
+
PORT=3000 Server port
|
|
27
|
+
BROWSER_FARM_URL Browser Farm endpoint
|
|
28
|
+
BROWSER_FARM_KEY Browser Farm API key
|
|
29
|
+
|
|
30
|
+
Deploy to Vercel:
|
|
31
|
+
Just push to GitHub and connect to Vercel — vercel.json is included.
|
|
32
|
+
`);
|
|
33
|
+
process.exit(0);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Parse --port
|
|
37
|
+
let port = process.env.PORT || 3000;
|
|
38
|
+
for (let i = 0; i < args.length; i++) {
|
|
39
|
+
if (args[i] === '--port' && args[i + 1]) {
|
|
40
|
+
port = args[i + 1];
|
|
41
|
+
i++;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Ensure data directory
|
|
46
|
+
const dataDir = path.join(__dirname, '..', 'data');
|
|
47
|
+
if (!fs.existsSync(dataDir)) {
|
|
48
|
+
fs.mkdirSync(dataDir, { recursive: true });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Start the server (inherit stdio so logs show)
|
|
52
|
+
console.log(`🎬 Starting Kuramanime API on port ${port}...`);
|
|
53
|
+
const child = spawn('node', [path.join(__dirname, '..', 'src', 'index.js')], {
|
|
54
|
+
env: { ...process.env, PORT: port },
|
|
55
|
+
stdio: 'inherit',
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
child.on('close', (code) => {
|
|
59
|
+
process.exit(code);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
process.on('SIGINT', () => child.kill());
|
|
63
|
+
process.on('SIGTERM', () => child.kill());
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "kuramanime-api",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "REST API for scraping anime data from Kuramanime \u2014 portal streaming anime sub Indo",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"anime",
|
|
7
|
+
"kuramanime",
|
|
8
|
+
"sub-indo",
|
|
9
|
+
"scraper",
|
|
10
|
+
"rest-api",
|
|
11
|
+
"express"
|
|
12
|
+
],
|
|
13
|
+
"homepage": "https://github.com/mocasus/kuramanime-api#readme",
|
|
14
|
+
"bugs": "https://github.com/mocasus/kuramanime-api/issues",
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"author": "mmoaa",
|
|
17
|
+
"main": "src/index.js",
|
|
18
|
+
"bin": {
|
|
19
|
+
"kuramanime-api": "bin/cli.js"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"bin/",
|
|
23
|
+
"src/",
|
|
24
|
+
"api/",
|
|
25
|
+
"data/sources.json",
|
|
26
|
+
"vercel.json",
|
|
27
|
+
"README.md",
|
|
28
|
+
"LICENSE"
|
|
29
|
+
],
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "git+https://github.com/mocasus/kuramanime-api.git"
|
|
33
|
+
},
|
|
34
|
+
"scripts": {
|
|
35
|
+
"start": "node src/index.js",
|
|
36
|
+
"dev": "node --watch src/index.js",
|
|
37
|
+
"vercel:dev": "vercel dev",
|
|
38
|
+
"docker:build": "docker build -t kuramanime-api .",
|
|
39
|
+
"docker:run": "docker run -p 3000:3000 -v $(pwd)/data:/app/data kuramanime-api"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"better-sqlite3": "^11.0.0",
|
|
43
|
+
"cheerio": "^1.0.0",
|
|
44
|
+
"cors": "^2.8.5",
|
|
45
|
+
"dotenv": "^16.4.5",
|
|
46
|
+
"express": "^4.21.0",
|
|
47
|
+
"express-rate-limit": "^7.4.0",
|
|
48
|
+
"node-cron": "^3.0.3",
|
|
49
|
+
"uuid": "^10.0.0"
|
|
50
|
+
},
|
|
51
|
+
"engines": {
|
|
52
|
+
"node": ">=18.0.0"
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
require('dotenv').config({ path: require('path').join(__dirname, '..', '..', '.env') });
|
|
2
|
+
|
|
3
|
+
module.exports = {
|
|
4
|
+
port: parseInt(process.env.PORT || '3000', 10),
|
|
5
|
+
nodeEnv: process.env.NODE_ENV || 'development',
|
|
6
|
+
|
|
7
|
+
cache: {
|
|
8
|
+
animeList: parseInt(process.env.CACHE_TTL_ANIME_LIST || '1800', 10),
|
|
9
|
+
animeDetail: parseInt(process.env.CACHE_TTL_ANIME_DETAIL || '3600', 10),
|
|
10
|
+
episodeStream: parseInt(process.env.CACHE_TTL_EPISODE || '7200', 10),
|
|
11
|
+
},
|
|
12
|
+
|
|
13
|
+
browserFarm: {
|
|
14
|
+
baseUrl: process.env.BROWSER_FARM_URL || 'https://cool-flying-lifetime-dream.trycloudflare.com',
|
|
15
|
+
apiKey: process.env.BROWSER_FARM_KEY || 'fox-browser-farm-2026',
|
|
16
|
+
},
|
|
17
|
+
|
|
18
|
+
autoUpdateIntervalHours: parseInt(process.env.SOURCE_AUTO_UPDATE_INTERVAL || '6', 10),
|
|
19
|
+
};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
const SOURCES_FILE = path.join(__dirname, '..', '..', 'data', 'sources.json');
|
|
5
|
+
|
|
6
|
+
const DEFAULT_SOURCES = {
|
|
7
|
+
primary: 'https://v18.kuramanime.ing',
|
|
8
|
+
alternatives: [
|
|
9
|
+
'https://v17.kuramanime.ing',
|
|
10
|
+
'https://v19.kuramanime.ing',
|
|
11
|
+
'https://kuramanime.ing',
|
|
12
|
+
],
|
|
13
|
+
lastUpdated: null,
|
|
14
|
+
updatedBy: null,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
function loadSources() {
|
|
18
|
+
try {
|
|
19
|
+
const raw = fs.readFileSync(SOURCES_FILE, 'utf-8');
|
|
20
|
+
const parsed = JSON.parse(raw);
|
|
21
|
+
if (parsed.primary) return parsed;
|
|
22
|
+
} catch (_) { /* ignore, use defaults */ }
|
|
23
|
+
return { ...DEFAULT_SOURCES };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function saveSources(sources) {
|
|
27
|
+
fs.mkdirSync(path.dirname(SOURCES_FILE), { recursive: true });
|
|
28
|
+
fs.writeFileSync(SOURCES_FILE, JSON.stringify(sources, null, 2));
|
|
29
|
+
return sources;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function getAllUrls() {
|
|
33
|
+
const s = loadSources();
|
|
34
|
+
return [s.primary, ...s.alternatives].filter(Boolean);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function getPrimary() {
|
|
38
|
+
return loadSources().primary;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function getAll() {
|
|
42
|
+
return loadSources();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function setPrimary(url) {
|
|
46
|
+
const s = loadSources();
|
|
47
|
+
s.primary = url;
|
|
48
|
+
s.lastUpdated = new Date().toISOString();
|
|
49
|
+
s.updatedBy = 'auto-check';
|
|
50
|
+
return saveSources(s);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function addAlternative(url) {
|
|
54
|
+
const s = loadSources();
|
|
55
|
+
if (!s.alternatives.includes(url) && url !== s.primary) {
|
|
56
|
+
s.alternatives.push(url);
|
|
57
|
+
s.lastUpdated = new Date().toISOString();
|
|
58
|
+
s.updatedBy = 'auto-check';
|
|
59
|
+
return saveSources(s);
|
|
60
|
+
}
|
|
61
|
+
return s;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function validateUrl(url) {
|
|
65
|
+
try {
|
|
66
|
+
const u = new URL(url);
|
|
67
|
+
return u.protocol === 'https:' || u.protocol === 'http:';
|
|
68
|
+
} catch {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
module.exports = {
|
|
74
|
+
getAllUrls,
|
|
75
|
+
getPrimary,
|
|
76
|
+
getAll,
|
|
77
|
+
setPrimary,
|
|
78
|
+
addAlternative,
|
|
79
|
+
validateUrl,
|
|
80
|
+
loadSources,
|
|
81
|
+
saveSources,
|
|
82
|
+
DEFAULT_SOURCES,
|
|
83
|
+
};
|
package/src/db/index.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
|
|
4
|
+
const DB_PATH = path.join(__dirname, '..', '..', 'data', 'kuramanime.db');
|
|
5
|
+
const isDisabled = !!process.env.DISABLE_SQLITE;
|
|
6
|
+
|
|
7
|
+
let db;
|
|
8
|
+
|
|
9
|
+
function getDb() {
|
|
10
|
+
if (isDisabled) return null;
|
|
11
|
+
if (!db) {
|
|
12
|
+
const Database = require('better-sqlite3');
|
|
13
|
+
fs.mkdirSync(path.dirname(DB_PATH), { recursive: true });
|
|
14
|
+
db = new Database(DB_PATH);
|
|
15
|
+
db.pragma('journal_mode = WAL');
|
|
16
|
+
db.pragma('busy_timeout = 5000');
|
|
17
|
+
migrate(db);
|
|
18
|
+
}
|
|
19
|
+
return db;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function migrate(database) {
|
|
23
|
+
if (!database) return;
|
|
24
|
+
database.exec(`
|
|
25
|
+
CREATE TABLE IF NOT EXISTS cache (
|
|
26
|
+
key TEXT PRIMARY KEY,
|
|
27
|
+
value TEXT NOT NULL,
|
|
28
|
+
created_at INTEGER NOT NULL,
|
|
29
|
+
ttl INTEGER NOT NULL
|
|
30
|
+
);
|
|
31
|
+
CREATE INDEX IF NOT EXISTS idx_cache_created ON cache(created_at);
|
|
32
|
+
CREATE TABLE IF NOT EXISTS source_log (
|
|
33
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
34
|
+
url TEXT NOT NULL,
|
|
35
|
+
status TEXT NOT NULL,
|
|
36
|
+
checked_at INTEGER NOT NULL
|
|
37
|
+
);
|
|
38
|
+
CREATE INDEX IF NOT EXISTS idx_source_log_url ON source_log(url);
|
|
39
|
+
`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function cacheGet(key) {
|
|
43
|
+
const d = getDb();
|
|
44
|
+
if (!d) return null;
|
|
45
|
+
const row = d.prepare('SELECT value, created_at, ttl FROM cache WHERE key = ?').get(key);
|
|
46
|
+
if (!row) return null;
|
|
47
|
+
if (Date.now() - row.created_at > row.ttl * 1000) {
|
|
48
|
+
d.prepare('DELETE FROM cache WHERE key = ?').run(key);
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
return JSON.parse(row.value);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function cacheSet(key, value, ttlSeconds = 1800) {
|
|
55
|
+
const d = getDb();
|
|
56
|
+
if (!d) return;
|
|
57
|
+
d.prepare(
|
|
58
|
+
'INSERT OR REPLACE INTO cache (key, value, created_at, ttl) VALUES (?, ?, ?, ?)'
|
|
59
|
+
).run(key, JSON.stringify(value), Date.now(), ttlSeconds);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function cacheClear(pattern) {
|
|
63
|
+
const d = getDb();
|
|
64
|
+
if (!d) return;
|
|
65
|
+
if (pattern) {
|
|
66
|
+
d.prepare('DELETE FROM cache WHERE key LIKE ?').run(`%${pattern}%`);
|
|
67
|
+
} else {
|
|
68
|
+
d.prepare('DELETE FROM cache').run();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function logSource(url, status) {
|
|
73
|
+
const d = getDb();
|
|
74
|
+
if (!d) return;
|
|
75
|
+
d.prepare('INSERT INTO source_log (url, status, checked_at) VALUES (?, ?, ?)').run(
|
|
76
|
+
url, status, Date.now()
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function getSourceLogs(limit = 20) {
|
|
81
|
+
const d = getDb();
|
|
82
|
+
if (!d) return [];
|
|
83
|
+
return d.prepare('SELECT * FROM source_log ORDER BY checked_at DESC LIMIT ?').all(limit);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
module.exports = {
|
|
87
|
+
getDb,
|
|
88
|
+
cacheGet,
|
|
89
|
+
cacheSet,
|
|
90
|
+
cacheClear,
|
|
91
|
+
logSource,
|
|
92
|
+
getSourceLogs,
|
|
93
|
+
};
|