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,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 { const viewer = await getViewer(answers.anilistToken); spinner.succeed(`Logged in as ${chalk.bold(viewer.name)}`); }
104
- catch { spinner.warn('Could not verify token - saved anyway, check it manually.'); }
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, // set to false via `ani-auto daemon disable`
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 { return { ...DEFAULTS, ...JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8')) }; }
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
@@ -22,7 +22,7 @@ async function safeRun() {
22
22
  return;
23
23
  }
24
24
 
25
- // Check internet before running
25
+ // Check internet before running
26
26
  const online = await checkInternet();
27
27
  if (!online) {
28
28
  warn('No internet connection — waiting for network...');
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
- _run(`
122
- INSERT INTO anime (title, site_id, anilist_id, source, quality, language)
123
- VALUES (?, ?, ?, ?, ?, ?)
124
- ON CONFLICT(site_id) DO UPDATE SET
125
- title = excluded.title,
126
- anilist_id = COALESCE(excluded.anilist_id, anime.anilist_id),
127
- quality = COALESCE(excluded.quality, anime.quality),
128
- language = COALESCE(excluded.language, anime.language)
129
- `, [title, siteId || title, anilistId || null, source || 'anilist', quality || null, language || null]);
130
- return _get('SELECT * FROM anime WHERE site_id = ?', [siteId || title]);
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
- function info(msg) { console.log(`${chalk.gray(ts())} ${chalk.cyan('ℹ')} ${msg}`); }
19
- function ok(msg) { console.log(`${chalk.gray(ts())} ${chalk.green('✔')} ${msg}`); }
20
- function warn(msg) { console.log(`${chalk.gray(ts())} ${chalk.yellow('')} ${msg}`); }
21
- function err(msg) { console.log(`${chalk.gray(ts())} ${chalk.red('')} ${msg}`); }
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
- db.saveConfirmedSiteMatch(workItem.title, siteAnime.session, siteAnime.title, siteAnime.poster || null);
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
- if (!watchList.length) {
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 = config.apiBaseUrl || 'https://aor-rex-anikuro-api.hf.space';
20
- const SEARCH_API = `${API_DOMAIN}/api/anime/search`;
21
- const RELEASES_API = `${API_DOMAIN}/api/anime`;
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>