ani-auto 1.4.3 → 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 +19 -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 +66 -13
- package/src/notify.js +82 -7
- package/src/scraper.js +2 -2
- package/src/server.js +108 -0
- package/utils.js +21 -21
- 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,96 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { Router } = require('express');
|
|
4
|
+
const fetch = require('node-fetch');
|
|
5
|
+
const { exec } = require('child_process');
|
|
6
|
+
const { version } = require('../../package.json');
|
|
7
|
+
|
|
8
|
+
const router = Router();
|
|
9
|
+
|
|
10
|
+
router.get('/check', async (req, res) => {
|
|
11
|
+
try {
|
|
12
|
+
const response = await fetch('https://registry.npmjs.org/ani-auto/latest', {
|
|
13
|
+
timeout: 10000,
|
|
14
|
+
});
|
|
15
|
+
const data = await response.json();
|
|
16
|
+
const latest = data.version;
|
|
17
|
+
res.json({
|
|
18
|
+
current: version,
|
|
19
|
+
latest,
|
|
20
|
+
updateAvailable: latest !== version,
|
|
21
|
+
});
|
|
22
|
+
} catch (e) {
|
|
23
|
+
res.json({
|
|
24
|
+
current: version,
|
|
25
|
+
latest: version,
|
|
26
|
+
updateAvailable: false,
|
|
27
|
+
error: e.message,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
router.post('/install', async (req, res) => {
|
|
33
|
+
const broadcast = req.app.locals.broadcast;
|
|
34
|
+
if (!broadcast) {
|
|
35
|
+
return res.status(500).json({ error: 'WebSocket not available' });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
res.json({ ok: true, message: 'Update started' });
|
|
39
|
+
|
|
40
|
+
broadcast('update:progress', { percent: 5, message: 'Checking latest version...' });
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
const response = await fetch('https://registry.npmjs.org/ani-auto/latest', {
|
|
44
|
+
timeout: 10000,
|
|
45
|
+
});
|
|
46
|
+
const data = await response.json();
|
|
47
|
+
const latest = data.version;
|
|
48
|
+
|
|
49
|
+
if (latest === version) {
|
|
50
|
+
broadcast('update:done', {
|
|
51
|
+
version,
|
|
52
|
+
message: `Already on v${version}`,
|
|
53
|
+
});
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
broadcast('update:progress', {
|
|
58
|
+
percent: 15,
|
|
59
|
+
message: `Updating v${version} → v${latest}...`,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const child = exec(
|
|
63
|
+
`npm install -g ani-auto@${latest}`,
|
|
64
|
+
{ timeout: 120000 },
|
|
65
|
+
(error) => {
|
|
66
|
+
if (error) {
|
|
67
|
+
broadcast('update:error', {
|
|
68
|
+
error: `Install failed: ${error.message}`,
|
|
69
|
+
});
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
broadcast('update:progress', { percent: 95, message: 'Finalizing...' });
|
|
73
|
+
setTimeout(() => {
|
|
74
|
+
broadcast('update:done', {
|
|
75
|
+
version: latest,
|
|
76
|
+
message: `Updated to v${latest}`,
|
|
77
|
+
});
|
|
78
|
+
}, 500);
|
|
79
|
+
}
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
child.stdout?.on('data', (chunk) => {
|
|
83
|
+
const text = chunk.toString();
|
|
84
|
+
if (text.includes('added') || text.includes('changed')) {
|
|
85
|
+
broadcast('update:progress', {
|
|
86
|
+
percent: 60,
|
|
87
|
+
message: 'Packages installed',
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
} catch (e) {
|
|
92
|
+
broadcast('update:error', { error: e.message });
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
module.exports = router;
|
package/src/cli.js
CHANGED
|
@@ -100,13 +100,19 @@ program
|
|
|
100
100
|
|
|
101
101
|
if (answers.anilistToken) {
|
|
102
102
|
const spinner = ora('Verifying AniList token...').start();
|
|
103
|
-
try {
|
|
104
|
-
|
|
103
|
+
try {
|
|
104
|
+
const viewer = await getViewer(answers.anilistToken);
|
|
105
|
+
spinner.succeed(`Logged in as ${chalk.bold(viewer.name)}`);
|
|
106
|
+
answers.anilistAvatar = viewer.avatar?.medium || null;
|
|
107
|
+
} catch {
|
|
108
|
+
spinner.warn('Could not verify token - saved anyway, check it manually.');
|
|
109
|
+
}
|
|
105
110
|
}
|
|
106
111
|
|
|
107
112
|
updateConfig({
|
|
108
113
|
anilistUsername: answers.anilistUsername || null,
|
|
109
114
|
anilistToken: answers.anilistToken || null,
|
|
115
|
+
anilistAvatar: answers.anilistAvatar || null,
|
|
110
116
|
watchStatuses: answers.watchStatuses || current.watchStatuses,
|
|
111
117
|
quality: answers.quality,
|
|
112
118
|
concurrency: answers.concurrency,
|
|
@@ -806,6 +812,16 @@ program
|
|
|
806
812
|
}
|
|
807
813
|
});
|
|
808
814
|
|
|
815
|
+
// ── web ───────────────────────────────────────────────────────────────────
|
|
816
|
+
|
|
817
|
+
program
|
|
818
|
+
.command('web')
|
|
819
|
+
.description('Start the web UI server on port 2640')
|
|
820
|
+
.action(() => {
|
|
821
|
+
const { startServer } = require('./server');
|
|
822
|
+
startServer();
|
|
823
|
+
});
|
|
824
|
+
|
|
809
825
|
// ── main ──────────────────────────────────────────────────────────────────
|
|
810
826
|
|
|
811
827
|
program
|
package/src/config.js
CHANGED
|
@@ -23,18 +23,30 @@ const DEFAULTS = {
|
|
|
23
23
|
telegramChatId: null,
|
|
24
24
|
telegramNotify: true,
|
|
25
25
|
airingBufferMinutes: 5,
|
|
26
|
-
daemonEnabled: true,
|
|
26
|
+
daemonEnabled: true,
|
|
27
|
+
autoUpdateCheck: true,
|
|
27
28
|
apiBaseUrl: 'https://aor-rex-anikuro-api.hf.space',
|
|
29
|
+
anilistClientId: '37183',
|
|
30
|
+
anilistClientSecret: null,
|
|
31
|
+
animepaheCfClearance: null,
|
|
32
|
+
animepaheUserAgent: null,
|
|
28
33
|
};
|
|
29
34
|
|
|
30
35
|
function ensureConfigDir() {
|
|
31
36
|
if (!fs.existsSync(CONFIG_DIR)) fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
32
37
|
}
|
|
33
38
|
|
|
39
|
+
const SENTINEL = '[set]';
|
|
40
|
+
const MASKABLE_KEYS = new Set(['anilistToken', 'anilistClientSecret', 'animepaheCfClearance', 'animepaheUserAgent']);
|
|
41
|
+
|
|
34
42
|
function loadConfig() {
|
|
35
43
|
ensureConfigDir();
|
|
36
44
|
if (!fs.existsSync(CONFIG_FILE)) { saveConfig(DEFAULTS); return { ...DEFAULTS }; }
|
|
37
|
-
try {
|
|
45
|
+
try {
|
|
46
|
+
const cfg = { ...DEFAULTS, ...JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8')) };
|
|
47
|
+
for (const k of MASKABLE_KEYS) { if (cfg[k] === SENTINEL) cfg[k] = null; }
|
|
48
|
+
return cfg;
|
|
49
|
+
}
|
|
38
50
|
catch { return { ...DEFAULTS }; }
|
|
39
51
|
}
|
|
40
52
|
|
package/src/daemon.js
CHANGED
package/src/db.js
CHANGED
|
@@ -117,17 +117,35 @@ function _run(sql, params = []) {
|
|
|
117
117
|
}
|
|
118
118
|
|
|
119
119
|
|
|
120
|
-
function upsertAnime({ title, siteId, anilistId, source, quality, language }) {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
120
|
+
function upsertAnime({ title, siteId, anilistId, source, quality, language, poster_url }) {
|
|
121
|
+
const finalSiteId = siteId || title;
|
|
122
|
+
const finalAnilistId = anilistId || null;
|
|
123
|
+
|
|
124
|
+
const existing = finalAnilistId
|
|
125
|
+
? (_get('SELECT id FROM anime WHERE anilist_id = ?', [finalAnilistId]) ||
|
|
126
|
+
_get('SELECT id FROM anime WHERE site_id = ?', [finalSiteId]))
|
|
127
|
+
: _get('SELECT id FROM anime WHERE site_id = ?', [finalSiteId]);
|
|
128
|
+
|
|
129
|
+
if (existing) {
|
|
130
|
+
_run(`
|
|
131
|
+
UPDATE anime SET
|
|
132
|
+
title = ?,
|
|
133
|
+
site_id = ?,
|
|
134
|
+
anilist_id = COALESCE(?, anime.anilist_id),
|
|
135
|
+
source = ?,
|
|
136
|
+
quality = COALESCE(?, anime.quality),
|
|
137
|
+
language = COALESCE(?, anime.language),
|
|
138
|
+
poster_url = COALESCE(?, anime.poster_url)
|
|
139
|
+
WHERE id = ?
|
|
140
|
+
`, [title, finalSiteId, finalAnilistId, source || 'anilist', quality || null, language || null, poster_url || null, existing.id]);
|
|
141
|
+
} else {
|
|
142
|
+
_run(`
|
|
143
|
+
INSERT INTO anime (title, site_id, anilist_id, source, quality, language, poster_url)
|
|
144
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
145
|
+
`, [title, finalSiteId, finalAnilistId, source || 'anilist', quality || null, language || null, poster_url || null]);
|
|
146
|
+
}
|
|
147
|
+
return _get('SELECT * FROM anime WHERE site_id = ?', [finalSiteId]) ||
|
|
148
|
+
_get('SELECT * FROM anime WHERE anilist_id = ?', [finalAnilistId]);
|
|
131
149
|
}
|
|
132
150
|
|
|
133
151
|
function getAnime(siteId) {
|
|
@@ -263,9 +281,18 @@ function updateNextAiring(animeDbId, airingAt) {
|
|
|
263
281
|
_run('UPDATE anime SET next_airing_at = ? WHERE id = ?', [airingAt, animeDbId]);
|
|
264
282
|
}
|
|
265
283
|
|
|
284
|
+
function execSql(sql, params = []) {
|
|
285
|
+
_run(sql, params);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function saveDb() {
|
|
289
|
+
_save();
|
|
290
|
+
}
|
|
291
|
+
|
|
266
292
|
module.exports = {
|
|
267
293
|
initDb,
|
|
268
294
|
getDb: getDbInstance,
|
|
295
|
+
execSql, saveDb,
|
|
269
296
|
upsertAnime, getAnime, getAnimeBySiteTitle, saveConfirmedSiteMatch, getAllEnabledAnime, setAnimeEnabled, setAnimeQuality, removeAnime,
|
|
270
297
|
isEpisodeDownloaded, getEpisodeRow, markEpisodeStatus, getEpisodeStats, getFailedEpisodes, getAllEpisodes, updateNextAiring,
|
|
271
298
|
logRun, getRecentRuns,
|
package/src/engine.js
CHANGED
|
@@ -15,11 +15,14 @@ const notify = require('./notify');
|
|
|
15
15
|
|
|
16
16
|
// ── Logging ────────────────────────────────────────────────────────────────
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
function
|
|
21
|
-
function
|
|
18
|
+
let currentBus = null;
|
|
19
|
+
|
|
20
|
+
function info(msg) { const m = stripAnsi(msg); console.log(`${chalk.gray(ts())} ${chalk.cyan('ℹ')} ${msg}`); if (currentBus) currentBus.emit('broadcast', 'log', { level: 'info', message: m, timestamp: ts() }); }
|
|
21
|
+
function ok(msg) { const m = stripAnsi(msg); console.log(`${chalk.gray(ts())} ${chalk.green('✔')} ${msg}`); if (currentBus) currentBus.emit('broadcast', 'log', { level: 'ok', message: m, timestamp: ts() }); }
|
|
22
|
+
function warn(msg) { const m = stripAnsi(msg); console.log(`${chalk.gray(ts())} ${chalk.yellow('⚠')} ${msg}`); if (currentBus) currentBus.emit('broadcast', 'log', { level: 'warn', message: m, timestamp: ts() }); }
|
|
23
|
+
function err(msg) { const m = stripAnsi(msg); console.log(`${chalk.gray(ts())} ${chalk.red('✖')} ${msg}`); if (currentBus) currentBus.emit('broadcast', 'log', { level: 'err', message: m, timestamp: ts() }); }
|
|
22
24
|
function ts() { return new Date().toISOString().replace('T', ' ').slice(0, 19); }
|
|
25
|
+
function stripAnsi(s) { return s.replace(/\x1b\[[0-9;]*m/g, ''); }
|
|
23
26
|
|
|
24
27
|
// ── Filename helpers ───────────────────────────────────────────────────────
|
|
25
28
|
|
|
@@ -69,6 +72,7 @@ async function resolveWatchList(config) {
|
|
|
69
72
|
knownEpisodes: entry.totalEpisodes,
|
|
70
73
|
animeStatus: entry.animeStatus,
|
|
71
74
|
anilistStatus: entry.status,
|
|
75
|
+
posterUrl: entry.posterUrl || null,
|
|
72
76
|
nextAiringAt: entry.nextAiring?.airingAt || null,
|
|
73
77
|
});
|
|
74
78
|
}
|
|
@@ -90,6 +94,7 @@ async function resolveWatchList(config) {
|
|
|
90
94
|
knownEpisodes: null,
|
|
91
95
|
animeStatus: null,
|
|
92
96
|
anilistStatus: null,
|
|
97
|
+
posterUrl: null,
|
|
93
98
|
nextAiringAt: null,
|
|
94
99
|
});
|
|
95
100
|
}
|
|
@@ -170,7 +175,7 @@ async function promptAnimeSelection(watchList) {
|
|
|
170
175
|
|
|
171
176
|
// ── Per-anime download ─────────────────────────────────────────────────────
|
|
172
177
|
|
|
173
|
-
async function processAnime(workItem, config, multiBar, auto = false, runQuality = null, runLanguage = null, range = null) {
|
|
178
|
+
async function processAnime(workItem, config, multiBar, auto = false, runQuality = null, runLanguage = null, range = null, eventBus = null) {
|
|
174
179
|
// Per-anime quality: run override > DB override > workItem > global config
|
|
175
180
|
const dbAnimeRow = db.getAnimeBySiteTitle(workItem.title);
|
|
176
181
|
const q = runQuality || dbAnimeRow?.override_quality || workItem.quality || config.quality || '1080p';
|
|
@@ -191,7 +196,8 @@ async function processAnime(workItem, config, multiBar, auto = false, runQuality
|
|
|
191
196
|
if (idMatch) {
|
|
192
197
|
siteAnime = idMatch;
|
|
193
198
|
info(`[${workItem.title}] Matched by AniList ID: ${chalk.bold(siteAnime.title)} (anilist:${siteAnime.anilistId})`);
|
|
194
|
-
|
|
199
|
+
const poster = workItem.posterUrl || siteAnime.poster || null;
|
|
200
|
+
db.saveConfirmedSiteMatch(workItem.title, siteAnime.session, siteAnime.title, poster);
|
|
195
201
|
} else {
|
|
196
202
|
warn(`[${workItem.title}] AniList ID ${workItem.anilistId} not found in search results — falling back to title match.`);
|
|
197
203
|
}
|
|
@@ -326,7 +332,16 @@ async function processAnime(workItem, config, multiBar, auto = false, runQuality
|
|
|
326
332
|
|
|
327
333
|
info(`[${workItem.title}] ${toDownload.length} unwatched episode(s) to download (Q:${q} L:${l}, skipping first ${watched}).`);
|
|
328
334
|
|
|
329
|
-
|
|
335
|
+
if (eventBus) {
|
|
336
|
+
eventBus.emit('broadcast', 'anime:start', {
|
|
337
|
+
animeId: dbRow?.id,
|
|
338
|
+
title: workItem.title,
|
|
339
|
+
episodeCount: toDownload.length,
|
|
340
|
+
quality: q,
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const posterUrl = workItem.posterUrl || null;
|
|
330
345
|
await notify.notifyDownloadStart(cleanTitle, toDownload.length, q, posterUrl);
|
|
331
346
|
|
|
332
347
|
const queue = [...toDownload];
|
|
@@ -354,17 +369,26 @@ async function processAnime(workItem, config, multiBar, auto = false, runQuality
|
|
|
354
369
|
while (!success && attempts < MAX_RETRIES) {
|
|
355
370
|
attempts++;
|
|
356
371
|
try {
|
|
372
|
+
if (eventBus) {
|
|
373
|
+
eventBus.emit('broadcast', 'episode:start', {
|
|
374
|
+
animeId: dbRow?.id,
|
|
375
|
+
title: cleanTitle,
|
|
376
|
+
episode: ep.episode,
|
|
377
|
+
quality: q,
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
|
|
357
381
|
const links = await scraper.getDownloadLinks(siteAnime.session, ep.session, q);
|
|
358
382
|
const link = scraper.pickLink(links, q, l);
|
|
359
383
|
if (!link?.mp4Url) throw new Error('No suitable download link found');
|
|
360
384
|
|
|
361
385
|
const relativeEp = epRelativeMap.get(ep.episode) || ep.episode;
|
|
362
386
|
const filename = buildFilename(displayTitle, ep.episode, season, relativeEp);
|
|
363
|
-
await scraper.downloadEpisode(link.mp4Url, filename, outputDir, multiBar, ep.episode);
|
|
387
|
+
await scraper.downloadEpisode(link.mp4Url, filename, outputDir, multiBar, ep.episode, eventBus, dbRow?.id, cleanTitle);
|
|
364
388
|
|
|
365
389
|
db.markEpisodeStatus(dbRow.id, ep.episode, 'done', filename);
|
|
366
390
|
ok(`[${cleanTitle}] EP${ep.episode} → ${filename}`);
|
|
367
|
-
const posterUrl =
|
|
391
|
+
const posterUrl = workItem.posterUrl || null;
|
|
368
392
|
await notify.notifyEpisode(cleanTitle, relativeEp || ep.episode, q, posterUrl);
|
|
369
393
|
success = true;
|
|
370
394
|
results.downloaded++;
|
|
@@ -373,6 +397,15 @@ async function processAnime(workItem, config, multiBar, auto = false, runQuality
|
|
|
373
397
|
err(`[${workItem.title}] EP${ep.episode} failed: ${e.message}`);
|
|
374
398
|
db.markEpisodeStatus(dbRow.id, ep.episode, 'failed', null, e.message);
|
|
375
399
|
await notify.notifyFailed(workItem.title, ep.episode, e.message);
|
|
400
|
+
if (eventBus) {
|
|
401
|
+
eventBus.emit('broadcast', 'episode:failed', {
|
|
402
|
+
animeId: dbRow?.id,
|
|
403
|
+
title: cleanTitle,
|
|
404
|
+
episode: ep.episode,
|
|
405
|
+
error: e.message,
|
|
406
|
+
willRetry: false,
|
|
407
|
+
});
|
|
408
|
+
}
|
|
376
409
|
results.failed++;
|
|
377
410
|
} else {
|
|
378
411
|
warn(`[${workItem.title}] EP${ep.episode} attempt ${attempts} failed, retrying...`);
|
|
@@ -384,14 +417,24 @@ async function processAnime(workItem, config, multiBar, auto = false, runQuality
|
|
|
384
417
|
});
|
|
385
418
|
|
|
386
419
|
await Promise.all(workers);
|
|
420
|
+
|
|
421
|
+
if (eventBus) {
|
|
422
|
+
eventBus.emit('broadcast', 'anime:complete', {
|
|
423
|
+
animeId: dbRow?.id,
|
|
424
|
+
title: workItem.title,
|
|
425
|
+
...results,
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
|
|
387
429
|
return results;
|
|
388
430
|
}
|
|
389
431
|
|
|
390
432
|
// ── Main cycle ─────────────────────────────────────────────────────────────
|
|
391
433
|
|
|
392
|
-
async function runCycle({ auto = false, quality = null, language = null, range = null, statuses = null } = {}) {
|
|
434
|
+
async function runCycle({ auto = false, quality = null, language = null, range = null, statuses = null, eventBus = null } = {}) {
|
|
393
435
|
await db.initDb();
|
|
394
436
|
const config = loadConfig();
|
|
437
|
+
currentBus = eventBus;
|
|
395
438
|
|
|
396
439
|
console.log(chalk.bold.red('\n ╔══════════════════════════════════╗'));
|
|
397
440
|
console.log(chalk.bold.red(` ║ ANI-AUTO v${version} — DOWNLOAD RUN ║`));
|
|
@@ -402,9 +445,11 @@ async function runCycle({ auto = false, quality = null, language = null, range =
|
|
|
402
445
|
: config;
|
|
403
446
|
const watchList = await resolveWatchList(effectiveConfig);
|
|
404
447
|
|
|
405
|
-
|
|
448
|
+
if (!watchList.length) {
|
|
406
449
|
warn('Watch list is empty. Add entries via AniList or `ani-auto add`.');
|
|
407
450
|
db.logRun({ downloaded: 0, skipped: 0, failed: 0, notes: 'empty watch list' });
|
|
451
|
+
if (eventBus) eventBus.emit('broadcast', 'run:complete', { downloaded: 0, skipped: 0, failed: 0 });
|
|
452
|
+
currentBus = null;
|
|
408
453
|
return;
|
|
409
454
|
}
|
|
410
455
|
|
|
@@ -416,6 +461,8 @@ async function runCycle({ auto = false, quality = null, language = null, range =
|
|
|
416
461
|
selected = await promptAnimeSelection(watchList);
|
|
417
462
|
if (!selected.length) {
|
|
418
463
|
warn('No anime selected — exiting.');
|
|
464
|
+
if (eventBus) eventBus.emit('broadcast', 'run:complete', { downloaded: 0, skipped: 0, failed: 0 });
|
|
465
|
+
currentBus = null;
|
|
419
466
|
return;
|
|
420
467
|
}
|
|
421
468
|
}
|
|
@@ -434,7 +481,7 @@ async function runCycle({ auto = false, quality = null, language = null, range =
|
|
|
434
481
|
const totals = { downloaded: 0, skipped: 0, failed: 0 };
|
|
435
482
|
|
|
436
483
|
for (const item of selected) {
|
|
437
|
-
const result = await processAnime(item, config, multiBar, auto, quality, language, range);
|
|
484
|
+
const result = await processAnime(item, config, multiBar, auto, quality, language, range, eventBus);
|
|
438
485
|
totals.downloaded += result.downloaded;
|
|
439
486
|
totals.skipped += result.skipped;
|
|
440
487
|
totals.failed += result.failed;
|
|
@@ -450,8 +497,14 @@ async function runCycle({ auto = false, quality = null, language = null, range =
|
|
|
450
497
|
|
|
451
498
|
db.logRun(totals);
|
|
452
499
|
await notify.notifySummary(totals);
|
|
500
|
+
|
|
501
|
+
if (eventBus) {
|
|
502
|
+
eventBus.emit('broadcast', 'run:complete', totals);
|
|
503
|
+
}
|
|
504
|
+
currentBus = null;
|
|
505
|
+
return totals;
|
|
453
506
|
}
|
|
454
507
|
|
|
455
508
|
function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
|
|
456
509
|
|
|
457
|
-
module.exports = { runCycle, resolveWatchList };
|
|
510
|
+
module.exports = { runCycle, resolveWatchList, processAnime };
|
package/src/notify.js
CHANGED
|
@@ -3,10 +3,53 @@
|
|
|
3
3
|
const { execSync } = require('child_process');
|
|
4
4
|
const fetch = require('node-fetch');
|
|
5
5
|
const { loadConfig } = require('./config');
|
|
6
|
-
const { proxifyPosterUrl } = require('../utils');
|
|
7
6
|
|
|
8
7
|
const BOT_TOKEN = '8695132702:AAEFDoSL6nkdwDOoz5wmoVkmuhaYa4tNw0o';
|
|
9
8
|
|
|
9
|
+
async function fetchPosterBuffer(photoUrl) {
|
|
10
|
+
if (!photoUrl) return null;
|
|
11
|
+
const res = await fetch(photoUrl, { timeout: 10000 });
|
|
12
|
+
if (!res.ok) {
|
|
13
|
+
throw new Error(`Poster fetch failed: HTTP ${res.status}`);
|
|
14
|
+
}
|
|
15
|
+
const contentType = res.headers.get('content-type') || 'image/jpeg';
|
|
16
|
+
const buffer = await res.buffer();
|
|
17
|
+
if (!buffer || !buffer.length) {
|
|
18
|
+
throw new Error('Poster fetch returned empty body');
|
|
19
|
+
}
|
|
20
|
+
return { buffer, contentType };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function buildMultipartBody(fields, fileField) {
|
|
24
|
+
const boundary = `----ani-auto-${Date.now().toString(16)}`;
|
|
25
|
+
const chunks = [];
|
|
26
|
+
|
|
27
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
28
|
+
chunks.push(Buffer.from(
|
|
29
|
+
`--${boundary}\r\n` +
|
|
30
|
+
`Content-Disposition: form-data; name="${key}"\r\n\r\n` +
|
|
31
|
+
`${value}\r\n`
|
|
32
|
+
));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (fileField) {
|
|
36
|
+
chunks.push(Buffer.from(
|
|
37
|
+
`--${boundary}\r\n` +
|
|
38
|
+
`Content-Disposition: form-data; name="${fileField.name}"; filename="${fileField.filename}"\r\n` +
|
|
39
|
+
`Content-Type: ${fileField.contentType}\r\n\r\n`
|
|
40
|
+
));
|
|
41
|
+
chunks.push(fileField.buffer);
|
|
42
|
+
chunks.push(Buffer.from('\r\n'));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
chunks.push(Buffer.from(`--${boundary}--\r\n`));
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
body: Buffer.concat(chunks),
|
|
49
|
+
boundary,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
10
53
|
async function sendTelegram(payload) {
|
|
11
54
|
const config = loadConfig();
|
|
12
55
|
const chatId = config.telegramChatId;
|
|
@@ -14,11 +57,44 @@ async function sendTelegram(payload) {
|
|
|
14
57
|
try {
|
|
15
58
|
const normalizedPayload = { ...payload };
|
|
16
59
|
if (normalizedPayload.photo) {
|
|
17
|
-
|
|
60
|
+
const poster = await fetchPosterBuffer(normalizedPayload.photo);
|
|
61
|
+
const { body, boundary } = buildMultipartBody(
|
|
62
|
+
{
|
|
63
|
+
chat_id: String(chatId),
|
|
64
|
+
parse_mode: 'HTML',
|
|
65
|
+
caption: normalizedPayload.caption || '',
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
name: 'photo',
|
|
69
|
+
filename: 'poster.jpg',
|
|
70
|
+
contentType: poster.contentType,
|
|
71
|
+
buffer: poster.buffer,
|
|
72
|
+
}
|
|
73
|
+
);
|
|
74
|
+
const res = await fetch(`https://api.telegram.org/bot${BOT_TOKEN}/sendPhoto`, {
|
|
75
|
+
method: 'POST',
|
|
76
|
+
headers: { 'Content-Type': `multipart/form-data; boundary=${boundary}` },
|
|
77
|
+
body,
|
|
78
|
+
timeout: 15000,
|
|
79
|
+
});
|
|
80
|
+
const text = await res.text();
|
|
81
|
+
if (!res.ok) {
|
|
82
|
+
throw new Error(`Telegram API ${res.status}: ${text.slice(0, 200)}`);
|
|
83
|
+
}
|
|
84
|
+
if (text) {
|
|
85
|
+
try {
|
|
86
|
+
const data = JSON.parse(text);
|
|
87
|
+
if (data.ok === false) {
|
|
88
|
+
throw new Error(`Telegram API rejected message: ${data.description || 'unknown error'}`);
|
|
89
|
+
}
|
|
90
|
+
} catch (err) {
|
|
91
|
+
if (err.message.startsWith('Telegram API rejected message:')) throw err;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return;
|
|
18
95
|
}
|
|
19
96
|
|
|
20
|
-
const
|
|
21
|
-
const res = await fetch(`https://api.telegram.org/bot${BOT_TOKEN}/${endpoint}`, {
|
|
97
|
+
const res = await fetch(`https://api.telegram.org/bot${BOT_TOKEN}/sendMessage`, {
|
|
22
98
|
method: 'POST',
|
|
23
99
|
headers: { 'Content-Type': 'application/json' },
|
|
24
100
|
body: JSON.stringify({ chat_id: String(chatId), parse_mode: 'HTML', ...normalizedPayload }),
|
|
@@ -46,14 +122,13 @@ async function sendTelegram(payload) {
|
|
|
46
122
|
async function sendDesktop(title, body, urgency = 'normal', posterUrl = null) {
|
|
47
123
|
try {
|
|
48
124
|
let icon = '';
|
|
49
|
-
|
|
50
|
-
if (imageUrl) {
|
|
125
|
+
if (posterUrl) {
|
|
51
126
|
const os = require('os');
|
|
52
127
|
const path = require('path');
|
|
53
128
|
const fs = require('fs');
|
|
54
129
|
const tmp = path.join(os.tmpdir(), 'ani-auto-poster.jpg');
|
|
55
130
|
try {
|
|
56
|
-
const res = await fetch(
|
|
131
|
+
const res = await fetch(posterUrl);
|
|
57
132
|
fs.writeFileSync(tmp, await res.buffer());
|
|
58
133
|
icon = `-i "${tmp}"`;
|
|
59
134
|
} catch (e) {}
|
package/src/scraper.js
CHANGED
|
@@ -119,8 +119,8 @@ function pickLink(links, quality, language) {
|
|
|
119
119
|
);
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
-
async function downloadEpisode(url, filename, outputDir, multiBar, episodeNum) {
|
|
123
|
-
await downloadFile(url, filename, outputDir, multiBar, episodeNum);
|
|
122
|
+
async function downloadEpisode(url, filename, outputDir, multiBar, episodeNum, eventBus = null, animeId = null, animeTitle = null) {
|
|
123
|
+
await downloadFile(url, filename, outputDir, multiBar, episodeNum, eventBus, animeId, animeTitle);
|
|
124
124
|
}
|
|
125
125
|
|
|
126
126
|
module.exports = { searchAnime, searchAll, getEpisodes, getDownloadLinks, pickLink, downloadEpisode };
|
package/src/server.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const express = require('express');
|
|
4
|
+
const http = require('http');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const { WebSocketServer } = require('ws');
|
|
8
|
+
const cors = require('cors');
|
|
9
|
+
|
|
10
|
+
const configRouter = require('./api/config');
|
|
11
|
+
const anilistRouter = require('./api/anilist');
|
|
12
|
+
const animeRouter = require('./api/anime');
|
|
13
|
+
const matchRouter = require('./api/match');
|
|
14
|
+
const downloadRouter = require('./api/download');
|
|
15
|
+
const runsRouter = require('./api/runs');
|
|
16
|
+
const daemonRouter = require('./api/daemon');
|
|
17
|
+
const updateRouter = require('./api/update');
|
|
18
|
+
|
|
19
|
+
const EventEmitter = require('events');
|
|
20
|
+
const downloadBus = new EventEmitter();
|
|
21
|
+
downloadBus.setMaxListeners(100);
|
|
22
|
+
|
|
23
|
+
const PORT = process.env.PORT || 2640;
|
|
24
|
+
const CLIENT_DIR = path.join(__dirname, '..', 'web', 'dist');
|
|
25
|
+
|
|
26
|
+
const app = express();
|
|
27
|
+
app.use(cors());
|
|
28
|
+
app.use(express.json());
|
|
29
|
+
|
|
30
|
+
app.use('/api/config', configRouter);
|
|
31
|
+
app.use('/api/anilist', anilistRouter);
|
|
32
|
+
app.use('/api/anime', animeRouter);
|
|
33
|
+
app.use('/api/match', matchRouter);
|
|
34
|
+
app.use('/api/runs', runsRouter);
|
|
35
|
+
app.use('/api/daemon', daemonRouter);
|
|
36
|
+
app.use('/api/download', downloadRouter(downloadBus));
|
|
37
|
+
app.use('/api/update', updateRouter);
|
|
38
|
+
|
|
39
|
+
const cfBypassRouter = require('./api/cf-bypass');
|
|
40
|
+
app.use('/api', cfBypassRouter);
|
|
41
|
+
|
|
42
|
+
const paheRouter = require('../anime/app');
|
|
43
|
+
const PaheConfig = require('../anime/utils/config');
|
|
44
|
+
const savedConfig = require('./config').loadConfig();
|
|
45
|
+
if (savedConfig.animepaheCfClearance) {
|
|
46
|
+
PaheConfig.setCfClearance(savedConfig.animepaheCfClearance);
|
|
47
|
+
}
|
|
48
|
+
if (savedConfig.animepaheUserAgent) {
|
|
49
|
+
PaheConfig.userAgent = savedConfig.animepaheUserAgent;
|
|
50
|
+
}
|
|
51
|
+
app.use('/api/pahe', paheRouter);
|
|
52
|
+
|
|
53
|
+
app.use(express.static(CLIENT_DIR));
|
|
54
|
+
app.use((req, res) => {
|
|
55
|
+
if (req.path.startsWith('/api')) return res.status(404).json({ error: 'not found' });
|
|
56
|
+
res.sendFile(path.join(CLIENT_DIR, 'index.html'), (err) => {
|
|
57
|
+
if (err) res.status(404).json({ error: 'not found' });
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const server = http.createServer(app);
|
|
62
|
+
const wss = new WebSocketServer({ server, path: '/ws' });
|
|
63
|
+
|
|
64
|
+
wss.on('connection', (ws) => {
|
|
65
|
+
ws.on('message', (data) => {
|
|
66
|
+
try {
|
|
67
|
+
const msg = JSON.parse(data);
|
|
68
|
+
if (msg.type === 'download:start') {
|
|
69
|
+
downloadBus.emit('ws:start', msg, ws);
|
|
70
|
+
}
|
|
71
|
+
if (msg.type === 'download:cancel') {
|
|
72
|
+
downloadBus.emit('ws:cancel');
|
|
73
|
+
}
|
|
74
|
+
if (msg.type === 'download:pause') {
|
|
75
|
+
downloadBus.emit('ws:pause');
|
|
76
|
+
}
|
|
77
|
+
if (msg.type === 'download:resume') {
|
|
78
|
+
downloadBus.emit('ws:resume');
|
|
79
|
+
}
|
|
80
|
+
} catch (e) {}
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
function broadcast(event, data) {
|
|
85
|
+
const payload = JSON.stringify({ type: event, ...data });
|
|
86
|
+
wss.clients.forEach((client) => {
|
|
87
|
+
if (client.readyState === 1) client.send(payload);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
app.locals.broadcast = broadcast;
|
|
92
|
+
|
|
93
|
+
downloadBus.on('broadcast', (event, data) => broadcast(event, data));
|
|
94
|
+
|
|
95
|
+
function startServer() {
|
|
96
|
+
server.listen(PORT, () => {
|
|
97
|
+
console.log(`ani-auto web ui running at http://localhost:${PORT}`);
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
process.on('SIGINT', () => process.exit(0));
|
|
102
|
+
process.on('SIGTERM', () => process.exit(0));
|
|
103
|
+
|
|
104
|
+
module.exports = { startServer, downloadBus, broadcast, app };
|
|
105
|
+
|
|
106
|
+
if (require.main === module) {
|
|
107
|
+
startServer();
|
|
108
|
+
}
|