ani-auto 1.2.5 → 1.3.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/README.md CHANGED
@@ -82,7 +82,6 @@ Commands:
82
82
 
83
83
  ```bash
84
84
  ani-auto config --set-quality 1080p
85
- ani-auto config --set-language sub
86
85
  ani-auto config --set-concurrency 3
87
86
  ani-auto config --set-output ~/videos/anime
88
87
  ani-auto config --set-interval 60
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ani-auto",
3
- "version": "1.2.5",
3
+ "version": "1.3.0",
4
4
  "description": "Automatic anime episode downloader",
5
5
  "bin": {
6
6
  "ani-auto": "./src/cli.js"
package/src/cli.js CHANGED
@@ -31,8 +31,7 @@ program
31
31
  .action(async () => {
32
32
  banner();
33
33
  const current = loadConfig();
34
- console.log(chalk.cyan(' Let\'s configure your auto-downloader.\n'));
35
-
34
+ console.log(chalk.cyan(' Let\'s configure your auto-downloader.'));
36
35
  const { anilistUsername } = await inquirer.prompt([{
37
36
  type: 'input', name: 'anilistUsername',
38
37
  message: 'AniList username (leave blank to skip AniList integration):',
@@ -58,8 +57,8 @@ program
58
57
  for (const opener of openers) {
59
58
  try { spawn(opener, [url], { stdio: 'ignore', detached: true, env: process.env }).unref(); opened = true; break; } catch (e) {}
60
59
  }
61
- console.log(chalk.cyan(`\n ${opened ? 'Browser opened!' : 'Visit:'} ${url}`));
62
- console.log(chalk.gray(' Authorize, then copy the token from the URL (the part after access_token=).\n'));
60
+ console.log(chalk.cyan(` ${opened ? 'Browser opened!' : 'Visit:'} ${url}`));
61
+ console.log(chalk.gray(' Authorize, then copy the token from the URL (the part after access_token=).'));
63
62
  await new Promise(r => setTimeout(r, 1500));
64
63
  }
65
64
 
@@ -86,12 +85,11 @@ program
86
85
  }
87
86
 
88
87
  const answers = await inquirer.prompt([
89
- { type: 'list', name: 'quality', message: 'Default download quality:', choices: ['1080p', '720p', '480p', '360p'], default: current.quality || '1080p' },
90
- { type: 'list', name: 'language', message: 'Default audio language:', choices: ['sub', 'dub'], default: current.language || 'sub' },
88
+ { type: 'list', name: 'quality', message: 'Default download quality:', choices: ['1080p', '720p', '360p'], default: current.quality || '1080p' },
91
89
  { type: 'number', name: 'concurrency', message: 'Parallel downloads per run (1-10):', default: current.concurrency || 2, validate: v => (v >= 1 && v <= 10) ? true : 'Enter a number 1–10' },
92
90
  { type: 'input', name: 'outputDir', message: 'Output directory for downloads:', default: current.outputDir },
93
91
  { type: 'number', name: 'daemonIntervalMinutes', message: 'Daemon polling interval (minutes):', default: current.daemonIntervalMinutes || 30 },
94
- { type: 'input', name: 'telegramChatId', message: 'Telegram chat ID for notifications (leave blank to skip):\n → Message @userinfobot on Telegram to get your ID:', default: current.telegramChatId || '' },
92
+ { type: 'input', name: 'telegramChatId', message: 'Telegram chat ID for notifications (leave blank to skip): → Message @userinfobot on Telegram to get your ID:', default: current.telegramChatId || '' },
95
93
  ]);
96
94
  answers.anilistUsername = anilistUsername;
97
95
  answers.anilistToken = anilistToken;
@@ -100,7 +98,7 @@ program
100
98
  if (answers.anilistToken) {
101
99
  const spinner = ora('Verifying AniList token...').start();
102
100
  try { const viewer = await getViewer(answers.anilistToken); spinner.succeed(`Logged in as ${chalk.bold(viewer.name)}`); }
103
- catch { spinner.warn('Could not verify token saved anyway, check it manually.'); }
101
+ catch { spinner.warn('Could not verify token - saved anyway, check it manually.'); }
104
102
  }
105
103
 
106
104
  updateConfig({
@@ -108,16 +106,15 @@ program
108
106
  anilistToken: answers.anilistToken || null,
109
107
  watchStatuses: answers.watchStatuses || current.watchStatuses,
110
108
  quality: answers.quality,
111
- language: answers.language,
112
109
  concurrency: answers.concurrency,
113
110
  outputDir: answers.outputDir,
114
111
  daemonIntervalMinutes: answers.daemonIntervalMinutes,
115
112
  telegramChatId: answers.telegramChatId || null,
116
113
  });
117
114
 
118
- console.log(chalk.green(`\n ✔ Config saved to ${CONFIG_FILE}\n`));
115
+ console.log(chalk.green(` ✔ Config saved to ${CONFIG_FILE}`));
119
116
  console.log(` Run ${chalk.bold('ani-auto check')} to do a manual download cycle.`);
120
- console.log(` Run ${chalk.bold('ani-auto daemon')} to start the background daemon.\n`);
117
+ console.log(` Run ${chalk.bold('ani-auto daemon')} to start the background daemon.`);
121
118
  });
122
119
 
123
120
  program
@@ -139,7 +136,7 @@ program
139
136
  type: 'list',
140
137
  name: 'pickedQuality',
141
138
  message: 'Quality for this run:',
142
- choices: ['1080p', '720p', '480p', '360p'],
139
+ choices: ['1080p', '720p', '360p'],
143
140
  default: quality,
144
141
  },
145
142
  {
@@ -152,7 +149,7 @@ program
152
149
  quality = pickedQuality;
153
150
  if (saveAsDefault) {
154
151
  updateConfig({ quality });
155
- console.log(chalk.green(` ✔ Saved default quality: ${quality}\n`));
152
+ console.log(chalk.green(` ✔ Saved default quality: ${quality}`));
156
153
  }
157
154
 
158
155
  const { pickedStatuses } = await inquirer.prompt([{
@@ -214,7 +211,7 @@ program
214
211
 
215
212
  if (!all.length) {
216
213
  console.log(chalk.yellow(' No anime tracked yet.'));
217
- console.log(` Use ${chalk.bold('ani-auto add "<title>"')} to add one manually.\n`);
214
+ console.log(` Use ${chalk.bold('ani-auto add "<title>"')} to add one manually.`);
218
215
  return;
219
216
  }
220
217
 
@@ -249,7 +246,7 @@ program
249
246
  const { picked } = await inquirer.prompt([{
250
247
  type: 'list', name: 'picked', message: 'Select the anime:',
251
248
  choices: results.map(r => ({
252
- name: `${pickTitle(r.titles)} (${r.year || '?'}) ${r.episodes ? r.episodes + ' eps' : 'ongoing'} [${r.status}]`,
249
+ name: `${pickTitle(r.titles)} (${r.year || '?'}) - ${r.episodes ? r.episodes + ' eps' : 'ongoing'} [${r.status}]`,
253
250
  value: r,
254
251
  })),
255
252
  }]);
@@ -279,7 +276,7 @@ program
279
276
  );
280
277
  });
281
278
  if (inAnilist) {
282
- console.log(chalk.yellow(` "${resolvedTitle}" is already in your AniList list (${inAnilist.status}) no need to add manually.`));
279
+ console.log(chalk.yellow(` "${resolvedTitle}" is already in your AniList list (${inAnilist.status}) - no need to add manually.`));
283
280
  return;
284
281
  }
285
282
  } catch (e) {}
@@ -290,7 +287,7 @@ program
290
287
  updateConfig({ manualList });
291
288
  db.upsertAnime({ title: resolvedTitle, siteId: resolvedTitle, anilistId: picked.anilistId, source: 'manual', quality: entry.quality, language: entry.language });
292
289
 
293
- console.log(chalk.green(`\n ✔ Added ${resolvedTitle} to manual list.`));
290
+ console.log(chalk.green(` ✔ Added ${resolvedTitle} to manual list.`));
294
291
  if (opts.quality) console.log(` Quality: ${opts.quality}`);
295
292
  if (opts.language) console.log(` Language: ${opts.language}`);
296
293
  console.log('');
@@ -305,7 +302,7 @@ program
305
302
  const config = loadConfig();
306
303
  const manualList = config.manualList || [];
307
304
 
308
- if (!manualList.length) { console.log(chalk.yellow(' Your manual list is empty.\n')); return; }
305
+ if (!manualList.length) { console.log(chalk.yellow(' Your manual list is empty.')); return; }
309
306
 
310
307
  let target = title;
311
308
  if (!target) {
@@ -317,14 +314,14 @@ program
317
314
  }
318
315
 
319
316
  const exists = manualList.find(m => m.title.toLowerCase() === target.toLowerCase());
320
- if (!exists) { console.log(chalk.yellow(` "${target}" is not in your manual list.\n`)); return; }
317
+ if (!exists) { console.log(chalk.yellow(` "${target}" is not in your manual list.`)); return; }
321
318
 
322
319
  const { confirm } = await inquirer.prompt([{ type: 'confirm', name: 'confirm', message: `Remove "${target}" and all its episode history?`, default: false }]);
323
320
  if (!confirm) return;
324
321
 
325
322
  updateConfig({ manualList: manualList.filter(m => m.title.toLowerCase() !== target.toLowerCase()) });
326
323
  db.removeAnime(target);
327
- console.log(chalk.green(` ✔ Removed "${target}".\n`));
324
+ console.log(chalk.green(` ✔ Removed "${target}".`));
328
325
  });
329
326
 
330
327
  async function pickFromWatchList(message) {
@@ -332,9 +329,9 @@ async function pickFromWatchList(message) {
332
329
  const config = loadConfig();
333
330
  const watchList = await resolveWatchList(config);
334
331
 
335
- if (!watchList.length) { console.log(chalk.yellow(' Watch list is empty.\n')); return null; }
332
+ if (!watchList.length) { console.log(chalk.yellow(' Watch list is empty.')); return null; }
336
333
 
337
- console.log(chalk.bold('\n Watch list:'));
334
+ console.log(chalk.bold(' Watch list:'));
338
335
  watchList.forEach((item, i) => console.log(` ${chalk.cyan(i + 1)}. ${item.title}`));
339
336
  console.log('');
340
337
 
@@ -357,7 +354,7 @@ program
357
354
  const item = await pickFromWatchList('Select anime to enable:');
358
355
  if (!item) return;
359
356
  db.setAnimeEnabled(item.title, true);
360
- console.log(chalk.green(`\n ✔ Enabled "${item.title}".\n`));
357
+ console.log(chalk.green(` ✔ Enabled "${item.title}".`));
361
358
  });
362
359
 
363
360
  program
@@ -368,7 +365,7 @@ program
368
365
  const item = await pickFromWatchList('Select anime to disable:');
369
366
  if (!item) return;
370
367
  db.setAnimeEnabled(item.title, false);
371
- console.log(chalk.yellow(`\n ✔ Disabled "${item.title}".\n`));
368
+ console.log(chalk.yellow(` ✔ Disabled "${item.title}".`));
372
369
  });
373
370
 
374
371
  program
@@ -379,7 +376,7 @@ program
379
376
  banner();
380
377
  await db.initDb();
381
378
  const runs = db.getRecentRuns(parseInt(opts.runs, 10));
382
- if (!runs.length) { console.log(chalk.yellow(' No runs recorded yet. Run `ani-auto check` to start.\n')); return; }
379
+ if (!runs.length) { console.log(chalk.yellow(' No runs recorded yet. Run `ani-auto check` to start.')); return; }
383
380
 
384
381
  console.log(chalk.bold(` ${'Time'.padEnd(20)} ${'Downloaded'.padEnd(12)} ${'Skipped'.padEnd(10)} Failed`));
385
382
  console.log(' ' + '─'.repeat(55));
@@ -407,7 +404,7 @@ program
407
404
  const { getConfigPath } = require('./config');
408
405
  const configPath = getConfigPath();
409
406
  const editor = process.env.EDITOR || process.env.VISUAL || 'nano';
410
- console.log(chalk.cyan(` Opening config in ${editor}: ${configPath}\n`));
407
+ console.log(chalk.cyan(` Opening config in ${editor}: ${configPath}`));
411
408
  try { execSync(`${editor} "${configPath}"`, { stdio: 'inherit' }); }
412
409
  catch (e) { console.log(chalk.yellow(` Could not open editor. Config is at: ${configPath}`)); }
413
410
  return;
@@ -423,11 +420,11 @@ program
423
420
 
424
421
  if (Object.keys(updates).length) {
425
422
  updateConfig(updates);
426
- console.log(chalk.green(' ✔ Config updated:\n'));
423
+ console.log(chalk.green(' ✔ Config updated:'));
427
424
  for (const [k, v] of Object.entries(updates)) console.log(` ${chalk.bold(k)}: ${v}`);
428
425
  } else {
429
426
  const config = loadConfig();
430
- console.log(chalk.bold(' Current configuration:\n'));
427
+ console.log(chalk.bold(' Current configuration:'));
431
428
  for (const [k, v] of Object.entries(config)) {
432
429
  if (k === 'anilistToken' && v) console.log(` ${chalk.cyan(k.padEnd(26))} ${chalk.gray('[set]')}`);
433
430
  else console.log(` ${chalk.cyan(k.padEnd(26))} ${JSON.stringify(v)}`);
@@ -436,6 +433,43 @@ program
436
433
  console.log('');
437
434
  });
438
435
 
436
+ // ── notify ───────────────────────────────────────────────────────────────
437
+
438
+ program
439
+ .command('notify [action]')
440
+ .description('Enable or disable Telegram notifications (on, off, status)')
441
+ .action(async (action) => {
442
+ banner();
443
+ const config = loadConfig();
444
+
445
+ if (!action) {
446
+ const enabled = !!config.telegramChatId && config.telegramNotify !== false;
447
+ console.log(` Telegram notifications: ${enabled ? chalk.green('enabled') : chalk.gray('disabled')}`);
448
+ if (!config.telegramChatId) console.log(chalk.yellow(' No chat ID set - run ani-auto setup to configure. '));
449
+ else console.log('');
450
+ return;
451
+ }
452
+
453
+ switch (action.toLowerCase()) {
454
+ case 'on':
455
+ if (!config.telegramChatId) {
456
+ console.log(chalk.yellow(' No chat ID set - run ani-auto setup first. '));
457
+ return;
458
+ }
459
+ updateConfig({ telegramNotify: true });
460
+ console.log(chalk.green(' ✔ Telegram notifications enabled. '));
461
+ break;
462
+
463
+ case 'off':
464
+ updateConfig({ telegramNotify: false });
465
+ console.log(chalk.yellow(' ✔ Telegram notifications disabled. '));
466
+ break;
467
+
468
+ default:
469
+ console.log(chalk.yellow(` Unknown action "${action}". Use: on, off`));
470
+ }
471
+ });
472
+
439
473
  // ── update ───────────────────────────────────────────────────────────────
440
474
 
441
475
  program
@@ -454,12 +488,12 @@ program
454
488
  spinner.stop();
455
489
 
456
490
  if (latest === version) {
457
- console.log(chalk.green(` ✔ Already on the latest version (v${version}).\n`));
491
+ console.log(chalk.green(` ✔ Already on the latest version (v${version}).`));
458
492
  return;
459
493
  }
460
494
 
461
495
  console.log(chalk.cyan(` Current version: ${chalk.bold(version)}`));
462
- console.log(chalk.cyan(` Latest version: ${chalk.bold(latest)}\n`));
496
+ console.log(chalk.cyan(` Latest version: ${chalk.bold(latest)}`));
463
497
 
464
498
  const { confirm } = await inquirer.prompt([{
465
499
  type: 'confirm',
@@ -473,7 +507,7 @@ program
473
507
  const installing = ora(`Installing v${latest}...`).start();
474
508
  try {
475
509
  execSync('npm install -g ani-auto@latest', { stdio: 'pipe' });
476
- installing.succeed(`Updated to v${latest} restart your terminal to use the new version.`);
510
+ installing.succeed(`Updated to v${latest} - restart your terminal to use the new version.`);
477
511
  } catch (e) {
478
512
  installing.fail('Update failed. Try manually: npm install -g ani-auto@latest');
479
513
  }
@@ -486,15 +520,107 @@ program
486
520
  // ── daemon ────────────────────────────────────────────────────────────────
487
521
 
488
522
  program
489
- .command('daemon')
490
- .description('Start the background daemon')
491
- .option('--auto', 'Skip prompts, auto-select all (default: true for daemon)')
492
- .action(() => { require('./daemon'); });
523
+ .command('daemon [action]')
524
+ .description('Manage the background daemon (start, stop, restart, status, install)')
525
+ .action(async (action) => {
526
+ const { execSync, spawnSync } = require('child_process');
527
+
528
+ const SERVICE = 'ani-auto';
529
+ const run = (cmd) => {
530
+ try {
531
+ const r = spawnSync(cmd, { shell: true, encoding: 'utf8' });
532
+ return { out: r.stdout?.trim(), err: r.stderr?.trim(), code: r.status };
533
+ } catch (e) {
534
+ return { out: '', err: e.message, code: 1 };
535
+ }
536
+ };
537
+
538
+ // No action - run inline (legacy behaviour)
539
+ if (!action) {
540
+ require('./daemon');
541
+ return;
542
+ }
543
+
544
+ switch (action.toLowerCase()) {
545
+
546
+ case 'install': {
547
+ banner();
548
+ const nodeBin = run('which node').out;
549
+ const daemonPath = run('npm root -g').out + '/ani-auto/src/daemon.js';
550
+ const serviceContent = [
551
+ '[Unit]',
552
+ 'Description=ani-auto daemon',
553
+ 'After=network-online.target',
554
+ '',
555
+ '[Service]',
556
+ `ExecStart=${nodeBin} ${daemonPath}`,
557
+ 'Restart=on-failure',
558
+ 'RestartSec=10',
559
+ '',
560
+ '[Install]',
561
+ 'WantedBy=default.target',
562
+ ].join('\n');
563
+
564
+ const fs = require('fs');
565
+ const os = require('os');
566
+ const path = require('path');
567
+ const dir = path.join(os.homedir(), '.config', 'systemd', 'user');
568
+ fs.mkdirSync(dir, { recursive: true });
569
+ fs.writeFileSync(path.join(dir, `${SERVICE}.service`), serviceContent);
570
+
571
+ run('systemctl --user daemon-reload');
572
+ run(`systemctl --user enable ${SERVICE}`);
573
+ run(`systemctl --user start ${SERVICE}`);
574
+ console.log(chalk.green(` ✔ ani-auto daemon installed and started.`));
575
+ console.log(chalk.gray(` Run ${chalk.bold('ani-auto daemon status')} to check.
576
+ `));
577
+ break;
578
+ }
579
+
580
+ case 'start': {
581
+ const r = run(`systemctl --user start ${SERVICE}`);
582
+ if (r.code === 0) console.log(chalk.green(' ✔ Daemon started. '));
583
+ else console.log(chalk.red(` ✖ Failed to start: ${r.err}
584
+ Try: ani-auto daemon install`));
585
+ break;
586
+ }
587
+
588
+ case 'stop': {
589
+ const r = run(`systemctl --user stop ${SERVICE}`);
590
+ if (r.code === 0) console.log(chalk.yellow(' ✔ Daemon stopped. '));
591
+ else console.log(chalk.red(` ✖ Failed to stop: ${r.err}`));
592
+ break;
593
+ }
594
+
595
+ case 'restart': {
596
+ const r = run(`systemctl --user restart ${SERVICE}`);
597
+ if (r.code === 0) console.log(chalk.green(' ✔ Daemon restarted. '));
598
+ else console.log(chalk.red(` ✖ Failed to restart: ${r.err}`));
599
+ break;
600
+ }
601
+
602
+ case 'status': {
603
+ const r = run(`systemctl --user status ${SERVICE}`);
604
+ console.log(r.out || r.err);
605
+ break;
606
+ }
607
+
608
+ case 'logs': {
609
+ console.log(chalk.gray(' Streaming logs (Ctrl+C to stop)... '));
610
+ const { spawn } = require('child_process');
611
+ spawn('journalctl', ['--user', '-u', SERVICE, '-f'], { stdio: 'inherit' });
612
+ break;
613
+ }
614
+
615
+ default:
616
+ console.log(chalk.yellow(` Unknown action "${action}". Use: install, start, stop, restart, status, logs`));
617
+ }
618
+ });
493
619
 
494
620
  program
495
621
  .name('ani-auto')
496
622
  .version(version)
497
- .description('Automatic anime episode downloader AniList-aware')
623
+ .description('Automatic anime episode downloader - AniList-aware')
498
624
  .parse(process.argv);
499
625
 
500
626
  if (!process.argv.slice(2).length) { banner(); program.help(); }
package/src/db.js CHANGED
@@ -56,6 +56,7 @@ function migrate(db) {
56
56
  language TEXT,
57
57
  confirmed_site_id TEXT,
58
58
  confirmed_site_title TEXT,
59
+ anilist_title TEXT,
59
60
  added_at TEXT NOT NULL DEFAULT (datetime('now'))
60
61
  )
61
62
  `);
