ani-auto 1.4.4 → 2.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.
Files changed (89) hide show
  1. package/anime/LICENSE +21 -0
  2. package/anime/api/index.js +110 -0
  3. package/anime/app.js +79 -0
  4. package/anime/controllers/animeInfoController.js +48 -0
  5. package/anime/controllers/animeListController.js +21 -0
  6. package/anime/controllers/homeController.js +42 -0
  7. package/anime/controllers/playController.js +140 -0
  8. package/anime/controllers/queueController.js +20 -0
  9. package/anime/controllers/testController.js +667 -0
  10. package/anime/index.js +139 -0
  11. package/anime/middleware/cache.js +64 -0
  12. package/anime/middleware/errorHandler.js +33 -0
  13. package/anime/middleware/rateLimiter.js +73 -0
  14. package/anime/models/animeInfoModel.js +193 -0
  15. package/anime/models/animeListModel.js +69 -0
  16. package/anime/models/homeModel.js +33 -0
  17. package/anime/models/playModel.js +528 -0
  18. package/anime/models/queueModel.js +22 -0
  19. package/anime/package.json +27 -0
  20. package/anime/pnpm-lock.yaml +1774 -0
  21. package/anime/readme.md +236 -0
  22. package/anime/routes/animeInfoRoutes.js +9 -0
  23. package/anime/routes/animeListRoutes.js +9 -0
  24. package/anime/routes/homeRoutes.js +10 -0
  25. package/anime/routes/playRoutes.js +11 -0
  26. package/anime/routes/queueRoutes.js +8 -0
  27. package/anime/routes/testRoutes.js +325 -0
  28. package/anime/routes/webhookRoutes.js +23 -0
  29. package/anime/scrapers/animepahe.js +1053 -0
  30. package/anime/test-server.js +10 -0
  31. package/anime/test.sh +275 -0
  32. package/anime/utils/browser.js +172 -0
  33. package/anime/utils/config.js +209 -0
  34. package/anime/utils/dataProcessor.js +172 -0
  35. package/anime/utils/diskCache.js +228 -0
  36. package/anime/utils/flaresolverr.js +361 -0
  37. package/anime/utils/jsParser.js +19 -0
  38. package/anime/utils/redis.js +64 -0
  39. package/anime/utils/requestManager.js +706 -0
  40. package/anime/utils/urlConverter.js +88 -0
  41. package/anime/utils/webhookNotifier.js +190 -0
  42. package/cookiereader.py +291 -0
  43. package/package.json +14 -6
  44. package/src/anilist.js +15 -2
  45. package/src/api/anilist.js +32 -0
  46. package/src/api/anime.js +181 -0
  47. package/src/api/cf-bypass.js +131 -0
  48. package/src/api/config.js +134 -0
  49. package/src/api/daemon.js +62 -0
  50. package/src/api/download.js +98 -0
  51. package/src/api/match.js +58 -0
  52. package/src/api/runs.js +15 -0
  53. package/src/api/update.js +96 -0
  54. package/src/cli.js +18 -2
  55. package/src/config.js +14 -2
  56. package/src/daemon.js +1 -1
  57. package/src/db.js +38 -11
  58. package/src/engine.js +62 -11
  59. package/src/scraper.js +2 -2
  60. package/src/server.js +108 -0
  61. package/utils.js +21 -4
  62. package/web/index.html +13 -0
  63. package/web/package-lock.json +1420 -0
  64. package/web/package.json +24 -0
  65. package/web/src/App.vue +200 -0
  66. package/web/src/api/client.js +75 -0
  67. package/web/src/assets/styles/base.css +961 -0
  68. package/web/src/components/UpdateModal.vue +157 -0
  69. package/web/src/components/sidebar/SidebarResizable.vue +170 -0
  70. package/web/src/components/sidebar/sidebarConfig.js +24 -0
  71. package/web/src/main.js +104 -0
  72. package/web/src/router/index.js +44 -0
  73. package/web/src/stores/config.js +27 -0
  74. package/web/src/stores/download.js +107 -0
  75. package/web/src/stores/update.js +73 -0
  76. package/web/src/views/About.vue +42 -0
  77. package/web/src/views/AniListSync.vue +122 -0
  78. package/web/src/views/AnimeDetail.vue +202 -0
  79. package/web/src/views/AnimeList.vue +110 -0
  80. package/web/src/views/CFResult.vue +312 -0
  81. package/web/src/views/DaemonControl.vue +83 -0
  82. package/web/src/views/Dashboard.vue +187 -0
  83. package/web/src/views/DownloadCenter.vue +153 -0
  84. package/web/src/views/MatchFinder.vue +131 -0
  85. package/web/src/views/OAuthCallback.vue +44 -0
  86. package/web/src/views/Onboarding.vue +93 -0
  87. package/web/src/views/RunHistory.vue +122 -0
  88. package/web/src/views/Settings.vue +410 -0
  89. package/web/vite.config.js +23 -0
