ani-auto 1.0.3 → 1.0.4

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 +21 -28
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ani-auto",
3
- "version": "1.0.3",
3
+ "version": "1.0.4",
4
4
  "description": "Automatic anime episode downloader",
5
5
  "bin": {
6
6
  "ani-auto": "./src/cli.js"
package/src/engine.js CHANGED
@@ -28,7 +28,12 @@ function buildFilename(title, episodeNum, season = null, displayEpisodeNum = nul
28
28
  }
29
29
 
30
30
  function buildFolderName(title) {
31
- return title.replace(/[<>:"/\\|?*]/g, '').trim();
31
+ return title
32
+ .replace(/[\u2018\u2019\u201A\u201B]/g, "'") // normalize smart single quotes
33
+ .replace(/[\u201C\u201D\u201E\u201F]/g, '"') // normalize smart double quotes
34
+ .replace(/[<>:"/\\|?*]/g, '') // strip illegal path chars
35
+ .replace(/ +/g, ' ') // collapse spaces
36
+ .trim();
32
37
  }
33
38
 
34
39
  function detectSeason(title) {
@@ -95,7 +100,6 @@ async function resolveWatchList(config) {
95
100
  // ── Anime selector prompt ──────────────────────────────────────────────────
96
101
 
97
102
  async function promptAnimeSelection(watchList) {
98
- // Show numbered list
99
103
  console.log(chalk.bold('\n Available anime:'));
100
104
  watchList.forEach((item, i) => {
101
105
  const watched = item.watchedEpisodes ? chalk.gray(` (watched: ${item.watchedEpisodes})`) : '';
@@ -134,11 +138,9 @@ async function processAnime(workItem, config, multiBar, auto = false, runQuality
134
138
  let siteAnime;
135
139
 
136
140
  if (existingRow?.confirmed_site_id) {
137
- // Already confirmed before — use saved match directly
138
141
  siteAnime = { session: existingRow.confirmed_site_id, title: existingRow.confirmed_site_title };
139
142
  info(`[${workItem.title}] Using saved site match: ${chalk.bold(siteAnime.title)}`);
140
143
  } else if (auto) {
141
- // Auto mode — use top search result, save it
142
144
  const siteResults = await scraper.searchAll(workItem.title);
143
145
  if (!siteResults.length) {
144
146
  warn(`[${workItem.title}] Not found on site — skipping.`);
@@ -148,16 +150,13 @@ async function processAnime(workItem, config, multiBar, auto = false, runQuality
148
150
  db.saveConfirmedSiteMatch(workItem.title, siteAnime.session, siteAnime.title);
149
151
  info(`[${workItem.title}] Auto-matched to: ${chalk.bold(siteAnime.title)}`);
150
152
  } else {
151
- // Search and ask user to confirm
152
153
  const siteResults = await scraper.searchAll(workItem.title);
153
-
154
154
  if (!siteResults.length) {
155
155
  warn(`[${workItem.title}] Not found on site — skipping.`);
156
156
  return { downloaded: 0, skipped: 0, failed: 0 };
157
157
  }
158
158
 
159
159
  const topResults = siteResults.slice(0, 5);
160
-
161
160
  const { pick } = await inquirer.prompt([{
162
161
  type: 'list',
163
162
  name: 'pick',
@@ -177,8 +176,6 @@ async function processAnime(workItem, config, multiBar, auto = false, runQuality
177
176
  }
178
177
 
179
178
  siteAnime = pick;
180
-
181
- // Save confirmed match to DB so daemon never prompts again
182
179
  db.saveConfirmedSiteMatch(workItem.title, siteAnime.session, siteAnime.title);
183
180
  ok(`[${workItem.title}] Site match saved: ${chalk.bold(siteAnime.title)}`);
184
181
  }
@@ -192,7 +189,7 @@ async function processAnime(workItem, config, multiBar, auto = false, runQuality
192
189
  language: workItem.language || null,
193
190
  });
194
191
 
195
- // Check enabled flag — also check by title in case site match not yet confirmed
192
+ // Check enabled flag by title (AniList title) to respect ani-auto disable
196
193
  const enabledCheck = db.getAnimeBySiteTitle(workItem.title);
197
194
  if (dbRow.enabled === 0 || enabledCheck?.enabled === 0) {
198
195
  info(`[${workItem.title}] Disabled — skipping.`);
@@ -212,16 +209,10 @@ async function processAnime(workItem, config, multiBar, auto = false, runQuality
212
209
  return { downloaded: 0, skipped: 0, failed: 0 };
213
210
  }
214
211
 
215
- // Sort episodes ascending by episode number
216
212
  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));
213
+ const watched = workItem.watchedEpisodes || 0;
214
+ const unwatched = sortedEpisodes.slice(watched);
215
+ const toDownload = unwatched.filter(ep => !db.isEpisodeDownloaded(dbRow.id, ep.episode));
225
216
 
226
217
  if (!toDownload.length) {
227
218
  info(`[${workItem.title}] Nothing new (watched ${watched}/${sortedEpisodes.length} episodes).`);
@@ -230,10 +221,12 @@ async function processAnime(workItem, config, multiBar, auto = false, runQuality
230
221
 
231
222
  info(`[${workItem.title}] ${toDownload.length} unwatched episode(s) to download (Q:${q} L:${l}, skipping first ${watched}).`);
232
223
 
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);
224
+ // Use AniList title for folder — consistent with what user sees on AniList
225
+ const cleanTitle = buildFolderName(workItem.title);
226
+ const season = detectSeason(workItem.title);
227
+ // Strip "Season X" from the display title used in filename to avoid duplication
228
+ const displayTitle = cleanTitle.replace(/\s*season\s*\d+\s*/gi, '').trim();
229
+ const outputDir = path.join(config.outputDir, cleanTitle);
237
230
  if (!fs.existsSync(outputDir)) fs.mkdirSync(outputDir, { recursive: true });
238
231
 
239
232
  const queue = [...toDownload];
@@ -252,16 +245,16 @@ async function processAnime(workItem, config, multiBar, auto = false, runQuality
252
245
  while (!success && attempts < MAX_RETRIES) {
253
246
  attempts++;
254
247
  try {
255
- const links = await scraper.getDownloadLinks(siteAnime.session, ep.session);
256
- const link = scraper.pickLink(links, q, l);
248
+ const links = await scraper.getDownloadLinks(siteAnime.session, ep.session);
249
+ const link = scraper.pickLink(links, q, l);
257
250
  if (!link?.mp4Url) throw new Error('No suitable download link found');
258
251
 
259
- // Each site listing is per-season, so episode position in sortedEpisodes
260
- // directly equals the season episode number (1-indexed).
252
+ // Per-season site listing: position in sortedEpisodes = season episode number
261
253
  const relativeEp = season
262
254
  ? sortedEpisodes.findIndex(e => e.episode === ep.episode) + 1
263
255
  : ep.episode;
264
- const filename = buildFilename(cleanTitle, ep.episode, season, relativeEp);
256
+
257
+ const filename = buildFilename(displayTitle, ep.episode, season, relativeEp);
265
258
  await scraper.downloadEpisode(link.mp4Url, filename, outputDir, multiBar, ep.episode);
266
259
 
267
260
  db.markEpisodeStatus(dbRow.id, ep.episode, 'done', filename);