@@ -74,6 +75,7 @@ function migrate(db) {
74
75
  // Add confirmed_site columns if upgrading from older DB
75
76
  try { db.run('ALTER TABLE anime ADD COLUMN confirmed_site_id TEXT'); } catch(e) {}
76
77
  try { db.run('ALTER TABLE anime ADD COLUMN confirmed_site_title TEXT'); } catch(e) {}
78
+ try { db.run('ALTER TABLE anime ADD COLUMN anilist_title TEXT'); } catch(e) {}
77
79
 
78
80
  db.run(`
79
81
  CREATE TABLE IF NOT EXISTS run_log (
@@ -168,6 +170,13 @@ function isEpisodeDownloaded(animeDbId, episodeNum) {
168
170
  );
169
171
  }
170
172
 
173
+ function getEpisodeRow(animeDbId, episodeNum) {
174
+ return _get(
175
+ 'SELECT * FROM episodes WHERE anime_id = ? AND episode_num = ?',
176
+ [animeDbId, episodeNum]
177
+ );
178
+ }
179
+
171
180
  function markEpisodeStatus(animeDbId, episodeNum, status, filename = null, error = null) {
172
181
  _run(`
173
182
  INSERT INTO episodes (anime_id, episode_num, status, filename, downloaded_at, error)
@@ -209,22 +218,27 @@ function getDbInstance() {
209
218
  }
210
219
 
211
220
  function getAnimeBySiteTitle(title) {
212
- return _get('SELECT * FROM anime WHERE LOWER(title) = LOWER(?)', [title]);
221
+ // Check by anilist_title first, then fall back to title column
222
+ return _get('SELECT * FROM anime WHERE LOWER(anilist_title) = LOWER(?)', [title])
223
+ || _get('SELECT * FROM anime WHERE LOWER(title) = LOWER(?)', [title]);
213
224
  }
214
225
 
215
- function saveConfirmedSiteMatch(title, confirmedSiteId, confirmedSiteTitle) {
216
- // Try to update existing row first
217
- const existing = _get('SELECT id FROM anime WHERE LOWER(title) = LOWER(?)', [title]);
226
+ function saveConfirmedSiteMatch(anilistTitle, confirmedSiteId, confirmedSiteTitle) {
227
+ // Look up by anilist_title, AniList title matching title col, or site title matching title col
228
+ const existing =
229
+ _get('SELECT id FROM anime WHERE LOWER(anilist_title) = LOWER(?)', [anilistTitle]) ||
230
+ _get('SELECT id FROM anime WHERE LOWER(title) = LOWER(?)', [anilistTitle]) ||
231
+ _get('SELECT id FROM anime WHERE LOWER(title) = LOWER(?)', [confirmedSiteTitle]);
232
+
218
233
  if (existing) {
219
234
  _run(
220
- 'UPDATE anime SET confirmed_site_id = ?, confirmed_site_title = ? WHERE id = ?',
221
- [confirmedSiteId, confirmedSiteTitle, existing.id]
235
+ 'UPDATE anime SET confirmed_site_id = ?, confirmed_site_title = ?, anilist_title = ? WHERE id = ?',
236
+ [confirmedSiteId, confirmedSiteTitle, anilistTitle, existing.id]
222
237
  );
223
238
  } else {
224
- // No row yet — insert one using confirmedSiteId as the site_id
225
239
  _run(
226
- 'INSERT OR IGNORE INTO anime (title, site_id, confirmed_site_id, confirmed_site_title, source) VALUES (?, ?, ?, ?, ?)',
227
- [title, confirmedSiteId, confirmedSiteId, confirmedSiteTitle, 'anilist']
240
+ 'INSERT OR IGNORE INTO anime (title, anilist_title, site_id, confirmed_site_id, confirmed_site_title, source) VALUES (?, ?, ?, ?, ?, ?)',
241
+ [anilistTitle, anilistTitle, confirmedSiteId, confirmedSiteId, confirmedSiteTitle, 'anilist']
228
242
  );
229
243
  }
230
244
  _save();
@@ -234,6 +248,6 @@ module.exports = {
234
248
  initDb,
235
249
  getDb: getDbInstance,
236
250
  upsertAnime, getAnime, getAnimeBySiteTitle, saveConfirmedSiteMatch, getAllEnabledAnime, setAnimeEnabled, removeAnime,
237
- isEpisodeDownloaded, markEpisodeStatus, getEpisodeStats,
251
+ isEpisodeDownloaded, getEpisodeRow, markEpisodeStatus, getEpisodeStats,
238
252
  logRun, getRecentRuns,
239
253
  };
package/src/engine.js CHANGED
@@ -155,7 +155,16 @@ async function processAnime(workItem, config, multiBar, auto = false, runQuality
155
155
  info(`[${workItem.title}] Using saved site match: ${chalk.bold(siteAnime.title)}`);
156
156
  db.saveConfirmedSiteMatch(workItem.title, siteAnime.session, siteAnime.title);
157
157
  } else if (auto) {
158
- siteAnime = siteResults[0];
158
+ // Try to find best match — prefer exact title match, then season match, then first result
159
+ const normalize = s => s.toLowerCase().replace(/[^a-z0-9]/g, '');
160
+ const normTitle = normalize(workItem.title);
161
+ const season = detectSeason(workItem.title);
162
+
163
+ const bestMatch = siteResults.find(r => normalize(r.title) === normTitle)
164
+ || (season ? siteResults.find(r => detectSeason(r.title) === season) : null)
165
+ || siteResults[0];
166
+
167
+ siteAnime = bestMatch;
159
168
  db.saveConfirmedSiteMatch(workItem.title, siteAnime.session, siteAnime.title);
160
169
  info(`[${workItem.title}] Auto-matched to: ${chalk.bold(siteAnime.title)}`);
161
170
  } else {
package/src/notify.js CHANGED
@@ -13,6 +13,7 @@ async function sendTelegram(text) {
13
13
  const config = loadConfig();
14
14
  const chatId = config.telegramChatId;
15
15
  if (!chatId) return;
16
+ if (config.telegramNotify === false) return;
16
17
  try {
17
18
  const fetch = require('node-fetch');
18
19
  await fetch(`https://api.telegram.org/bot${BOT_TOKEN}/sendMessage`, {