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.
- package/package.json +1 -1
- package/src/engine.js +41 -44
package/package.json
CHANGED
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
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
|
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
|
-
|
|
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));
|
|
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
|
-
|
|
234
|
-
|
|
235
|
-
const season
|
|
236
|
-
|
|
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
|
|
256
|
-
const link
|
|
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
|
-
//
|
|
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
|
-
|
|
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(
|
|
297
|
-
console.log(chalk.bold.red('
|
|
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
|
|