ani-auto 1.0.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/README.md +159 -0
- package/package.json +23 -0
- package/src/anilist.js +132 -0
- package/src/cli.js +517 -0
- package/src/config.js +79 -0
- package/src/daemon.js +75 -0
- package/src/db.js +232 -0
- package/src/engine.js +343 -0
- package/src/scraper.js +137 -0
- package/utils.js +118 -0
package/src/scraper.js
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* scraper.js
|
|
5
|
+
*
|
|
6
|
+
* Thin adapter over the k-anime-cli utils.
|
|
7
|
+
* This is the ONLY file that touches the sister CLI project.
|
|
8
|
+
*
|
|
9
|
+
* Expected peer dependency layout:
|
|
10
|
+
* ../k-anime/utils.js ← the existing CLI's utils
|
|
11
|
+
*
|
|
12
|
+
* If you keep ani-auto as a separate repo, set KANIME_UTILS_PATH
|
|
13
|
+
* in your environment or config to point to utils.js.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const path = require('path');
|
|
17
|
+
const fetch = require('node-fetch');
|
|
18
|
+
|
|
19
|
+
// ── Resolve the sibling CLI utils ─────────────────────────────────────────
|
|
20
|
+
const UTILS_PATH = process.env.KANIME_UTILS_PATH
|
|
21
|
+
|| path.resolve(__dirname, '../utils.js');
|
|
22
|
+
|
|
23
|
+
let _utils = null;
|
|
24
|
+
function utils() {
|
|
25
|
+
if (_utils) return _utils;
|
|
26
|
+
try {
|
|
27
|
+
_utils = require(UTILS_PATH);
|
|
28
|
+
} catch (e) {
|
|
29
|
+
throw new Error(
|
|
30
|
+
`Cannot find k-anime utils at "${UTILS_PATH}".\n` +
|
|
31
|
+
`Set KANIME_UTILS_PATH env var to the correct path.\n` +
|
|
32
|
+
`Original error: ${e.message}`
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
return _utils;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ── Public surface ────────────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Search the site for an anime by title.
|
|
42
|
+
* Returns the best match or null.
|
|
43
|
+
*
|
|
44
|
+
* @param {string} title
|
|
45
|
+
* @returns {Promise<SiteAnime|null>}
|
|
46
|
+
*/
|
|
47
|
+
async function searchAnime(title) {
|
|
48
|
+
const results = await searchAll(title);
|
|
49
|
+
if (!results.length) return null;
|
|
50
|
+
const lower = title.toLowerCase();
|
|
51
|
+
const exact = results.find(r => r.title?.toLowerCase() === lower);
|
|
52
|
+
return exact || results[0];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Returns all search results (up to API limit) for user confirmation. */
|
|
56
|
+
async function searchAll(title) {
|
|
57
|
+
const { SEARCH_API, USER_AGENT } = utils();
|
|
58
|
+
const url = `${SEARCH_API}?q=${encodeURIComponent(title)}`;
|
|
59
|
+
const res = await fetch(url, { headers: { 'User-Agent': USER_AGENT } });
|
|
60
|
+
if (!res.ok) return [];
|
|
61
|
+
const data = await res.json();
|
|
62
|
+
return data.results || [];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Fetch episode list for a given site anime session id.
|
|
67
|
+
*
|
|
68
|
+
* @param {string} animeId (session field from searchAnime result)
|
|
69
|
+
* @returns {Promise<SiteEpisode[]>}
|
|
70
|
+
*/
|
|
71
|
+
async function getEpisodes(animeId) {
|
|
72
|
+
const { INFO_API, USER_AGENT } = utils();
|
|
73
|
+
const res = await fetch(`${INFO_API}?id=${animeId}`, { headers: { 'User-Agent': USER_AGENT } });
|
|
74
|
+
if (!res.ok) throw new Error(`Failed to fetch episode list for ${animeId}`);
|
|
75
|
+
const data = await res.json();
|
|
76
|
+
return data.results?.episodes || [];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Fetch download links for a single episode.
|
|
81
|
+
*
|
|
82
|
+
* @param {string} animeId
|
|
83
|
+
* @param {string} episodeId (session field from episode object)
|
|
84
|
+
* @returns {Promise<DownloadLink[]>}
|
|
85
|
+
*/
|
|
86
|
+
async function getDownloadLinks(animeId, episodeId) {
|
|
87
|
+
const { DOWNLOAD_API, USER_AGENT } = utils();
|
|
88
|
+
const res = await fetch(
|
|
89
|
+
`${DOWNLOAD_API}?animeId=${animeId}&episodeId=${episodeId}`,
|
|
90
|
+
{ headers: { 'User-Agent': USER_AGENT } }
|
|
91
|
+
);
|
|
92
|
+
if (!res.ok) return [];
|
|
93
|
+
const data = await res.json();
|
|
94
|
+
return data.results?.downloadLinks || [];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Pick the best download link given quality + language preferences.
|
|
99
|
+
* Falls back gracefully: exact → quality-only → first available.
|
|
100
|
+
*
|
|
101
|
+
* @param {DownloadLink[]} links
|
|
102
|
+
* @param {string} quality e.g. '1080p'
|
|
103
|
+
* @param {string} language e.g. 'sub'
|
|
104
|
+
* @returns {DownloadLink|null}
|
|
105
|
+
*/
|
|
106
|
+
function pickLink(links, quality, language) {
|
|
107
|
+
if (!links.length) return null;
|
|
108
|
+
return (
|
|
109
|
+
links.find(l => l.resolution === quality && l.audio === language) ||
|
|
110
|
+
links.find(l => l.resolution === quality) ||
|
|
111
|
+
links[0]
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Download a single episode file.
|
|
117
|
+
* Delegates entirely to the existing CLI's downloadFile().
|
|
118
|
+
*
|
|
119
|
+
* @param {string} url
|
|
120
|
+
* @param {string} filename
|
|
121
|
+
* @param {string} outputDir
|
|
122
|
+
* @param {*} multiBar cliProgress.MultiBar instance (or null for silent)
|
|
123
|
+
* @param {number} episodeNum
|
|
124
|
+
*/
|
|
125
|
+
async function downloadEpisode(url, filename, outputDir, multiBar, episodeNum) {
|
|
126
|
+
const { downloadFile } = utils();
|
|
127
|
+
await downloadFile(url, filename, outputDir, multiBar, episodeNum);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
module.exports = {
|
|
131
|
+
searchAnime,
|
|
132
|
+
searchAll,
|
|
133
|
+
getEpisodes,
|
|
134
|
+
getDownloadLinks,
|
|
135
|
+
pickLink,
|
|
136
|
+
downloadEpisode,
|
|
137
|
+
};
|
package/utils.js
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const chalk = require('chalk');
|
|
5
|
+
|
|
6
|
+
// --- Configuration ---
|
|
7
|
+
const API_DOMAIN = 'https://arcane-nx-cipher-pol.hf.space/api';
|
|
8
|
+
const SEARCH_API = `${API_DOMAIN}/anime/search`;
|
|
9
|
+
const INFO_API = `${API_DOMAIN}/anime/info`;
|
|
10
|
+
const DOWNLOAD_API = `${API_DOMAIN}/anime/download`;
|
|
11
|
+
const USER_AGENT = 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Mobile Safari/537.36';
|
|
12
|
+
|
|
13
|
+
// --- Helpers ---
|
|
14
|
+
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
|
15
|
+
|
|
16
|
+
const getTerminalWidth = () => process.stdout.columns || 80;
|
|
17
|
+
|
|
18
|
+
const formatBytes = (bytes, decimals = 2) => {
|
|
19
|
+
if (!+bytes) return '0 Bytes';
|
|
20
|
+
const k = 1024;
|
|
21
|
+
const dm = decimals < 0 ? 0 : decimals;
|
|
22
|
+
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
|
23
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
24
|
+
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// --- Download Logic ---
|
|
28
|
+
async function downloadFile(url, filename, folder, multiBar, episode) {
|
|
29
|
+
const filePath = path.join(folder, filename);
|
|
30
|
+
let downloadedBytes = 0;
|
|
31
|
+
let fileExists = false;
|
|
32
|
+
|
|
33
|
+
if (fs.existsSync(filePath)) {
|
|
34
|
+
downloadedBytes = fs.statSync(filePath).size;
|
|
35
|
+
fileExists = true;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const progressBar = multiBar ? multiBar.create(100, 0, {
|
|
39
|
+
episode: episode,
|
|
40
|
+
speed: '0 B/s',
|
|
41
|
+
value_formatted: formatBytes(downloadedBytes),
|
|
42
|
+
total_formatted: 'Unknown'
|
|
43
|
+
}) : null;
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
const headers = {
|
|
47
|
+
'User-Agent': USER_AGENT,
|
|
48
|
+
'Referer': 'https://kwik.cx/'
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
if (fileExists) headers['Range'] = `bytes=${downloadedBytes}-`;
|
|
52
|
+
|
|
53
|
+
const response = await fetch(url, { headers });
|
|
54
|
+
|
|
55
|
+
if (response.status === 416) {
|
|
56
|
+
if (progressBar) { progressBar.update(100, { episode, value_formatted: 'Complete', total_formatted: 'Complete', speed: '0 B/s' }); progressBar.stop(); }
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (!response.ok && response.status !== 206) {
|
|
61
|
+
throw new Error(`HTTP ${response.status}`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const isResuming = response.status === 206;
|
|
65
|
+
const contentLength = parseInt(response.headers.get('content-length'), 10);
|
|
66
|
+
const totalBytes = isResuming ? downloadedBytes + contentLength : contentLength;
|
|
67
|
+
|
|
68
|
+
if (progressBar) {
|
|
69
|
+
progressBar.setTotal(totalBytes);
|
|
70
|
+
progressBar.update(downloadedBytes, { episode, total_formatted: formatBytes(totalBytes) });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const fileStream = fs.createWriteStream(filePath, { flags: isResuming ? 'a' : 'w' });
|
|
74
|
+
const reader = response.body.getReader();
|
|
75
|
+
let currentProgress = downloadedBytes;
|
|
76
|
+
const startTime = Date.now();
|
|
77
|
+
|
|
78
|
+
while (true) {
|
|
79
|
+
const { done, value } = await reader.read();
|
|
80
|
+
if (done) break;
|
|
81
|
+
|
|
82
|
+
fileStream.write(Buffer.from(value));
|
|
83
|
+
currentProgress += value.length;
|
|
84
|
+
|
|
85
|
+
if (progressBar) {
|
|
86
|
+
const elapsed = (Date.now() - startTime) / 1000;
|
|
87
|
+
const speed = elapsed > 0 ? (currentProgress - downloadedBytes) / elapsed : 0;
|
|
88
|
+
progressBar.update(currentProgress, {
|
|
89
|
+
episode,
|
|
90
|
+
value_formatted: formatBytes(currentProgress),
|
|
91
|
+
speed: formatBytes(speed) + '/s'
|
|
92
|
+
});
|
|
93
|
+
} else {
|
|
94
|
+
process.stdout.write(`\r EP${episode}: ${formatBytes(currentProgress)}${totalBytes ? ' / ' + formatBytes(totalBytes) : ''}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
fileStream.end();
|
|
99
|
+
if (progressBar) progressBar.stop();
|
|
100
|
+
else process.stdout.write('\n');
|
|
101
|
+
return true;
|
|
102
|
+
|
|
103
|
+
} catch (err) {
|
|
104
|
+
if (progressBar) progressBar.stop();
|
|
105
|
+
throw err;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
module.exports = {
|
|
110
|
+
SEARCH_API,
|
|
111
|
+
INFO_API,
|
|
112
|
+
DOWNLOAD_API,
|
|
113
|
+
USER_AGENT,
|
|
114
|
+
sleep,
|
|
115
|
+
getTerminalWidth,
|
|
116
|
+
formatBytes,
|
|
117
|
+
downloadFile,
|
|
118
|
+
};
|