ani-auto 1.2.0 → 1.2.2

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 (3) hide show
  1. package/package.json +1 -1
  2. package/src/cli.js +20 -4
  3. package/src/engine.js +26 -23
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ani-auto",
3
- "version": "1.2.0",
3
+ "version": "1.2.2",
4
4
  "description": "Automatic anime episode downloader",
5
5
  "bin": {
6
6
  "ani-auto": "./src/cli.js"
package/src/cli.js CHANGED
@@ -126,9 +126,10 @@ program
126
126
  banner();
127
127
  try {
128
128
  const config = loadConfig();
129
- let quality = config.quality || '1080p';
130
- let language = config.language || 'sub';
131
- let range = null;
129
+ let quality = config.quality || '1080p';
130
+ let language = config.language || 'sub';
131
+ let range = null;
132
+ let runStatuses = config.watchStatuses || ['CURRENT'];
132
133
 
133
134
  if (!opts.auto) {
134
135
  const { pickedQuality, saveAsDefault } = await inquirer.prompt([
@@ -152,6 +153,21 @@ program
152
153
  console.log(chalk.green(` ✔ Saved default quality: ${quality}\n`));
153
154
  }
154
155
 
156
+ // Status selection for this run
157
+ const { pickedStatuses } = await inquirer.prompt([{
158
+ type: 'checkbox',
159
+ name: 'pickedStatuses',
160
+ message: 'AniList statuses to fetch for this run:',
161
+ choices: [
162
+ { name: 'CURRENT (Watching)', value: 'CURRENT', checked: (config.watchStatuses || ['CURRENT']).includes('CURRENT') },
163
+ { name: 'PLANNING (Plan to Watch)', value: 'PLANNING', checked: (config.watchStatuses || []).includes('PLANNING') },
164
+ { name: 'PAUSED', value: 'PAUSED', checked: (config.watchStatuses || []).includes('PAUSED') },
165
+ { name: 'REPEATING', value: 'REPEATING', checked: (config.watchStatuses || []).includes('REPEATING') },
166
+ ],
167
+ validate: v => v.length > 0 ? true : 'Select at least one status',
168
+ }]);
169
+ runStatuses = pickedStatuses;
170
+
155
171
  const { useRange } = await inquirer.prompt([{
156
172
  type: 'confirm',
157
173
  name: 'useRange',
@@ -178,7 +194,7 @@ program
178
194
  }
179
195
  }
180
196
 
181
- await runCycle({ auto: !!opts.auto, quality, language, range });
197
+ await runCycle({ auto: !!opts.auto, quality, language, range, statuses: runStatuses });
182
198
  }
183
199
  catch (e) { console.error(chalk.red('[ERROR]'), e.message); process.exit(1); }
184
200
  });
package/src/engine.js CHANGED
@@ -31,10 +31,10 @@ function buildFilename(title, episodeNum, season = null, displayEpisodeNum = nul
31
31
 
32
32
  function buildFolderName(title) {
33
33
  return title
34
- .replace(/[\u2018\u2019\u201A\u201B]/g, "'") // normalize smart single quotes
35
- .replace(/[\u201C\u201D\u201E\u201F]/g, '"') // normalize smart double quotes
36
- .replace(/[<>:"/\\|?*]/g, '') // strip illegal path chars
37
- .replace(/ +/g, ' ') // collapse spaces
34
+ .replace(/[\u2018\u2019\u201A\u201B]/g, "'")
35
+ .replace(/[\u201C\u201D\u201E\u201F]/g, '"')
36
+ .replace(/[<>:"/\\|?*]/g, '')
37
+ .replace(/ +/g, ' ')
38
38
  .trim();
39
39
  }
40
40
 
@@ -68,6 +68,7 @@ async function resolveWatchList(config) {
68
68
  watchedEpisodes: entry.progress || 0,
69
69
  knownEpisodes: entry.totalEpisodes,
70
70
  animeStatus: entry.animeStatus,
71
+ anilistStatus: entry.status,
71
72
  });
72
73
  }
73
74
  info(`AniList returned ${chalk.bold(entries.length)} entries.`);
@@ -87,6 +88,7 @@ async function resolveWatchList(config) {
87
88
  watchedEpisodes: 0,
88
89
  knownEpisodes: null,
89
90
  animeStatus: null,
91
+ anilistStatus: null,
90
92
  });
91
93
  }
92
94
 
@@ -105,7 +107,8 @@ async function promptAnimeSelection(watchList) {
105
107
  console.log(chalk.bold('\n Available anime:'));
106
108
  watchList.forEach((item, i) => {
107
109
  const watched = item.watchedEpisodes ? chalk.gray(` (watched: ${item.watchedEpisodes})`) : '';
108
- console.log(` ${chalk.cyan(i + 1)}. ${item.title}${watched}`);
110
+ const status = item.anilistStatus ? chalk.gray(` [${item.anilistStatus}]`) : '';
111
+ console.log(` ${chalk.cyan(i + 1)}. ${item.title}${watched}${status}`);
109
112
  });
110
113
  console.log(` ${chalk.cyan('a')}. All`);
111
114
  console.log('');
@@ -135,11 +138,9 @@ async function processAnime(workItem, config, multiBar, auto = false, runQuality
135
138
  const q = runQuality || workItem.quality || config.quality || '1080p';
136
139
  const l = runLanguage || workItem.language || config.language || 'sub';
137
140
 
138
- // Check if we already have a confirmed site match saved in DB
139
141
  const existingRow = db.getAnimeBySiteTitle(workItem.title);
140
142
  let siteAnime;
141
143
 
142
- // Always do a fresh search — session IDs expire on AnimePahe
143
144
  const siteResults = await scraper.searchAll(workItem.title);
144
145
  if (!siteResults.length) {
145
146
  warn(`[${workItem.title}] Not found on site — skipping.`);
@@ -147,13 +148,11 @@ async function processAnime(workItem, config, multiBar, auto = false, runQuality
147
148
  }
148
149
 
149
150
  if (existingRow?.confirmed_site_title) {
150
- // We have a saved title preference — find it in fresh results
151
151
  const savedMatch = siteResults.find(r =>
152
152
  r.title?.toLowerCase() === existingRow.confirmed_site_title.toLowerCase()
153
153
  ) || siteResults[0];
154
154
  siteAnime = savedMatch;
155
155
  info(`[${workItem.title}] Using saved site match: ${chalk.bold(siteAnime.title)}`);
156
- // Update session ID in DB
157
156
  db.saveConfirmedSiteMatch(workItem.title, siteAnime.session, siteAnime.title);
158
157
  } else if (auto) {
159
158
  siteAnime = siteResults[0];
@@ -193,7 +192,6 @@ async function processAnime(workItem, config, multiBar, auto = false, runQuality
193
192
  language: workItem.language || null,
194
193
  });
195
194
 
196
- // Check enabled flag by title (AniList title) to respect ani-auto disable
197
195
  const enabledCheck = db.getAnimeBySiteTitle(workItem.title);
198
196
  if (dbRow.enabled === 0 || enabledCheck?.enabled === 0) {
199
197
  info(`[${workItem.title}] Disabled — skipping.`);
@@ -218,7 +216,6 @@ async function processAnime(workItem, config, multiBar, auto = false, runQuality
218
216
  const unwatched = sortedEpisodes.slice(watched);
219
217
  let toDownload = unwatched.filter(ep => !db.isEpisodeDownloaded(dbRow.id, ep.episode));
220
218
 
221
- // Apply episode range filter if specified
222
219
  if (range) {
223
220
  const clean = range.trim();
224
221
  let rangeFrom, rangeTo;
@@ -227,7 +224,6 @@ async function processAnime(workItem, config, multiBar, auto = false, runQuality
227
224
  } else {
228
225
  [rangeFrom, rangeTo] = clean.split('-').map(Number);
229
226
  }
230
- // Range is relative to the unwatched slice (1 = first unwatched episode)
231
227
  toDownload = toDownload.filter((ep, idx) => {
232
228
  const pos = idx + 1;
233
229
  return pos >= rangeFrom && pos <= rangeTo;
@@ -242,10 +238,8 @@ async function processAnime(workItem, config, multiBar, auto = false, runQuality
242
238
 
243
239
  info(`[${workItem.title}] ${toDownload.length} unwatched episode(s) to download (Q:${q} L:${l}, skipping first ${watched}).`);
244
240
 
245
- // Folder uses AniList title — consistent with what user sees on AniList
246
241
  const cleanTitle = buildFolderName(workItem.title);
247
242
  const season = detectSeason(workItem.title);
248
- // Filename uses site title (better casing) with Season X stripped to avoid duplication
249
243
  const siteClean = buildFolderName(siteAnime.title || workItem.title);
250
244
  const displayTitle = siteClean.replace(/[:\s]*season\s*\d+[:\s]*/gi, ' ').replace(/\s+/g, ' ').trim();
251
245
  const outputDir = path.join(config.outputDir, cleanTitle);
@@ -256,6 +250,15 @@ async function processAnime(workItem, config, multiBar, auto = false, runQuality
256
250
  const concurrency = Math.min(config.concurrency || 2, queue.length);
257
251
  const MAX_RETRIES = config.maxRetries || 3;
258
252
 
253
+ // Pre-compute relative episode numbers before queueing
254
+ const epRelativeMap = new Map();
255
+ toDownload.forEach((ep, idx) => {
256
+ const relEp = season
257
+ ? sortedEpisodes.findIndex(e => e.episode === ep.episode) + 1
258
+ : ep.episode;
259
+ epRelativeMap.set(ep.episode, relEp);
260
+ });
261
+
259
262
  const workers = Array(concurrency).fill(0).map(async () => {
260
263
  while (queue.length > 0) {
261
264
  const ep = queue.shift();
@@ -271,11 +274,7 @@ async function processAnime(workItem, config, multiBar, auto = false, runQuality
271
274
  const link = scraper.pickLink(links, q, l);
272
275
  if (!link?.mp4Url) throw new Error('No suitable download link found');
273
276
 
274
- // Per-season site listing: position in sortedEpisodes = season episode number
275
- const relativeEp = season
276
- ? sortedEpisodes.findIndex(e => e.episode === ep.episode) + 1
277
- : ep.episode;
278
-
277
+ const relativeEp = epRelativeMap.get(ep.episode) || ep.episode;
279
278
  const filename = buildFilename(displayTitle, ep.episode, season, relativeEp);
280
279
  await scraper.downloadEpisode(link.mp4Url, filename, outputDir, multiBar, ep.episode);
281
280
 
@@ -305,15 +304,19 @@ async function processAnime(workItem, config, multiBar, auto = false, runQuality
305
304
 
306
305
  // ── Main cycle ─────────────────────────────────────────────────────────────
307
306
 
308
- async function runCycle({ auto = false, quality = null, language = null, range = null } = {}) {
307
+ async function runCycle({ auto = false, quality = null, language = null, range = null, statuses = null } = {}) {
309
308
  await db.initDb();
310
309
  const config = loadConfig();
311
310
 
312
- console.log(chalk.bold.red('\n ╔══════════════════════════════╗'));
311
+ console.log(chalk.bold.red('\n ╔════════════════════════════════╗'));
313
312
  console.log(chalk.bold.red(` ║ ANI-AUTO v${version} — CHECK RUN ║`));
314
- console.log(chalk.bold.red(' ╚══════════════════════════════╝\n'));
313
+ console.log(chalk.bold.red(' ╚════════════════════════════════╝\n'));
315
314
 
316
- const watchList = await resolveWatchList(config);
315
+ // Use run-level statuses if provided, otherwise fall back to config
316
+ const effectiveConfig = statuses
317
+ ? { ...config, watchStatuses: statuses }
318
+ : config;
319
+ const watchList = await resolveWatchList(effectiveConfig);
317
320
 
318
321
  if (!watchList.length) {
319
322
  warn('Watch list is empty. Add entries via AniList or `ani-auto add`.');