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.
- package/package.json +1 -1
- package/src/engine.js +21 -28
package/package.json
CHANGED
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
|
|
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
|
|
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
|
-
|
|
219
|
-
|
|
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
|
-
|
|
234
|
-
|
|
235
|
-
const season
|
|
236
|
-
|
|
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
|
|
256
|
-
const link
|
|
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
|
-
//
|
|
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
|
-
|
|
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);
|