forked from snobu/destreamer
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdestreamer.ts
More file actions
287 lines (241 loc) · 9.69 KB
/
destreamer.ts
File metadata and controls
287 lines (241 loc) · 9.69 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
import { sleep, getVideoUrls } from './utils';
import { execSync } from 'child_process';
import isElevated from 'is-elevated';
import puppeteer from 'puppeteer';
import { terminal as term } from 'terminal-kit';
import fs from 'fs';
import os from 'os';
import path from 'path';
import yargs from 'yargs';
import sanitize from 'sanitize-filename';
import axios from 'axios';
/**
* exitCode 25 = cannot split videoID from videUrl
* exitCode 27 = no hlsUrl in the API response
* exitCode 29 = invalid response from API
* exitCode 88 = error extracting cookies
*/
const argv = yargs.options({
username: { alias: "u", type: 'string', demandOption: false },
outputDirectory: { type: 'string', alias: 'o', default: 'videos' },
videoUrls: {
alias: "V",
describe: `List of video urls or path to txt file containing the urls`,
type: 'array',
demandOption: true
},
format: {
alias:"f",
describe: `Expose youtube-dl --format option, for details see\n
https://github.com/ytdl-org/youtube-dl/blob/master/README.md#format-selection`,
type:'string',
demandOption: false
},
simulate: {
alias: "s",
describe: `If this is set to true no video will be downloaded and the script
will log the video info (default: false)`,
type: "boolean",
default: false,
demandOption: false
},
verbose: {
alias: "v",
describe: `Print additional information to the console
(use this before opening an issue on GitHub)`,
type: "boolean",
default: false,
demandOption: false
}
}).argv;
if (argv.simulate){
console.info('Video URLs: %s', argv.videoUrls);
console.info('Username: %s', argv.username);
term.blue("There will be no video downloaded, it's only a simulation\n");
} else {
console.info('Video URLs: %s', argv.videoUrls);
console.info('Username: %s', argv.username);
console.info('Output Directory: %s', argv.outputDirectory);
console.info('Video/Audio Quality: %s', argv.format);
}
function sanityChecks() {
try {
const ytdlVer = execSync('youtube-dl --version');
term.green(`Using youtube-dl version ${ytdlVer}`);
}
catch (e) {
console.error('You need youtube-dl in $PATH for this to work. Make sure it is a relatively recent one, baked after 2019.');
process.exit(22);
}
try {
const ffmpegVer = execSync('ffmpeg -version')
.toString().split('\n')[0];
term.green(`Using ${ffmpegVer}\n`);
}
catch (e) {
console.error('FFmpeg is missing. You need a fairly recent release of FFmpeg in $PATH.');
}
if (!fs.existsSync(argv.outputDirectory)){
console.log('Creating output directory: ' +
process.cwd() + path.sep + argv.outputDirectory);
fs.mkdirSync(argv.outputDirectory);
}
}
async function rentVideoForLater(videoUrls: string[], outputDirectory: string, username?: string) {
if (argv.verbose) {
console.log('[VERBOSE] URL List:');
console.log(videoUrls);
}
console.log('Launching headless Chrome to perform the OpenID Connect dance...');
const browser = await puppeteer.launch({
// Switch to false if you need to login interactively
headless: false,
args: ['--disable-dev-shm-usage']
});
const page = (await browser.pages())[0];
console.log('Navigating to STS login page...');
// This breaks on slow connections, needs more reliable logic
await page.goto(videoUrls[0], { waitUntil: "networkidle2" });
await page.waitForSelector('input[type="email"]');
if (username) {
await page.keyboard.type(username);
await page.click('input[type="submit"]');
}
await browser.waitForTarget(target => target.url().includes('microsoftstream.com/'), { timeout: 90000 });
console.log('We are logged in.');
// We may or may not need to sleep here.
// Who am i to deny a perfectly good nap?
await sleep(1500);
for (let videoUrl of videoUrls) {
let videoID = videoUrl.split('/').pop() ??
(console.error("Couldn't split the videoID, wrong url"), process.exit(25));
// changed waitUntil value to load (page completly loaded)
await page.goto(videoUrl, { waitUntil: 'load' });
await sleep(2000);
// try this instead of hardcoding sleep
// https://github.com/GoogleChrome/puppeteer/issues/3649
console.log("Page loaded")
await sleep(4000);
console.log("Calling Microsoft Stream API...");
let sessionInfo: any;
let session = await page.evaluate(
() => {
return {
AccessToken: sessionInfo.AccessToken,
ApiGatewayUri: sessionInfo.ApiGatewayUri,
ApiGatewayVersion: sessionInfo.ApiGatewayVersion
};
}
);
if (argv.verbose) {
console.log(`\n\n[VERBOSE] ApiGatewayUri: ${session.ApiGatewayUri}\n
ApiGatewayVersion: ${session.ApiGatewayVersion}\n\n`);
}
console.log(`ApiGatewayUri: ${session.ApiGatewayUri}`);
console.log(`ApiGatewayVersion: ${session.ApiGatewayVersion}`);
console.log("Fetching title and HLS URL...");
var [title, date, hlsUrl] = await getVideoInfo(videoID, session);
const sanitized = sanitize(title);
title = (sanitized == "") ?
`Video${videoUrls.indexOf(videoUrl)}` :
sanitized;
// Add date
title += ' - '+date;
// Add random index to prevent unwanted file overwrite!
let k = 0;
let ntitle = title;
while (fs.existsSync(outputDirectory+"/"+ntitle+".mp4"))
ntitle = title+' - '+(++k).toString();
title = ntitle;
term.blue("Video title is: ");
console.log(`${title} \n`);
console.log('Spawning youtube-dl with cookie and HLS URL...');
const format = argv.format ? `-f "${argv.format}"` : "";
var youtubedlCmd = 'youtube-dl --no-call-home --no-warnings ' + format +
` --output "${outputDirectory}/${title}.mp4" --add-header ` +
`"Authorization: Bearer ${session.AccessToken}" "${hlsUrl}"`;
if (argv.simulate) {
youtubedlCmd = youtubedlCmd + " -s";
}
if (argv.verbose) {
console.log(`\n\n[VERBOSE] Invoking youtube-dl:\n${youtubedlCmd}\n\n`);
}
execSync(youtubedlCmd, { stdio: 'inherit' });
}
console.log("At this point Chrome's job is done, shutting it down...");
await browser.close();
}
async function getVideoInfo(videoID: string, session: any) {
let title: string;
let date: string;
let hlsUrl: string;
let content = axios.get(
`${session.ApiGatewayUri}videos/${videoID}` +
`?$expand=creator,tokens,status,liveEvent,extensions&api-version=${session.ApiGatewayVersion}`,
{
headers: {
Authorization: `Bearer ${session.AccessToken}`
}
})
.then(function (response) {
return response.data;
})
.catch(function (error) {
term.red('Error when calling Microsoft Stream API: ' +
`${error.response.status} ${error.response.reason}`);
console.error(error.response.status);
console.error(error.response.data);
console.error("Exiting...");
if (argv.verbose) {
term.red("[VERBOSE]");
console.error(error)
}
process.exit(29);
});
title = await content.then(data => {
return data["name"];
});
date = await content.then(data => {
const dateJs = new Date(data["publishedDate"]);
const day = dateJs.getDate().toString().padStart(2, '0');
const month = (dateJs.getMonth() + 1).toString(10).padStart(2, '0');
return day+'-'+month+'-'+dateJs.getFullYear();
});
hlsUrl = await content.then(data => {
if (argv.verbose) {
console.log(JSON.stringify(data, undefined, 2));
}
let playbackUrl = null;
try {
playbackUrl = data["playbackUrls"]
.filter((item: { [x: string]: string; }) =>
item["mimeType"] == "application/vnd.apple.mpegurl")
.map((item: { [x: string]: string }) =>
{ return item["playbackUrl"]; })[0];
}
catch (e) {
console.error(`Error fetching HLS URL: ${e}.\n playbackUrl is ${playbackUrl}`);
process.exit(27);
}
return playbackUrl;
});
return [title, date, hlsUrl];
}
// FIXME
process.on('unhandledRejection', (reason, promise) => {
term.red("Unhandled error!\nTimeout or fatal error, please check your downloads and try again if necessary.\n");
term.red(reason);
throw new Error("Killing process..\n");
});
async function main() {
const isValidUser = !(await isElevated());
if (!isValidUser) {
const usrName = os.platform() === 'win32' ? 'Admin':'root';
term.red('\nERROR: Destreamer does not run as '+usrName+'!\nPlease run destreamer with a non-privileged user.\n');
process.exit(-1);
}
sanityChecks();
rentVideoForLater(getVideoUrls(argv.videoUrls), argv.outputDirectory, argv.username);
}
// run
main();