192 lines
5.5 KiB
Plaintext
192 lines
5.5 KiB
Plaintext
|
#!/bin/node
|
||
|
|
||
|
const https = require('https');
|
||
|
const fs = require('fs');
|
||
|
const zlib = require('zlib');
|
||
|
const crypto = require('crypto');
|
||
|
const CryptoJS = require('crypto-js');
|
||
|
|
||
|
const url = process.argv[2];
|
||
|
const episodes = process.argv.slice(3);
|
||
|
|
||
|
const animeMatches = url.match(/\/a\/([^/]+)(\/([0-9]+))?/i);
|
||
|
const anime = animeMatches[1];
|
||
|
const episode = Number(animeMatches[3]);
|
||
|
if (episodes.length === 0) episodes.push(episode);
|
||
|
|
||
|
console.log('Anime:', anime, 'episodes:', episodes);
|
||
|
|
||
|
function handleResponse(response, callback) {
|
||
|
const enc = response.headers['content-encoding'];
|
||
|
let d = '';
|
||
|
response.on('data', data => d += enc === 'gzip' ? zlib.unzipSync(data) : data);
|
||
|
response.on('end', () => {
|
||
|
callback(d);
|
||
|
});
|
||
|
}
|
||
|
|
||
|
function getCookie(then) {
|
||
|
https.request(url, {
|
||
|
headers: {
|
||
|
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:79.0) Gecko/20100101 Firefox/79.0",
|
||
|
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
|
||
|
"Accept-Language": "en-US,en;q=0.5",
|
||
|
"Accept-Encoding": "gzip, deflate, br",
|
||
|
"Connection": "keep-alive",
|
||
|
"Upgrade-Insecure-Requests": "1",
|
||
|
"Pragma": "no-cache",
|
||
|
"Cache-Control": "no-cache"
|
||
|
}
|
||
|
}, response => {
|
||
|
handleResponse(response, data => {
|
||
|
const script = data.match(/<script>(.+?)e\(r\);<\/script>/);
|
||
|
if (!script) {
|
||
|
console.error(`Cannot find cookie script in`, data);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
eval(script[1]);
|
||
|
r = r.replace('document.cookie=', 'cookie=')
|
||
|
.replace('location.reload();', '');
|
||
|
eval(r);
|
||
|
console.log('Found cookie:', cookie);
|
||
|
then(cookie);
|
||
|
});
|
||
|
}).end();
|
||
|
}
|
||
|
|
||
|
function getAccessToken(cookie, then) {
|
||
|
https.request(url, {
|
||
|
headers: {
|
||
|
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:79.0) Gecko/20100101 Firefox/79.0",
|
||
|
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
|
||
|
"Accept-Language": "en-US,en;q=0.5",
|
||
|
"Accept-Encoding": "gzip, deflate, br",
|
||
|
"Connection": "keep-alive",
|
||
|
"Upgrade-Insecure-Requests": "1",
|
||
|
"Pragma": "no-cache",
|
||
|
"Cache-Control": "no-cache",
|
||
|
"Cookie": cookie
|
||
|
}
|
||
|
}, response => {
|
||
|
handleResponse(response, data => {
|
||
|
let reqMatches = data.match(/(<link href="([^"]+?)\.js" rel="preload" as="script">)/g);
|
||
|
if (!reqMatches) {
|
||
|
console.error(`Couldn't find script.`, data);
|
||
|
console.log(cookie);
|
||
|
console.log(response.headers);
|
||
|
return;
|
||
|
}
|
||
|
reqMatches = reqMatches[reqMatches.length - 1].match(/href="(.+?)"/);
|
||
|
const scriptUrl = "https://twist.moe" + reqMatches[1];
|
||
|
|
||
|
console.log("Obtaining access token from " + scriptUrl);
|
||
|
https.request(scriptUrl, {
|
||
|
}, response => {
|
||
|
handleResponse(response, data => {
|
||
|
const accessToken = data.match(/"token=([^"]+?)"/)[1];
|
||
|
const password = data.match(/name: ?".+",.+?k: ?"([^"]+?)",/)[1];
|
||
|
console.log('Access token:', accessToken);
|
||
|
//k: "LXgIVP&PorO68Rq7dTx8N^lP!Fa5sGJ^*XK",
|
||
|
console.log('password:', password);
|
||
|
then(accessToken, password);
|
||
|
});
|
||
|
}).end();
|
||
|
});
|
||
|
}).end();
|
||
|
}
|
||
|
|
||
|
function getPlaylist(accessToken, then) {
|
||
|
https.request('https://twist.moe/api/anime/' + anime + '/sources', {
|
||
|
headers: {
|
||
|
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:79.0) Gecko/20100101 Firefox/79.0",
|
||
|
"Accept": "application/json, text/plain, */*",
|
||
|
"Accept-Language": "en-US,en;q=0.5",
|
||
|
"Accept-Encoding": "gzip, deflate, br",
|
||
|
"Connection": "keep-alive",
|
||
|
"Upgrade-Insecure-Requests": "1",
|
||
|
"Pragma": "no-cache",
|
||
|
"Cache-Control": "no-cache",
|
||
|
"Referer": url,
|
||
|
"x-access-token": accessToken,
|
||
|
}
|
||
|
}, response => {
|
||
|
handleResponse(response, data => {
|
||
|
const playlist = JSON.parse(data);
|
||
|
console.log('Playlist found with', playlist.length, 'items.');
|
||
|
then(playlist);
|
||
|
});
|
||
|
}).end();
|
||
|
}
|
||
|
|
||
|
function download(sourceUrl, file) {
|
||
|
return new Promise(resolve => {
|
||
|
https.request(sourceUrl, {
|
||
|
headers: {
|
||
|
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:79.0) Gecko/20100101 Firefox/79.0",
|
||
|
"Accept": "video/webm,video/ogg,video/*;q=0.9,application/ogg;q=0.7,audio/*;q=0.6,*/*;q=0.5",
|
||
|
"Accept-Language": "en-US,en;q=0.5",
|
||
|
"Range": "bytes=0-",
|
||
|
"Connection": "keep-alive",
|
||
|
"Referer": "https://twist.moe",
|
||
|
"Pragma": "no-cache",
|
||
|
"Cache-Control": "no-cache",
|
||
|
}
|
||
|
}, response => {
|
||
|
if (response.headers.location) {
|
||
|
console.log('Redirected to', response.headers.location);
|
||
|
return resolve(download(response.headers.location, file));
|
||
|
}
|
||
|
|
||
|
console.log('Downloading', sourceUrl, 'to', file);
|
||
|
|
||
|
const t = Number(response.headers["content-length"]);
|
||
|
let l = 0;
|
||
|
response.on('data', data => {
|
||
|
fs.appendFileSync(file, data);
|
||
|
l += data.length;
|
||
|
process.stdout.clearLine();
|
||
|
process.stdout.cursorTo(0);
|
||
|
process.stdout.write(((l / t) * 100).toFixed(2) + '%');
|
||
|
});
|
||
|
|
||
|
response.on('end', () => {
|
||
|
console.log();
|
||
|
console.log('Done');
|
||
|
resolve();
|
||
|
});
|
||
|
}).end();
|
||
|
});
|
||
|
}
|
||
|
|
||
|
|
||
|
getCookie(cookie => {
|
||
|
getAccessToken(cookie, (accessToken, password) => {
|
||
|
getPlaylist(accessToken, async playlist => {
|
||
|
try {
|
||
|
for (const episode of episodes) {
|
||
|
const episodeData = playlist[episode - 1];
|
||
|
if (!episodeData) {
|
||
|
console.error('Episode', episode, 'not found.');
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
const url = 'https://twistcdn.bunny.sh' + CryptoJS.AES.decrypt(episodeData.source, password).toString(CryptoJS.enc.Utf8);
|
||
|
|
||
|
// Get automatic filename
|
||
|
const parts = url.split('/');
|
||
|
const file = parts[parts.length - 1];
|
||
|
if (fs.existsSync(file)) {
|
||
|
console.error(file + ' already exists.');
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
// Download
|
||
|
await download(url, file);
|
||
|
}
|
||
|
} catch (err) { console.error(err); }
|
||
|
|
||
|
process.exit(0);
|
||
|
});
|
||
|
});
|
||
|
});
|