Skip to content

Instantly share code, notes, and snippets.

@Abhi1code
Last active September 15, 2020 06:21
Show Gist options
  • Save Abhi1code/4228d6d92eeec79c2ad7228ac8a34d44 to your computer and use it in GitHub Desktop.
Save Abhi1code/4228d6d92eeec79c2ad7228ac8a34d44 to your computer and use it in GitHub Desktop.
"use strict";
var __importDefault = (this && this.__importDefault) || function(mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const CommandLineParser_1 = require("./CommandLineParser");
const Events_1 = require("./Events");
const Logger_1 = require("./Logger");
const PuppeteerHelper_1 = require("./PuppeteerHelper");
const Thumbnail_1 = require("./Thumbnail");
const TokenCache_1 = require("./TokenCache");
const Utils_1 = require("./Utils");
const VideoUtils_1 = require("./VideoUtils");
const cli_progress_1 = __importDefault(require("cli-progress"));
const fs_1 = __importDefault(require("fs"));
const is_elevated_1 = __importDefault(require("is-elevated"));
const puppeteer_1 = __importDefault(require("puppeteer"));
const { FFmpegCommand, FFmpegInput, FFmpegOutput } = require('@tedconf/fessonia')();
const tokenCache = new TokenCache_1.TokenCache();
exports.chromeCacheFolder = '.chrome_data';
async function init() {
Events_1.setProcessEvents(); // must be first!
if (CommandLineParser_1.argv.verbose) {
Logger_1.logger.level = 'verbose';
}
if (await is_elevated_1.default()) {
process.exit(1 /* ELEVATED_SHELL */ );
}
Utils_1.checkRequirements();
if (CommandLineParser_1.argv.username) {
Logger_1.logger.info(`Username: ${CommandLineParser_1.argv.username}`);
}
if (CommandLineParser_1.argv.simulate) {
Logger_1.logger.warn('Simulate mode, there will be no video downloaded. \n');
}
}
async function DoInteractiveLogin(url, username) {
Logger_1.logger.info('Launching headless Chrome to perform the OpenID Connect dance...');
const browser = await puppeteer_1.default.launch({
executablePath: PuppeteerHelper_1.getPuppeteerChromiumPath(),
headless: false,
userDataDir: (CommandLineParser_1.argv.keepLoginCookies) ? exports.chromeCacheFolder : undefined,
args: [
'--disable-dev-shm-usage',
'--fast-start',
'--no-sandbox'
]
});
const page = (await browser.pages())[0];
Logger_1.logger.info('Navigating to login page...');
await page.goto(url, { waitUntil: 'load' });
try {
if (username) {
await page.waitForSelector('input[type="email"]', { timeout: 3000 });
await page.keyboard.type(username);
await page.click('input[type="submit"]');
} else {
/* If a username was not provided we let the user take actions that
lead up to the video page. */
}
} catch (e) {
/* If there is no email input selector we aren't in the login module,
we are probably using the cache to aid the login.
It could finish the login on its own if the user said 'yes' when asked to
remember the credentials or it could still prompt the user for a password */
}
await browser.waitForTarget((target) => target.url().endsWith('microsoftstream.com/'), { timeout: 150000 });
Logger_1.logger.info('We are logged in.');
let session = null;
let tries = 1;
while (!session) {
try {
let sessionInfo;
session = await page.evaluate(() => {
return {
AccessToken: sessionInfo.AccessToken,
ApiGatewayUri: sessionInfo.ApiGatewayUri,
ApiGatewayVersion: sessionInfo.ApiGatewayVersion
};
});
} catch (error) {
if (tries > 5) {
process.exit(6 /* NO_SESSION_INFO */ );
}
session = null;
tries++;
await page.waitFor(3000);
}
}
tokenCache.Write(session);
Logger_1.logger.info('Wrote access token to token cache.');
Logger_1.logger.info("At this point Chromium's job is done, shutting it down...\n");
await browser.close();
return session;
}
async function downloadVideo(videoGUIDs, outputDirectories, session) {
Logger_1.logger.info('Fetching videos info... \n');
const videos = VideoUtils_1.createUniquePath(await VideoUtils_1.getVideoInfo(videoGUIDs, session, CommandLineParser_1.argv.closedCaptions), outputDirectories, CommandLineParser_1.argv.outputTemplate, CommandLineParser_1.argv.format, CommandLineParser_1.argv.skip);
if (!CommandLineParser_1.argv.simulate) {
videos.forEach((video) => {
Logger_1.logger.info('\nTitle: '.green + video.title +
'\nOutPath: '.green + video.outPath +
'\nPublished Date: '.green + video.publishDate +
'\nPlayback URL: '.green + video.playbackUrl +
'\nPlayback Duration: '.green + video.duration +
((video.captionsUrl) ? ('\nCC URL: '.green + video.captionsUrl) : ''));
});
//return;
}
for (const [index, video] of videos.entries()) {
if (CommandLineParser_1.argv.skip && fs_1.default.existsSync(video.outPath)) {
Logger_1.logger.info(`File already exists: ${video.outPath} \n`);
fs_1.default.unlinkSync(video.outPath);
}
if (CommandLineParser_1.argv.keepLoginCookies && index !== 0) {
Logger_1.logger.info('Trying to refresh token...');
session = await TokenCache_1.refreshSession('https://web.microsoftstream.com/video/' + videoGUIDs[index]);
}
const pbar = new cli_progress_1.default.SingleBar({
barCompleteChar: '\u2588',
barIncompleteChar: '\u2591',
format: 'progress [{bar}] {percentage}% {speed} {eta_formatted}',
// process.stdout.columns may return undefined in some terminals (Cygwin/MSYS)
barsize: Math.floor((process.stdout.columns || 30) / 3),
stopOnComplete: true,
hideCursor: true,
});
Logger_1.logger.info(`\nDownloading Video: ${video.title} \n`);
Logger_1.logger.info('Extra video info \n' +
'\t Video m3u8 playlist URL: '.cyan + video.playbackUrl + '\n' +
'\t Video tumbnail URL: '.cyan + video.posterImageUrl + '\n' +
'\t Video subtitle URL (may not exist): '.cyan + video.captionsUrl + '\n' +
'\t Video total chunks: '.cyan + video.totalChunks + '\n');
Logger_1.logger.info('Spawning ffmpeg with access token and HLS URL. This may take a few seconds...\n\n');
if (!process.stdout.columns) {
Logger_1.logger.warn('Unable to get number of columns from terminal.\n' +
'This happens sometimes in Cygwin/MSYS.\n' +
'No progress bar can be rendered, however the download process should not be affected.\n\n' +
'Please use PowerShell or cmd.exe to run destreamer on Windows.');
}
Logger_1.logger.info(`\nPlayback url: ${video.playbackUrl} \n`);
const headers = 'Authorization: Bearer ' + session.AccessToken;
if (!CommandLineParser_1.argv.noExperiments) {
await Thumbnail_1.drawThumbnail(video.posterImageUrl, session);
}
const cleanupFn = () => {
pbar.stop();
/*if (CommandLineParser_1.argv.noCleanup) {
return;
}
try {
fs_1.default.unlinkSync(video.outPath);
} catch (e) {
// Future handling of an error (maybe)
}*/
};
pbar.start(video.totalChunks, 0, {
speed: '0'
});
process.on('SIGINT', cleanupFn);
const seconds = ((parseInt(video.totalChunks))) * 60;
var speed = [];
var chunks = [];
var codes = [];
const threads = 16;
const singleLoad = seconds / threads;
const updateFn = () => {
var s = 0,
c = 0;
for (let i = 0; i < threads; i++) {
if (!isNaN(speed[i])) s = s + speed[i];
if (!isNaN(chunks[i])) c = c + chunks[i];
}
//Logger_1.logger.error(`Chunks : ${c}`);
pbar.update(c, {
speed: ((s / 1000).toString() + " Mb/s")
});
};
for (let i = 0; i < threads; i++) {
var startTime = null;
var endTime = null;
codes[i] = 2;
if (i != 0) {
startTime = (singleLoad * i);
endTime = (singleLoad * (i + 1));
if (i === (threads - 1)) { endTime = null; }
} else {
startTime = null;
endTime = (singleLoad * (i + 1));
}
var ffmpegInpt;
if (i === 0) {
ffmpegInpt = new FFmpegInput(video.playbackUrl, new Map([
['headers', headers],
['to', endTime]
]));
} else {
if (i === (threads - 1)) {
ffmpegInpt = new FFmpegInput(video.playbackUrl, new Map([
['headers', headers],
['ss', startTime],
]));
} else {
ffmpegInpt = new FFmpegInput(video.playbackUrl, new Map([
['headers', headers],
['ss', startTime],
['to', endTime]
]));
}
}
if (fs_1.default.existsSync('videos/tmp/' + i + '.mkv')) {
fs_1.default.unlinkSync('videos/tmp/' + i + '.mkv');
}
const ffmpegOutput = new FFmpegOutput('videos/tmp/' + i + '.mkv', new Map([
CommandLineParser_1.argv.acodec === 'none' ? ['an', null] : ['c:a', CommandLineParser_1.argv.acodec],
CommandLineParser_1.argv.vcodec === 'none' ? ['vn', null] : ['c:v', CommandLineParser_1.argv.vcodec],
['n', null]
]));
const ffmpegCmd = new FFmpegCommand();
ffmpegCmd.addInput(ffmpegInpt);
ffmpegCmd.addOutput(ffmpegOutput);
ffmpegCmd.on('update', async(data) => {
const currentChunks = Utils_1.ffmpegTimemarkToChunk(data.out_time);
speed[i] = parseInt((data.bitrate.split("."))[0]);
chunks[i] = currentChunks;
updateFn();
});
ffmpegCmd.on('error', (error) => {
Logger_1.logger.error(`FFmpeg: ${i} returned an error: ${error.message}`);
codes[i] = 0;
});
ffmpegCmd.on('success', () => {
codes[i] = 1;
});
ffmpegCmd.spawn();
}
// let the magic begin...
await new Promise((resolve) => {
const monitorFn = setInterval(() => {
for (let i = 0; i < threads; i++) {
if (codes[i] == 2) return;
}
for (let i = 0; i < threads; i++) {
if (codes[i] != 1) {
Logger_1.logger.error(`FFmpeg returned an error`);
cleanupFn();
stopFn();
process.exit(4 /* UNK_FFMPEG_ERROR */ );
}
}
pbar.update(video.totalChunks); // set progress bar to 100%
Logger_1.logger.info(`\nDownload finished\n`);
stopFn();
resolve();
}, 1000);
const stopFn = () => {
clearInterval(monitorFn);
};
});
await combineVideo(threads, video);
process.removeListener('SIGINT', cleanupFn);
}
}
async function combineVideo(threads, video) {
if (!await TokenCache_1.txtOperation(threads)) {
return false;
}
const pbar = new cli_progress_1.default.SingleBar({
barCompleteChar: '\u2588',
barIncompleteChar: '\u2591',
format: 'progress [{bar}] {percentage}% {speed} {eta_formatted}',
// process.stdout.columns may return undefined in some terminals (Cygwin/MSYS)
barsize: Math.floor((process.stdout.columns || 30) / 3),
stopOnComplete: true,
hideCursor: true,
});
Logger_1.logger.info(`\nAssembling Video: ${video.title} \n`);
pbar.start(video.totalChunks, 0, {
speed: '0'
});
const ffmpegInpt = new FFmpegInput('videos/tmp/file.txt', new Map([
['f', 'concat'],
['safe', 0]
]));
//Logger_1.logger.info(`\nPlayback url: ${video.playbackUrl} \n`);
const ffmpegOutput = new FFmpegOutput(video.outPath, new Map([
['c', 'copy']
]));
const ffmpegCmd = new FFmpegCommand();
const cleanupFn = () => {
pbar.stop();
try {
fs_1.default.unlinkSync(video.outPath);
} catch (e) {
// Future handling of an error (maybe)
}
};
// prepare ffmpeg command line
ffmpegCmd.addInput(ffmpegInpt);
ffmpegCmd.addOutput(ffmpegOutput);
ffmpegCmd.on('update', async(data) => {
//Logger_1.logger.info(`\nPlayback speed: ${data.out_time} \n`);
const currentChunks = Utils_1.ffmpegTimemarkToChunk(data.out_time);
pbar.update(currentChunks, {
speed: parseInt((data.bitrate.split("."))[0])
});
});
// let the magic begin...
await new Promise((resolve) => {
ffmpegCmd.on('error', (error) => {
cleanupFn();
Logger_1.logger.error(`FFmpeg returned an error: ${error.message}`);
process.exit(4 /* UNK_FFMPEG_ERROR */ );
});
ffmpegCmd.on('success', () => {
pbar.update(video.totalChunks); // set progress bar to 100%
Logger_1.logger.info(`\nVideo Assembled: ${video.outPath} \n`);
resolve();
});
ffmpegCmd.spawn();
});
return true;
}
async function main() {
var _a;
await init(); // must be first
let session;
session = (_a = tokenCache.Read()) !== null && _a !== void 0 ? _a : await DoInteractiveLogin('https://web.microsoftstream.com/', CommandLineParser_1.argv.username);
Logger_1.logger.verbose('Session and API info \n' +
'\t API Gateway URL: '.cyan + session.ApiGatewayUri + '\n' +
'\t API Gateway version: '.cyan + session.ApiGatewayVersion + '\n');
let videoGUIDs;
let outDirs;
if (CommandLineParser_1.argv.videoUrls) {
Logger_1.logger.info('Parsing video/group urls');
[videoGUIDs, outDirs] = await Utils_1.parseCLIinput(CommandLineParser_1.argv.videoUrls, CommandLineParser_1.argv.outputDirectory, session);
} else {
Logger_1.logger.info('Parsing input file');
[videoGUIDs, outDirs] = await Utils_1.parseInputFile(CommandLineParser_1.argv.inputFile, CommandLineParser_1.argv.outputDirectory, session);
}
Logger_1.logger.verbose('List of GUIDs and corresponding output directory \n' +
videoGUIDs.map((guid, i) => `\thttps://web.microsoftstream.com/video/${guid} => ${outDirs[i]} \n`).join(''));
Logger_1.logger.info(`Output directory ${ outDirs }`);
downloadVideo(videoGUIDs, outDirs, session);
}
main()
//# sourceMappingURL=destreamer.js.map
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment