ani-auto 1.0.3 → 1.1.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 (2) hide show
  1. package/package.json +1 -1
  2. package/src/engine.js +41 -44
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ani-auto",
3
- "version": "1.0.3",
3
+ "version": "1.1.0",
4
4
  "description": "Automatic anime episode downloader",
5
5
  "bin": {
6
6
  "ani-auto": "./src/cli.js"
package/src/engine.js CHANGED
@@ -10,6 +10,7 @@ const { loadConfig } = require('./config');
10
10
  const db = require('./db');
11
11
  const { getUserList, pickTitle } = require('./anilist');
12
12
  const scraper = require('./scraper');
13
+ const { version } = require('../package.json');
13
14
 
14
15
  // ── Logging ────────────────────────────────────────────────────────────────
15
16
 
@@ -28,7 +29,12 @@ function buildFilename(title, episodeNum, season = null, displayEpisodeNum = nul
28
29
  }
29
30
 
30
31
  function buildFolderName(title) {
31
- return title.replace(/[<>:"/\\|?*]/g, '').trim();
32
+ return title
33
+ .replace(/[\u2018\u2019\u201A\u201B]/g, "'") // normalize smart single quotes
34
+ .replace(/[\u201C\u201D\u201E\u201F]/g, '"') // normalize smart double quotes
35
+ .replace(/[<>:"/\\|?*]/g, '') // strip illegal path chars
36
+ .replace(/ +/g, ' ') // collapse spaces
37
+ .trim();
32
38
  }
33
39
 
34
40
  function detectSeason(title) {
@@ -95,7 +101,6 @@ async function resolveWatchList(config) {
95
101
  // ── Anime selector prompt ──────────────────────────────────────────────────
96
102
 
97
103
  async function promptAnimeSelection(watchList) {
98
- // Show numbered list
99
104
  console.log(chalk.bold('\n Available anime:'));
100
105
  watchList.forEach((item, i) => {
101
106
  const watched = item.watchedEpisodes ? chalk.gray(` (watched: ${item.watchedEpisodes})`) : '';
@@ -133,31 +138,28 @@ async function processAnime(workItem, config, multiBar, auto = false, runQuality
133
138
  const existingRow = db.getAnimeBySiteTitle(workItem.title);
134
139
  let siteAnime;
135
140
 
136
- if (existingRow?.confirmed_site_id) {
137
- // Already confirmed before — use saved match directly
138
- siteAnime = { session: existingRow.confirmed_site_id, title: existingRow.confirmed_site_title };
141
+ // Always do a fresh search — session IDs expire on AnimePahe
142
+ const siteResults = await scraper.searchAll(workItem.title);
143
+ if (!siteResults.length) {
144
+ warn(`[${workItem.title}] Not found on site — skipping.`);
145
+ return { downloaded: 0, skipped: 0, failed: 0 };
146
+ }
147
+
148
+ if (existingRow?.confirmed_site_title) {
149
+ // We have a saved title preference — find it in fresh results
150
+ const savedMatch = siteResults.find(r =>
151
+ r.title?.toLowerCase() === existingRow.confirmed_site_title.toLowerCase()
152
+ ) || siteResults[0];
153
+ siteAnime = savedMatch;
139
154
  info(`[${workItem.title}] Using saved site match: ${chalk.bold(siteAnime.title)}`);
155
+ // Update session ID in DB
156
+ db.saveConfirmedSiteMatch(workItem.title, siteAnime.session, siteAnime.title);
140
157
  } else if (auto) {
141
- // Auto mode — use top search result, save it
142
- const siteResults = await scraper.searchAll(workItem.title);
143
- if (!siteResults.length) {
144
- warn(`[${workItem.title}] Not found on site — skipping.`);
145
- return { downloaded: 0, skipped: 0, failed: 0 };
146
- }
147
158
  siteAnime = siteResults[0];
148
159
  db.saveConfirmedSiteMatch(workItem.title, siteAnime.session, siteAnime.title);
149
160
  info(`[${workItem.title}] Auto-matched to: ${chalk.bold(siteAnime.title)}`);
150
161
  } else {
151
- // Search and ask user to confirm
152
- const siteResults = await scraper.searchAll(workItem.title);
153
-
154
- if (!siteResults.length) {
155
- warn(`[${workItem.title}] Not found on site — skipping.`);
156
- return { downloaded: 0, skipped: 0, failed: 0 };
157
- }
158
-
159
162
  const topResults = siteResults.slice(0, 5);
160
-
161
163
  const { pick } = await inquirer.prompt([{
162
164
  type: 'list',
163
165
  name: 'pick',
@@ -177,8 +179,6 @@ async function processAnime(workItem, config, multiBar, auto = false, runQuality
177
179
  }
178
180
 
179
181
  siteAnime = pick;
180
-
181
- // Save confirmed match to DB so daemon never prompts again
182
182
  db.saveConfirmedSiteMatch(workItem.title, siteAnime.session, siteAnime.title);
183
183
  ok(`[${workItem.title}] Site match saved: ${chalk.bold(siteAnime.title)}`);
184
184
  }
@@ -192,7 +192,7 @@ async function processAnime(workItem, config, multiBar, auto = false, runQuality
192
192
  language: workItem.language || null,
193
193
  });
194
194
 
195
- // Check enabled flag — also check by title in case site match not yet confirmed
195
+ // Check enabled flag by title (AniList title) to respect ani-auto disable
196
196
  const enabledCheck = db.getAnimeBySiteTitle(workItem.title);
197
197
  if (dbRow.enabled === 0 || enabledCheck?.enabled === 0) {
198
198
  info(`[${workItem.title}] Disabled — skipping.`);
@@ -212,16 +212,10 @@ async function processAnime(workItem, config, multiBar, auto = false, runQuality
212
212
  return { downloaded: 0, skipped: 0, failed: 0 };
213
213
  }
214
214
 
215
- // Sort episodes ascending by episode number
216
215
  const sortedEpisodes = [...episodes].sort((a, b) => a.episode - b.episode);
217
-
218
- // AniList progress = episodes watched *within this season/entry*.
219
- // The site may use absolute episode numbers (e.g. S2E1 = EP25).
220
- // So we skip the first N episodes by position, not by episode number.
221
- const watched = workItem.watchedEpisodes || 0;
222
- const unwatched = sortedEpisodes.slice(watched);
223
-
224
- const toDownload = unwatched.filter(ep => !db.isEpisodeDownloaded(dbRow.id, ep.episode));
216
+ const watched = workItem.watchedEpisodes || 0;
217
+ const unwatched = sortedEpisodes.slice(watched);
218
+ const toDownload = unwatched.filter(ep => !db.isEpisodeDownloaded(dbRow.id, ep.episode));
225
219
 
226
220
  if (!toDownload.length) {
227
221
  info(`[${workItem.title}] Nothing new (watched ${watched}/${sortedEpisodes.length} episodes).`);
@@ -230,10 +224,13 @@ async function processAnime(workItem, config, multiBar, auto = false, runQuality
230
224
 
231
225
  info(`[${workItem.title}] ${toDownload.length} unwatched episode(s) to download (Q:${q} L:${l}, skipping first ${watched}).`);
232
226
 
233
- const cleanTitle = buildFolderName(siteAnime.title || workItem.title);
234
- // detectSeason from site title first, fall back to AniList title
235
- const season = detectSeason(cleanTitle) || detectSeason(workItem.title);
236
- const outputDir = path.join(config.outputDir, cleanTitle);
227
+ // Folder uses AniList title consistent with what user sees on AniList
228
+ const cleanTitle = buildFolderName(workItem.title);
229
+ const season = detectSeason(workItem.title);
230
+ // Filename uses site title (better casing) with Season X stripped to avoid duplication
231
+ const siteClean = buildFolderName(siteAnime.title || workItem.title);
232
+ const displayTitle = siteClean.replace(/[:\s]*season\s*\d+[:\s]*/gi, ' ').replace(/\s+/g, ' ').trim();
233
+ const outputDir = path.join(config.outputDir, cleanTitle);
237
234
  if (!fs.existsSync(outputDir)) fs.mkdirSync(outputDir, { recursive: true });
238
235
 
239
236
  const queue = [...toDownload];
@@ -252,16 +249,16 @@ async function processAnime(workItem, config, multiBar, auto = false, runQuality
252
249
  while (!success && attempts < MAX_RETRIES) {
253
250
  attempts++;
254
251
  try {
255
- const links = await scraper.getDownloadLinks(siteAnime.session, ep.session);
256
- const link = scraper.pickLink(links, q, l);
252
+ const links = await scraper.getDownloadLinks(siteAnime.session, ep.session);
253
+ const link = scraper.pickLink(links, q, l);
257
254
  if (!link?.mp4Url) throw new Error('No suitable download link found');
258
255
 
259
- // Each site listing is per-season, so episode position in sortedEpisodes
260
- // directly equals the season episode number (1-indexed).
256
+ // Per-season site listing: position in sortedEpisodes = season episode number
261
257
  const relativeEp = season
262
258
  ? sortedEpisodes.findIndex(e => e.episode === ep.episode) + 1
263
259
  : ep.episode;
264
- const filename = buildFilename(cleanTitle, ep.episode, season, relativeEp);
260
+
261
+ const filename = buildFilename(displayTitle, ep.episode, season, relativeEp);
265
262
  await scraper.downloadEpisode(link.mp4Url, filename, outputDir, multiBar, ep.episode);
266
263
 
267
264
  db.markEpisodeStatus(dbRow.id, ep.episode, 'done', filename);
@@ -292,9 +289,9 @@ async function runCycle({ auto = false, quality = null, language = null } = {})
292
289
  await db.initDb();
293
290
  const config = loadConfig();
294
291
 
295
- console.log(chalk.bold.red('\n ╔══════════════════════════════╗'));
296
- console.log(chalk.bold.red(' ANI-AUTO — CHECK RUN ║'));
297
- console.log(chalk.bold.red(' ╚══════════════════════════════╝\n'));
292
+ console.log(chalk.bold.red('\n ╔════════════════════════════════╗'));
293
+ console.log(chalk.bold.red(` ANI-AUTO v${version} — CHECK RUN ║`));
294
+ console.log(chalk.bold.red(' ╚════════════════════════════════╝\n'));
298
295
 
299
296
  const watchList = await resolveWatchList(config);
300
297