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,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
|
|
|
@@ -172,7 +175,7 @@ async function promptAnimeSelection(watchList) {
|
|
|
172
175
|
|
|
173
176
|
// ── Per-anime download ─────────────────────────────────────────────────────
|
|
174
177
|
|
|
175
|
-
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) {
|
|
176
179
|
// Per-anime quality: run override > DB override > workItem > global config
|
|
177
180
|
const dbAnimeRow = db.getAnimeBySiteTitle(workItem.title);
|
|
178
181
|
const q = runQuality || dbAnimeRow?.override_quality || workItem.quality || config.quality || '1080p';
|
|
@@ -193,7 +196,8 @@ async function processAnime(workItem, config, multiBar, auto = false, runQuality
|
|
|
193
196
|
if (idMatch) {
|
|
194
197
|
siteAnime = idMatch;
|
|
195
198
|
info(`[${workItem.title}] Matched by AniList ID: ${chalk.bold(siteAnime.title)} (anilist:${siteAnime.anilistId})`);
|
|
196
|
-
|
|
199
|
+
const poster = workItem.posterUrl || siteAnime.poster || null;
|
|
200
|
+
db.saveConfirmedSiteMatch(workItem.title, siteAnime.session, siteAnime.title, poster);
|
|
197
201
|
} else {
|
|
198
202
|
warn(`[${workItem.title}] AniList ID ${workItem.anilistId} not found in search results — falling back to title match.`);
|
|
199
203
|
}
|
|
@@ -328,6 +332,15 @@ async function processAnime(workItem, config, multiBar, auto = false, runQuality
|
|
|
328
332
|
|
|
329
333
|
info(`[${workItem.title}] ${toDownload.length} unwatched episode(s) to download (Q:${q} L:${l}, skipping first ${watched}).`);
|
|
330
334
|
|
|
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
|
+
|
|
331
344
|
const posterUrl = workItem.posterUrl || null;
|
|
332
345
|
await notify.notifyDownloadStart(cleanTitle, toDownload.length, q, posterUrl);
|
|
333
346
|
|
|
@@ -356,13 +369,22 @@ async function processAnime(workItem, config, multiBar, auto = false, runQuality
|
|
|
356
369
|
while (!success && attempts < MAX_RETRIES) {
|
|
357
370
|
attempts++;
|
|
358
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
|
+
|
|
359
381
|
const links = await scraper.getDownloadLinks(siteAnime.session, ep.session, q);
|
|
360
382
|
const link = scraper.pickLink(links, q, l);
|
|
361
383
|
if (!link?.mp4Url) throw new Error('No suitable download link found');
|
|
362
384
|
|
|
363
385
|
const relativeEp = epRelativeMap.get(ep.episode) || ep.episode;
|
|
364
386
|
const filename = buildFilename(displayTitle, ep.episode, season, relativeEp);
|
|
365
|
-
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);
|
|
366
388
|
|
|
367
389
|
db.markEpisodeStatus(dbRow.id, ep.episode, 'done', filename);
|
|
368
390
|
ok(`[${cleanTitle}] EP${ep.episode} → ${filename}`);
|
|
@@ -375,6 +397,15 @@ async function processAnime(workItem, config, multiBar, auto = false, runQuality
|
|
|
375
397
|
err(`[${workItem.title}] EP${ep.episode} failed: ${e.message}`);
|
|
376
398
|
db.markEpisodeStatus(dbRow.id, ep.episode, 'failed', null, e.message);
|
|
377
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
|
+
}
|
|
378
409
|
results.failed++;
|
|
379
410
|
} else {
|
|
380
411
|
warn(`[${workItem.title}] EP${ep.episode} attempt ${attempts} failed, retrying...`);
|
|
@@ -386,14 +417,24 @@ async function processAnime(workItem, config, multiBar, auto = false, runQuality
|
|
|
386
417
|
});
|
|
387
418
|
|
|
388
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
|
+
|
|
389
429
|
return results;
|
|
390
430
|
}
|
|
391
431
|
|
|
392
432
|
// ── Main cycle ─────────────────────────────────────────────────────────────
|
|
393
433
|
|
|
394
|
-
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 } = {}) {
|
|
395
435
|
await db.initDb();
|
|
396
436
|
const config = loadConfig();
|
|
437
|
+
currentBus = eventBus;
|
|
397
438
|
|
|
398
439
|
console.log(chalk.bold.red('\n ╔══════════════════════════════════╗'));
|
|
399
440
|
console.log(chalk.bold.red(` ║ ANI-AUTO v${version} — DOWNLOAD RUN ║`));
|
|
@@ -404,9 +445,11 @@ async function runCycle({ auto = false, quality = null, language = null, range =
|
|
|
404
445
|
: config;
|
|
405
446
|
const watchList = await resolveWatchList(effectiveConfig);
|
|
406
447
|
|
|
407
|
-
|
|
448
|
+
if (!watchList.length) {
|
|
408
449
|
warn('Watch list is empty. Add entries via AniList or `ani-auto add`.');
|
|
409
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;
|
|
410
453
|
return;
|
|
411
454
|
}
|
|
412
455
|
|
|
@@ -418,6 +461,8 @@ async function runCycle({ auto = false, quality = null, language = null, range =
|
|
|
418
461
|
selected = await promptAnimeSelection(watchList);
|
|
419
462
|
if (!selected.length) {
|
|
420
463
|
warn('No anime selected — exiting.');
|
|
464
|
+
if (eventBus) eventBus.emit('broadcast', 'run:complete', { downloaded: 0, skipped: 0, failed: 0 });
|
|
465
|
+
currentBus = null;
|
|
421
466
|
return;
|
|
422
467
|
}
|
|
423
468
|
}
|
|
@@ -436,7 +481,7 @@ async function runCycle({ auto = false, quality = null, language = null, range =
|
|
|
436
481
|
const totals = { downloaded: 0, skipped: 0, failed: 0 };
|
|
437
482
|
|
|
438
483
|
for (const item of selected) {
|
|
439
|
-
const result = await processAnime(item, config, multiBar, auto, quality, language, range);
|
|
484
|
+
const result = await processAnime(item, config, multiBar, auto, quality, language, range, eventBus);
|
|
440
485
|
totals.downloaded += result.downloaded;
|
|
441
486
|
totals.skipped += result.skipped;
|
|
442
487
|
totals.failed += result.failed;
|
|
@@ -452,8 +497,14 @@ async function runCycle({ auto = false, quality = null, language = null, range =
|
|
|
452
497
|
|
|
453
498
|
db.logRun(totals);
|
|
454
499
|
await notify.notifySummary(totals);
|
|
500
|
+
|
|
501
|
+
if (eventBus) {
|
|
502
|
+
eventBus.emit('broadcast', 'run:complete', totals);
|
|
503
|
+
}
|
|
504
|
+
currentBus = null;
|
|
505
|
+
return totals;
|
|
455
506
|
}
|
|
456
507
|
|
|
457
508
|
function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
|
|
458
509
|
|
|
459
|
-
module.exports = { runCycle, resolveWatchList };
|
|
510
|
+
module.exports = { runCycle, resolveWatchList, processAnime };
|
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
|
+
}
|
package/utils.js
CHANGED
|
@@ -16,9 +16,9 @@ function loadConfig() {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
const config = loadConfig();
|
|
19
|
-
const API_DOMAIN =
|
|
20
|
-
const SEARCH_API = `${API_DOMAIN}/api/
|
|
21
|
-
const RELEASES_API = `${API_DOMAIN}/api/
|
|
19
|
+
const API_DOMAIN = `http://localhost:${process.env.PORT || 2640}`;
|
|
20
|
+
const SEARCH_API = `${API_DOMAIN}/api/pahe/search`;
|
|
21
|
+
const RELEASES_API = `${API_DOMAIN}/api/pahe`;
|
|
22
22
|
const USER_AGENT = 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Mobile Safari/537.36';
|
|
23
23
|
|
|
24
24
|
// --- Helpers ---
|
|
@@ -36,7 +36,7 @@ const formatBytes = (bytes, decimals = 2) => {
|
|
|
36
36
|
};
|
|
37
37
|
|
|
38
38
|
// --- Download Logic ---
|
|
39
|
-
async function downloadFile(url, filename, folder, multiBar, episode) {
|
|
39
|
+
async function downloadFile(url, filename, folder, multiBar, episode, eventBus = null, animeId = null, animeTitle = null) {
|
|
40
40
|
const filePath = path.join(folder, filename);
|
|
41
41
|
let downloadedBytes = 0;
|
|
42
42
|
let fileExists = false;
|
|
@@ -107,6 +107,7 @@ async function downloadFile(url, filename, folder, multiBar, episode) {
|
|
|
107
107
|
let currentProgress = downloadedBytes;
|
|
108
108
|
const startTime = Date.now();
|
|
109
109
|
|
|
110
|
+
let lastEmit = 0;
|
|
110
111
|
while (true) {
|
|
111
112
|
const { done, value } = await reader.read();
|
|
112
113
|
if (done) break;
|
|
@@ -114,6 +115,22 @@ async function downloadFile(url, filename, folder, multiBar, episode) {
|
|
|
114
115
|
fileStream.write(Buffer.from(value));
|
|
115
116
|
currentProgress += value.length;
|
|
116
117
|
|
|
118
|
+
if (eventBus && (currentProgress - lastEmit > 524288 || currentProgress === downloadedBytes + 1)) {
|
|
119
|
+
lastEmit = currentProgress;
|
|
120
|
+
const elapsed = (Date.now() - startTime) / 1000;
|
|
121
|
+
const speed = elapsed > 0 ? (currentProgress - downloadedBytes) / elapsed : 0;
|
|
122
|
+
const percent = totalBytes ? Math.round((currentProgress / totalBytes) * 100) : 0;
|
|
123
|
+
eventBus.emit('broadcast', 'episode:progress', {
|
|
124
|
+
animeId,
|
|
125
|
+
title: animeTitle,
|
|
126
|
+
episode,
|
|
127
|
+
percent,
|
|
128
|
+
speed: formatBytes(speed) + '/s',
|
|
129
|
+
downloaded: currentProgress,
|
|
130
|
+
total: totalBytes,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
117
134
|
if (progressBar) {
|
|
118
135
|
const elapsed = (Date.now() - startTime) / 1000;
|
|
119
136
|
const speed = elapsed > 0 ? (currentProgress - downloadedBytes) / elapsed : 0;
|
package/web/index.html
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en" class="dark">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>ani-auto</title>
|
|
7
|
+
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><text y='28' font-size='28' fill='%23CC0000'>A</text></svg>" />
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<div id="app"></div>
|
|
11
|
+
<script type="module" src="/src/main.js"></script>
|
|
12
|
+
</body>
|
|
13
|
+
</html>
|