@@ -0,0 +1,88 @@
1
+ const path = require('path');
2
+
3
+ class UrlConverter {
4
+ /**
5
+ * Converts a stream m3u8 URL to a direct MP4 download URL
6
+ * Input: https://exampledomain.top/stream/14/04/{hash}/uwu.m3u8
7
+ * Output: https://anotherexampledomain.cx/mp4/14/04/{hash}
8
+ *
9
+ * @param {string} m3u8Url - The original stream URL
10
+ * @param {string} kwikDomain - The kwik domain (e.g. 'kwik.cx')
11
+ * @returns {string|null} - The converted download URL or null if invalid
12
+ */
13
+ static getMp4Url(m3u8Url, kwikDomain) {
14
+ if (!m3u8Url || !m3u8Url.includes('/stream/')) return null;
15
+
16
+ try {
17
+ const urlObj = new URL(m3u8Url);
18
+
19
+ // Preserve the exact CDN host returned by AnimePahe.
20
+ // The site has been switching between hosts like uwucdn.top and
21
+ // owocdn.top, so forcing Config.iframeBaseUrl can produce bad links.
22
+ urlObj.pathname = urlObj.pathname.replace('/stream/', '/mp4/');
23
+
24
+ if (urlObj.pathname.endsWith('/uwu.m3u8')) {
25
+ urlObj.pathname = urlObj.pathname.replace('/uwu.m3u8', '');
26
+ } else if (urlObj.pathname.endsWith('.m3u8')) {
27
+ urlObj.pathname = urlObj.pathname.replace('.m3u8', '');
28
+ }
29
+
30
+ return urlObj.toString();
31
+ } catch (e) {
32
+ console.error('Error converting stream URL:', e);
33
+ return null;
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Generates a descriptive filename for the download
39
+ * Format: AnimePahe_{Anime_Title}_{Eng_Dub?}_-_{Episode}_{BD?}_{Resolution}_{Fansub}.mp4
40
+ */
41
+ static getFilename(animeTitle, episode, resolution, fansub, isDub, isBD) {
42
+ if (!animeTitle) return 'video.mp4';
43
+
44
+ // Sanitize title
45
+ const safeTitle = animeTitle.replace(/[^a-z0-9]/gi, '_').replace(/_+/g, '_');
46
+ const dubStr = isDub ? '_Eng_Dub' : '';
47
+ const bdStr = isBD ? '_BD' : '';
48
+ const resStr = resolution ? `_${resolution}p` : '';
49
+ const fansubStr = fansub ? `_${fansub}` : '';
50
+ const epStr = episode || '0';
51
+
52
+ return `AnimePahe_${safeTitle}${dubStr}_-_${epStr}${bdStr}${resStr}${fansubStr}.mp4`;
53
+ }
54
+
55
+ /**
56
+ * Builds the full download URL with filename parameter and CDN headers
57
+ * Returns an object with url and headers for proper CDN access
58
+ *
59
+ * @returns {Object} { url, headers } where headers contains Referer
60
+ */
61
+ static buildDownloadUrl(m3u8Url, kwikDomain, metadata) {
62
+ const mp4Url = this.getMp4Url(m3u8Url, kwikDomain);
63
+ if (!mp4Url) return null;
64
+
65
+ const filename = this.getFilename(
66
+ metadata.animeTitle,
67
+ metadata.episode,
68
+ metadata.resolution,
69
+ metadata.fansub,
70
+ metadata.isDub,
71
+ metadata.isBD
72
+ );
73
+
74
+ const downloadUrl = `${mp4Url}?file=${filename}`;
75
+
76
+ const headers = {
77
+ Referer: 'https://kwik.cx/',
78
+ Origin: 'https://kwik.cx',
79
+ };
80
+
81
+ return {
82
+ url: downloadUrl,
83
+ headers,
84
+ };
85
+ }
86
+ }
87
+
88
+ module.exports = UrlConverter;
@@ -0,0 +1,190 @@
1
+ const fs = require("fs").promises;
2
+ const path = require("path");
3
+ const axios = require("axios");
4
+ const HomeModel = require("../models/homeModel");
5
+ const PlayModel = require("../models/playModel");
6
+
7
+ const STATE_PATH = process.env.WEBHOOK_STATE_PATH || path.join("/tmp", "anime-webhook-state.json");
8
+ const POLL_MS = Math.max(parseInt(process.env.WEBHOOK_POLL_MS || "600000", 10), 60000);
9
+ const DOWNLOAD_PROXY_BASE = String(process.env.DOWNLOAD_PROXY_BASE || "").trim().replace(/\/+$/, "");
10
+
11
+ let interval = null;
12
+ let running = false;
13
+ let sentState = new Set();
14
+ let loaded = false;
15
+
16
+ function isEnabled() {
17
+ return !!(process.env.WEBHOOK_URL && process.env.WEBHOOK_SECRET);
18
+ }
19
+
20
+ function buildKey(item) {
21
+ return [
22
+ item.anime_session || "",
23
+ item.episode_session || "",
24
+ item.episode || "",
25
+ ].join(":");
26
+ }
27
+
28
+ function buildProxyUrl(url) {
29
+ if (!url) return url;
30
+ if (!DOWNLOAD_PROXY_BASE) return url;
31
+ return `${DOWNLOAD_PROXY_BASE}/api/anime/download-proxy?url=${encodeURIComponent(url)}`;
32
+ }
33
+
34
+ async function loadState() {
35
+ if (loaded) return;
36
+ loaded = true;
37
+ try {
38
+ const raw = await fs.readFile(STATE_PATH, "utf8");
39
+ const parsed = JSON.parse(raw);
40
+ if (Array.isArray(parsed.sent)) {
41
+ sentState = new Set(parsed.sent);
42
+ }
43
+ } catch (_) {
44
+ sentState = new Set();
45
+ }
46
+ }
47
+
48
+ async function saveState() {
49
+ try {
50
+ const payload = JSON.stringify({ sent: Array.from(sentState).slice(-1000) });
51
+ await fs.writeFile(STATE_PATH, payload, "utf8");
52
+ } catch (err) {
53
+ console.error("[webhook] state save failed:", err.message);
54
+ }
55
+ }
56
+
57
+ async function postWebhook(payload) {
58
+ const res = await axios.post(process.env.WEBHOOK_URL, payload, {
59
+ timeout: 15000,
60
+ headers: {
61
+ "Content-Type": "application/json",
62
+ "x-webhook-secret": process.env.WEBHOOK_SECRET,
63
+ },
64
+ validateStatus: () => true,
65
+ });
66
+
67
+ if (res.status < 200 || res.status >= 300) {
68
+ throw new Error(`webhook HTTP ${res.status}: ${JSON.stringify(res.data).slice(0, 200)}`);
69
+ }
70
+
71
+ return res.data;
72
+ }
73
+
74
+ async function buildPayload(item) {
75
+ if (!item.anime_session || !item.episode_session) return null;
76
+
77
+ const detail = await PlayModel.getStreamingLinks(item.anime_session, item.episode_session, true);
78
+ const downloads = Array.isArray(detail?.downloads) ? detail.downloads : [];
79
+ const usable = downloads
80
+ .map((dl) => ({
81
+ quality: dl.quality || dl.resolution || null,
82
+ filesize: dl.filesize || dl.size || null,
83
+ isDub: Boolean(dl.isDub || dl.dub),
84
+ url: buildProxyUrl(dl.download || dl.url || null),
85
+ }))
86
+ .filter((dl) => dl.url);
87
+
88
+ if (!usable.length) return null;
89
+
90
+ return {
91
+ anime_id: item.anime_id || null,
92
+ anime_session: item.anime_session || null,
93
+ episode: item.episode || null,
94
+ episode_session: item.episode_session || null,
95
+ title: detail?.anime_title || item.title || null,
96
+ image: item.image || null,
97
+ created_at: item.created_at || null,
98
+ downloads: usable,
99
+ };
100
+ }
101
+
102
+ async function tick() {
103
+ return runTick({});
104
+ }
105
+
106
+ async function runTick(options = {}) {
107
+ if (running || !isEnabled()) return;
108
+ running = true;
109
+ let seen = 0;
110
+ let delivered = 0;
111
+ let missingEpisodeSession = 0;
112
+ let alreadySent = 0;
113
+ let unresolvedDownloads = 0;
114
+ let matchedTarget = 0;
115
+
116
+ try {
117
+ await loadState();
118
+ const airing = await HomeModel.getAiringAnime(1);
119
+ const items = Array.isArray(airing?.data) ? airing.data : [];
120
+ const targetSession = String(options.animeSession || "").trim() || null;
121
+
122
+ for (const item of items) {
123
+ if (targetSession && String(item.anime_session || "") !== targetSession) continue;
124
+ matchedTarget += 1;
125
+ const key = buildKey(item);
126
+ if (!item.episode_session) {
127
+ missingEpisodeSession += 1;
128
+ console.warn(`[webhook] skip missing episode_session: ${item.anime_session || "unknown"} ep=${item.episode || "?"}`);
129
+ continue;
130
+ }
131
+ seen += 1;
132
+ if (sentState.has(key)) {
133
+ alreadySent += 1;
134
+ continue;
135
+ }
136
+
137
+ try {
138
+ const payload = await buildPayload(item);
139
+ if (!payload) {
140
+ unresolvedDownloads += 1;
141
+ console.warn(`[webhook] skip unresolved downloads: ${item.title || item.anime_title || item.anime_session}`);
142
+ continue;
143
+ }
144
+
145
+ const result = await postWebhook(payload);
146
+ console.log(`[webhook] delivered ${key} matched=${result?.matched ?? "?"} delivered=${result?.delivered ?? "?"}`);
147
+ sentState.add(key);
148
+ delivered += Number(result?.delivered || 0);
149
+ if (sentState.size > 1000) {
150
+ sentState = new Set(Array.from(sentState).slice(-1000));
151
+ }
152
+ await saveState();
153
+ } catch (err) {
154
+ console.error(`[webhook] failed ${key}:`, err.message);
155
+ }
156
+ }
157
+ } catch (err) {
158
+ console.error("[webhook] tick failed:", err.message);
159
+ throw err;
160
+ } finally {
161
+ running = false;
162
+ }
163
+
164
+ return {
165
+ matchedTarget,
166
+ seen,
167
+ delivered,
168
+ missingEpisodeSession,
169
+ alreadySent,
170
+ unresolvedDownloads,
171
+ };
172
+ }
173
+
174
+ function startWebhookNotifier() {
175
+ if (!isEnabled()) {
176
+ console.log("[webhook] disabled (set WEBHOOK_URL and WEBHOOK_SECRET to enable)");
177
+ return;
178
+ }
179
+ if (interval) return;
180
+ console.log(`[webhook] enabled -> ${process.env.WEBHOOK_URL}`);
181
+ tick().catch(() => {});
182
+ interval = setInterval(() => {
183
+ tick().catch(() => {});
184
+ }, POLL_MS);
185
+ }
186
+
187
+ module.exports = {
188
+ startWebhookNotifier,
189
+ runTick,
190
+ };
@@ -0,0 +1,291 @@
1
+ #!/usr/bin/env python3
2
+ """Multi-browser AnimePahe cookie reader.
3
+
4
+ Commands:
5
+ detect Scan all browsers for animepahe cookies
6
+ read Read cookies from a specific browser
7
+ clear Delete animepahe cookies from a specific browser
8
+
9
+ Output is JSON to stdout.
10
+ """
11
+ import argparse
12
+ import json
13
+ import os
14
+ import shutil
15
+ import sqlite3
16
+ import sys
17
+ import tempfile
18
+
19
+ # ── Browser cookie locations ──────────────────────────────────────────
20
+ FIREFOX_DIRS = [os.path.expanduser("~/.mozilla/firefox")]
21
+ CHROMIUM_BASED = [
22
+ ("chrome", os.path.expanduser("~/.config/google-chrome")),
23
+ ("chromium", os.path.expanduser("~/.config/chromium")),
24
+ ("brave", os.path.expanduser("~/.config/BraveSoftware/Brave-Browser")),
25
+ ("edge", os.path.expanduser("~/.config/microsoft-edge")),
26
+ ]
27
+
28
+ DOMAIN_FILTERS = ["%animepahe%", "%pahe%"]
29
+
30
+ # ── Helpers ───────────────────────────────────────────────────────────
31
+
32
+ def _find_firefox_profiles():
33
+ profiles = []
34
+ for d in FIREFOX_DIRS:
35
+ if not os.path.isdir(d):
36
+ continue
37
+ for entry in os.listdir(d):
38
+ db_path = os.path.join(d, entry, "cookies.sqlite")
39
+ if os.path.isfile(db_path):
40
+ profiles.append({"profile": entry, "path": db_path})
41
+ return profiles
42
+
43
+
44
+ def _read_firefox_cookies(db_path):
45
+ tmp = tempfile.mktemp(suffix=".sqlite")
46
+ try:
47
+ shutil.copy2(db_path, tmp)
48
+ wal = db_path + "-wal"
49
+ if os.path.isfile(wal):
50
+ shutil.copy2(wal, tmp + "-wal")
51
+ conn = sqlite3.connect(tmp)
52
+ cursor = conn.cursor()
53
+ cursor.execute(
54
+ "SELECT name, value FROM moz_cookies WHERE host LIKE ? OR host LIKE ?",
55
+ DOMAIN_FILTERS,
56
+ )
57
+ rows = cursor.fetchall()
58
+ conn.close()
59
+ return {name: val for name, val in rows}
60
+ except Exception as e:
61
+ return {"_error": str(e)}
62
+ finally:
63
+ try: os.unlink(tmp)
64
+ except: pass
65
+ try: os.unlink(tmp + "-wal")
66
+ except: pass
67
+
68
+
69
+ def _find_chromium_profiles(browser_name, base_dir):
70
+ profiles = []
71
+ if not os.path.isdir(base_dir):
72
+ return profiles
73
+ for entry in os.listdir(base_dir):
74
+ cookie_path = os.path.join(base_dir, entry, "Cookies")
75
+ local_state = os.path.join(base_dir, "Local State")
76
+ if os.path.isfile(cookie_path):
77
+ profiles.append({
78
+ "profile": entry,
79
+ "path": cookie_path,
80
+ "local_state": local_state if os.path.isfile(local_state) else None,
81
+ })
82
+ return profiles
83
+
84
+
85
+ def _decrypt_chromium_cookies(browser_name, profile):
86
+ """Attempt to decrypt cookies from a Chromium-based browser.
87
+
88
+ Returns dict of cookie name→value, or dict with _error key.
89
+ Returns empty dict on failure — caller should fall back gracefully.
90
+ """
91
+ # For now, this is a best-effort attempt that may fail on modern
92
+ # setups (portal-based encryption on KDE, etc.)
93
+ return {}
94
+
95
+
96
+ def _read_chromium_cookies(browser_name, profile):
97
+ tmp = tempfile.mktemp(suffix=".sqlite")
98
+ try:
99
+ shutil.copy2(profile["path"], tmp)
100
+ conn = sqlite3.connect(tmp)
101
+ cursor = conn.cursor()
102
+ cursor.execute(
103
+ "SELECT name, value, encrypted_value FROM cookies "
104
+ "WHERE host_key LIKE ? OR host_key LIKE ?",
105
+ DOMAIN_FILTERS,
106
+ )
107
+ rows = cursor.fetchall()
108
+ conn.close()
109
+
110
+ cookies = {}
111
+ for name, value, enc_val in rows:
112
+ if value:
113
+ cookies[name] = value
114
+ else:
115
+ if enc_val and enc_val[:3] in (b"v10", b"v11"):
116
+ try_decrypt = _decrypt_chromium_cookies(browser_name, profile)
117
+ if name in try_decrypt:
118
+ cookies[name] = try_decrypt[name]
119
+ return cookies
120
+ except Exception as e:
121
+ return {"_error": str(e)}
122
+ finally:
123
+ try: os.unlink(tmp)
124
+ except: pass
125
+
126
+
127
+ def _cookie_map_to_string(cookies):
128
+ parts = []
129
+ for k, v in cookies.items():
130
+ if k.startswith("_"):
131
+ continue
132
+ parts.append(f"{k}={v}")
133
+ return "; ".join(parts)
134
+
135
+
136
+ # ── Commands ──────────────────────────────────────────────────────────
137
+
138
+ def cmd_detect():
139
+ result = {}
140
+
141
+ # Firefox
142
+ ff_profiles = _find_firefox_profiles()
143
+ ff_cookies = {}
144
+ for p in ff_profiles:
145
+ c = _read_firefox_cookies(p["path"])
146
+ if "_error" not in c and c:
147
+ ff_cookies.update(c)
148
+ if ff_cookies:
149
+ result["firefox"] = {
150
+ "found": True,
151
+ "profiles": [p["profile"] for p in ff_profiles],
152
+ "cookies": {k: v for k, v in ff_cookies.items() if not k.startswith("_")},
153
+ "cookie_string": _cookie_map_to_string(ff_cookies),
154
+ }
155
+ else:
156
+ result["firefox"] = {"found": False, "profiles": [p["profile"] for p in ff_profiles]}
157
+
158
+ # Chromium-based
159
+ for name, base_dir in CHROMIUM_BASED:
160
+ profiles = _find_chromium_profiles(name, base_dir)
161
+ if not profiles:
162
+ result[name] = {"found": False, "error": "No profiles found"}
163
+ continue
164
+ all_cookies = {}
165
+ for p in profiles:
166
+ c = _read_chromium_cookies(name, p)
167
+ if "_error" not in c and c:
168
+ all_cookies.update(c)
169
+ encrypted = any(p["path"] for p in profiles)
170
+ if all_cookies:
171
+ result[name] = {
172
+ "found": True,
173
+ "profiles": [p["profile"] for p in profiles],
174
+ "cookies": {k: v for k, v in all_cookies.items() if not k.startswith("_")},
175
+ "cookie_string": _cookie_map_to_string(all_cookies),
176
+ "encrypted": encrypted,
177
+ }
178
+ else:
179
+ result[name] = {
180
+ "found": False,
181
+ "profiles": [p["profile"] for p in profiles],
182
+ "encrypted": encrypted,
183
+ "error": "Cookies found but could not be decrypted (try manual paste)",
184
+ }
185
+
186
+ return result
187
+
188
+
189
+ def cmd_read(browser, profile_name=None):
190
+ if browser == "firefox":
191
+ for p in _find_firefox_profiles():
192
+ if profile_name and p["profile"] != profile_name:
193
+ continue
194
+ c = _read_firefox_cookies(p["path"])
195
+ if "_error" not in c and c:
196
+ return {
197
+ "browser": "firefox",
198
+ "profile": p["profile"],
199
+ "cookies": {k: v for k, v in c.items() if not k.startswith("_")},
200
+ "cookie_string": _cookie_map_to_string(c),
201
+ }
202
+ return {"browser": "firefox", "error": "No cookies found"}
203
+
204
+ for name, base_dir in CHROMIUM_BASED:
205
+ if name != browser:
206
+ continue
207
+ for p in _find_chromium_profiles(name, base_dir):
208
+ if profile_name and p["profile"] != profile_name:
209
+ continue
210
+ c = _read_chromium_cookies(name, p)
211
+ if "_error" not in c and c:
212
+ return {
213
+ "browser": name,
214
+ "profile": p["profile"],
215
+ "cookies": {k: v for k, v in c.items() if not k.startswith("_")},
216
+ "cookie_string": _cookie_map_to_string(c),
217
+ }
218
+ if c:
219
+ return {"browser": name, "error": "Found but could not decrypt"}
220
+ return {"browser": name, "error": "No cookies found"}
221
+
222
+ return {"error": f"Unknown browser: {browser}"}
223
+
224
+
225
+ def cmd_clear(browser):
226
+ if browser == "firefox":
227
+ cleared = []
228
+ locked = []
229
+ for p in _find_firefox_profiles():
230
+ try:
231
+ conn = sqlite3.connect(p["path"])
232
+ conn.execute(
233
+ "DELETE FROM moz_cookies WHERE host LIKE ? OR host LIKE ?",
234
+ DOMAIN_FILTERS,
235
+ )
236
+ conn.commit()
237
+ conn.close()
238
+ cleared.append(p["profile"])
239
+ except Exception as e:
240
+ locked.append({"profile": p["profile"], "error": str(e)})
241
+ return {"ok": True, "cleared": cleared, "locked": locked}
242
+
243
+ for name, base_dir in CHROMIUM_BASED:
244
+ if name != browser:
245
+ continue
246
+ cleared = []
247
+ locked = []
248
+ for p in _find_chromium_profiles(name, base_dir):
249
+ try:
250
+ conn = sqlite3.connect(p["path"])
251
+ conn.execute(
252
+ "DELETE FROM cookies WHERE host_key LIKE ? OR host_key LIKE ?",
253
+ DOMAIN_FILTERS,
254
+ )
255
+ conn.commit()
256
+ conn.close()
257
+ cleared.append(p["profile"])
258
+ except Exception as e:
259
+ locked.append({"profile": p["profile"], "error": str(e)})
260
+ return {"ok": True, "cleared": cleared, "locked": locked}
261
+
262
+ return {"error": f"Unknown browser: {browser}"}
263
+
264
+
265
+ # ── CLI ───────────────────────────────────────────────────────────────
266
+
267
+ def main():
268
+ parser = argparse.ArgumentParser(description="Read AnimePahe cookies from browsers")
269
+ parser.add_argument("command", choices=["detect", "read", "clear"])
270
+ parser.add_argument("--browser", "-b", help="Browser name (firefox, chrome, brave, edge)")
271
+ parser.add_argument("--profile", "-p", help="Profile name (optional)")
272
+ args = parser.parse_args()
273
+
274
+ if args.command == "detect":
275
+ result = cmd_detect()
276
+ elif args.command == "read":
277
+ if not args.browser:
278
+ print(json.dumps({"error": "--browser is required for read"}))
279
+ sys.exit(1)
280
+ result = cmd_read(args.browser, args.profile)
281
+ elif args.command == "clear":
282
+ if not args.browser:
283
+ print(json.dumps({"error": "--browser is required for clear"}))
284
+ sys.exit(1)
285
+ result = cmd_clear(args.browser)
286
+
287
+ print(json.dumps(result, indent=2))
288
+
289
+
290
+ if __name__ == "__main__":
291
+ main()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ani-auto",
3
- "version": "1.4.4",
3
+ "version": "2.0.0",
4
4
  "description": "automatic anime episode downloader",
5
5
  "bin": {
6
6
  "ani-auto": "src/cli.js"
@@ -8,18 +8,26 @@
8
8
  "scripts": {
9
9
  "start": "node src/daemon.js",
10
10
  "download": "node src/cli.js download",
11
- "setup": "node src/cli.js setup"
11
+ "setup": "node src/cli.js setup",
12
+ "web": "node src/server.js"
12
13
  },
13
14
  "dependencies": {
15
+ "axios": "^1.18.1",
14
16
  "chalk": "^4.1.2",
17
+ "cheerio": "^1.0.0-rc.12",
15
18
  "cli-progress": "^3.12.0",
19
+ "cloudscraper": "^4.6.0",
16
20
  "commander": "^12.0.0",
21
+ "cors": "^2.8.6",
22
+ "dotenv": "^16.6.1",
23
+ "express": "^5.2.1",
17
24
  "inquirer": "^8.2.6",
25
+ "jsdom": "^22.1.0",
18
26
  "node-fetch": "^2.7.0",
19
27
  "ora": "^5.4.1",
20
- "puppeteer": "^24.40.0",
21
- "puppeteer-extra": "^3.3.6",
22
- "puppeteer-extra-plugin-stealth": "^2.11.2",
23
- "sql.js": "^1.12.0"
28
+ "p-limit": "^5.0.0",
29
+ "playwright": "^1.61.1",
30
+ "sql.js": "^1.12.0",
31
+ "ws": "^8.21.0"
24
32
  }
25
33
  }
package/src/anilist.js CHANGED
@@ -27,7 +27,15 @@ query ($userName: String, $status: MediaListStatus) {
27
27
 
28
28
  const VIEWER_QUERY = `
29
29
  query {
30
- Viewer { id name }
30
+ Viewer { id name avatar { medium } }
31
+ }`;
32
+
33
+ const MEDIA_BY_ID_QUERY = `
34
+ query ($id: Int) {
35
+ Media(id: $id, type: ANIME) {
36
+ id
37
+ coverImage { extraLarge large medium }
38
+ }
31
39
  }`;
32
40
 
33
41
  const SEARCH_QUERY = `
@@ -115,4 +123,9 @@ function pickTitle(titles) {
115
123
  return titles.english || titles.romaji || titles.native;
116
124
  }
117
125
 
118
- module.exports = { getViewer, getUserList, searchAnime, pickTitle };
126
+ async function getMediaById(anilistId, token = null) {
127
+ const data = await gqlRequest(MEDIA_BY_ID_QUERY, { id: anilistId }, token);
128
+ return data.Media || null;
129
+ }
130
+
131
+ module.exports = { getViewer, getUserList, searchAnime, pickTitle, getMediaById };
@@ -0,0 +1,32 @@
1
+ 'use strict';
2
+
3
+ const { Router } = require('express');
4
+ const { getUserList, searchAnime } = require('../anilist');
5
+ const { loadConfig } = require('../config');
6
+
7
+ const router = Router();
8
+
9
+ router.get('/list', async (req, res) => {
10
+ try {
11
+ const config = loadConfig();
12
+ if (!config.anilistUsername) return res.json([]);
13
+ const statuses = req.query.statuses ? req.query.statuses.split(',') : (config.watchStatuses || ['CURRENT']);
14
+ const entries = await getUserList(config.anilistUsername, statuses, config.anilistToken || null);
15
+ res.json(entries);
16
+ } catch (e) {
17
+ res.status(500).json({ error: e.message });
18
+ }
19
+ });
20
+
21
+ router.get('/search', async (req, res) => {
22
+ try {
23
+ const q = req.query.q;
24
+ if (!q) return res.json([]);
25
+ const results = await searchAnime(q);
26
+ res.json(results);
27
+ } catch (e) {
28
+ res.status(500).json({ error: e.message });
29
+ }
30
+ });
31
+
32
+ module.exports = router;