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.
- package/package.json +1 -1
- package/src/cli.js +20 -4
- package/src/engine.js +26 -23
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -126,9 +126,10 @@ program
|
|
|
126
126
|
banner();
|
|
127
127
|
try {
|
|
128
128
|
const config = loadConfig();
|
|
129
|
-
let quality
|
|
130
|
-
let language
|
|
131
|
-
let range
|
|
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, "'")
|
|
35
|
-
.replace(/[\u201C\u201D\u201E\u201F]/g, '"')
|
|
36
|
-
.replace(/[<>:"/\\|?*]/g, '')
|
|
37
|
-
.replace(/ +/g, ' ')
|
|
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
|
-
|
|
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
|
-
|
|
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('
|
|
313
|
+
console.log(chalk.bold.red(' ╚════════════════════════════════╝\n'));
|
|
315
314
|
|
|
316
|
-
|
|
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`.');
|