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.
- package/anime/LICENSE +21 -0
- package/anime/api/index.js +110 -0
- package/anime/app.js +79 -0
- package/anime/controllers/animeInfoController.js +48 -0
- package/anime/controllers/animeListController.js +21 -0
- package/anime/controllers/homeController.js +42 -0
- package/anime/controllers/playController.js +140 -0
- package/anime/controllers/queueController.js +20 -0
- package/anime/controllers/testController.js +667 -0
- package/anime/index.js +139 -0
- package/anime/middleware/cache.js +64 -0
- package/anime/middleware/errorHandler.js +33 -0
- package/anime/middleware/rateLimiter.js +73 -0
- package/anime/models/animeInfoModel.js +193 -0
- package/anime/models/animeListModel.js +69 -0
- package/anime/models/homeModel.js +33 -0
- package/anime/models/playModel.js +528 -0
- package/anime/models/queueModel.js +22 -0
- package/anime/package.json +27 -0
- package/anime/pnpm-lock.yaml +1774 -0
- package/anime/readme.md +236 -0
- package/anime/routes/animeInfoRoutes.js +9 -0
- package/anime/routes/animeListRoutes.js +9 -0
- package/anime/routes/homeRoutes.js +10 -0
- package/anime/routes/playRoutes.js +11 -0
- package/anime/routes/queueRoutes.js +8 -0
- package/anime/routes/testRoutes.js +325 -0
- package/anime/routes/webhookRoutes.js +23 -0
- package/anime/scrapers/animepahe.js +1053 -0
- package/anime/test-server.js +10 -0
- package/anime/test.sh +275 -0
- package/anime/utils/browser.js +172 -0
- package/anime/utils/config.js +209 -0
- package/anime/utils/dataProcessor.js +172 -0
- package/anime/utils/diskCache.js +228 -0
- package/anime/utils/flaresolverr.js +361 -0
- package/anime/utils/jsParser.js +19 -0
- package/anime/utils/redis.js +64 -0
- package/anime/utils/requestManager.js +706 -0
- package/anime/utils/urlConverter.js +88 -0
- package/anime/utils/webhookNotifier.js +190 -0
- package/cookiereader.py +291 -0
- package/package.json +14 -6
- package/src/anilist.js +15 -2
- package/src/api/anilist.js +32 -0
- package/src/api/anime.js +181 -0
- package/src/api/cf-bypass.js +131 -0
- package/src/api/config.js +134 -0
- package/src/api/daemon.js +62 -0
- package/src/api/download.js +98 -0
- package/src/api/match.js +58 -0
- package/src/api/runs.js +15 -0
- package/src/api/update.js +96 -0
- package/src/cli.js +18 -2
- package/src/config.js +14 -2
- package/src/daemon.js +1 -1
- package/src/db.js +38 -11
- package/src/engine.js +62 -11
- package/src/scraper.js +2 -2
- package/src/server.js +108 -0
- package/utils.js +21 -4
- package/web/index.html +13 -0
- package/web/package-lock.json +1420 -0
- package/web/package.json +24 -0
- package/web/src/App.vue +200 -0
- package/web/src/api/client.js +75 -0
- package/web/src/assets/styles/base.css +961 -0
- package/web/src/components/UpdateModal.vue +157 -0
- package/web/src/components/sidebar/SidebarResizable.vue +170 -0
- package/web/src/components/sidebar/sidebarConfig.js +24 -0
- package/web/src/main.js +104 -0
- package/web/src/router/index.js +44 -0
- package/web/src/stores/config.js +27 -0
- package/web/src/stores/download.js +107 -0
- package/web/src/stores/update.js +73 -0
- package/web/src/views/About.vue +42 -0
- package/web/src/views/AniListSync.vue +122 -0
- package/web/src/views/AnimeDetail.vue +202 -0
- package/web/src/views/AnimeList.vue +110 -0
- package/web/src/views/CFResult.vue +312 -0
- package/web/src/views/DaemonControl.vue +83 -0
- package/web/src/views/Dashboard.vue +187 -0
- package/web/src/views/DownloadCenter.vue +153 -0
- package/web/src/views/MatchFinder.vue +131 -0
- package/web/src/views/OAuthCallback.vue +44 -0
- package/web/src/views/Onboarding.vue +93 -0
- package/web/src/views/RunHistory.vue +122 -0
- package/web/src/views/Settings.vue +410 -0
- 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
|
+
};
|
package/cookiereader.py
ADDED
|
@@ -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": "
|
|
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
|
-
"
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
"
|
|
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
|
-
|
|
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;
|