client-scripts/twist-dl.sh

192 lines
5.5 KiB
Bash
Executable File

#!/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);
});
});
});