Fix updated README and fix indenting
This commit is contained in:
parent
db950e8f80
commit
b9c3aa3a0e
2 changed files with 44 additions and 84 deletions
60
README.md
60
README.md
|
@ -12,13 +12,17 @@ Alpha-quality, don't expect much. It does work though, so that's a neat feature.
|
||||||
|
|
||||||
It's slow (e.g. a 60-min video takes 20-30 minutes to download). Not much i can do about it for now unless i find a better way than ripping HLS.
|
It's slow (e.g. a 60-min video takes 20-30 minutes to download). Not much i can do about it for now unless i find a better way than ripping HLS.
|
||||||
|
|
||||||
|
## NEWS
|
||||||
|
|
||||||
|
- We now have a token cache so we can reuse access tokens for their one hour lifetime. What this really means is that within one hour you only need to login via the popup browser once.
|
||||||
|
|
||||||
## This project is now looking for contributors
|
## This project is now looking for contributors
|
||||||
<img src="https://www.whitesourcesoftware.com/wp-content/uploads/2018/02/10-github-to-follow.jpg" width=400 />
|
<img src="https://www.whitesourcesoftware.com/wp-content/uploads/2018/02/10-github-to-follow.jpg" width=400 />
|
||||||
|
|
||||||
Roadmap -
|
Roadmap -
|
||||||
- [ ] Token cache (so you don't have to log in every time you run destreamer)
|
- [X] Token cache (so you don't have to log in every time you run destreamer)
|
||||||
- [ ] Download closed captions if available
|
- [ ] Download closed captions if available
|
||||||
- [ ] Performance improvements (via aria2c maybe?)
|
- [ ] Performance improvements (via aria2c maybe?) // _This is under consideration, we're not sure if this borders on abusing the streaming endpoints or not._
|
||||||
- [ ] Single static binary (for each major OS)
|
- [ ] Single static binary (for each major OS)
|
||||||
|
|
||||||
Send a quality PR first and i'll add you as a contributor to the repository.
|
Send a quality PR first and i'll add you as a contributor to the repository.
|
||||||
|
@ -27,12 +31,10 @@ Send a quality PR first and i'll add you as a contributor to the repository.
|
||||||
|
|
||||||
Hopefully this doesn't break the end user agreement for Microsoft Stream. Since we're simply saving the HLS stream to disk as if we were a browser, this does not abuse the streaming endpoints. However i take no responsibility if either Microsoft or your Office 365 admins request a chat with you in a small white room.
|
Hopefully this doesn't break the end user agreement for Microsoft Stream. Since we're simply saving the HLS stream to disk as if we were a browser, this does not abuse the streaming endpoints. However i take no responsibility if either Microsoft or your Office 365 admins request a chat with you in a small white room.
|
||||||
|
|
||||||
|
|
||||||
## PREREQS
|
## PREREQS
|
||||||
|
|
||||||
* **Node.js**: anything above v8.0 seems to work. A GitHub Action runs tests on all major Node versions on every commit.
|
* **Node.js**: anything above v8.0 seems to work. A GitHub Action runs tests on all major Node versions on every commit.
|
||||||
* **youtube-dl**: https://ytdl-org.github.io/youtube-dl/download.html, you'll need a fairly recent version that understands encrypted HLS streams. This needs to be in your $PATH. Destreamer calls `youtube-dl` with a bunch of arguments.
|
* **ffmpeg**: a recent version (year 2019 or above), in `$PATH` or in the same directory as `destreamer.ts`.
|
||||||
* **ffmpeg**: a recent version (year 2019 or above), in `$PATH`.
|
|
||||||
|
|
||||||
Destreamer takes a [honeybadger](https://www.youtube.com/watch?v=4r7wHMg5Yjg) approach towards the OS it's running on, tested on Windows, results may vary, feel free to open an issue if trouble arise.
|
Destreamer takes a [honeybadger](https://www.youtube.com/watch?v=4r7wHMg5Yjg) approach towards the OS it's running on, tested on Windows, results may vary, feel free to open an issue if trouble arise.
|
||||||
|
|
||||||
|
@ -50,14 +52,6 @@ Options:
|
||||||
--videoUrls [array] [required]
|
--videoUrls [array] [required]
|
||||||
--username [string]
|
--username [string]
|
||||||
--outputDirectory [string] [default: "videos"]
|
--outputDirectory [string] [default: "videos"]
|
||||||
--format, -f Expose youtube-dl --format option, for details see
|
|
||||||
|
|
||||||
https://github.com/ytdl-org/youtube-dl/blob/master/README.m
|
|
||||||
d#format-selection [string]
|
|
||||||
--simulate, -s If this is set to true no video will be downloaded and the
|
|
||||||
script
|
|
||||||
will log the video info (default: false)
|
|
||||||
[boolean] [default: false]
|
|
||||||
--verbose, -v Print additional information to the console
|
--verbose, -v Print additional information to the console
|
||||||
(use this before opening an issue on GitHub)
|
(use this before opening an issue on GitHub)
|
||||||
[boolean] [default: false]
|
[boolean] [default: false]
|
||||||
|
@ -74,13 +68,9 @@ You can use an absolute path for `--outputDirectory`, for example `/mnt/videos`.
|
||||||
|
|
||||||
Your video URLs **must** include the URL schema (the leading `https://`).
|
Your video URLs **must** include the URL schema (the leading `https://`).
|
||||||
|
|
||||||
To choose preferred video format and quality you can use the `-f` (`--format`) option. It exposes a native [`youtube-dl` parameter][4].
|
## RANDOM NOTE
|
||||||
If you do not pass this parameter then `youtube-dl` will download the best available quality for each video.
|
|
||||||
|
|
||||||
## IMPORTANT NOTE
|
Just ignore this error, we already have what we need to start the download, no time to deal with collaterals -
|
||||||
For now you need to keep the puppeteer browser window open (the one that pops up for logging in) if you download more than one video in one go.
|
|
||||||
|
|
||||||
Also, just ignore this error, we already have what we need to start the download, no time to deal with collaterals -
|
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
@ -88,35 +78,7 @@ Also, just ignore this error, we already have what we need to start the download
|
||||||
## EXPECTED OUTPUT
|
## EXPECTED OUTPUT
|
||||||
|
|
||||||
```
|
```
|
||||||
Using youtube-dl version 2019.01.17
|
<<<< OUTPUT >>>>
|
||||||
Launching headless Chrome to perform the OpenID Connect dance...
|
|
||||||
|
|
||||||
Navigating to STS login page...
|
|
||||||
We are logged in. Sorry, i mean "you".
|
|
||||||
Got cookie. Consuming cookie...
|
|
||||||
Looking up AMS stream locator...
|
|
||||||
Video title is: Mondays with IGD 11th March-2019
|
|
||||||
At this point Chrome's job is done, shutting it down...
|
|
||||||
Constructing HLSv3 URL...
|
|
||||||
Spawning youtube-dl with cookie and HLSv3 URL...
|
|
||||||
|
|
||||||
[generic] manifest(format=m3u8-aapl-v3): Requesting header
|
|
||||||
[generic] manifest(format=m3u8-aapl-v3): Downloading m3u8 information
|
|
||||||
[download] Destination: Mondays with IGD 11th March-2019.mp4
|
|
||||||
ffmpeg version 4.0.2 Copyright (c) 2000-2018 the FFmpeg developers
|
|
||||||
built with gcc 7.3.1 (GCC) 20180722
|
|
||||||
configuration: --enable-gpl --enable-version3 --enable-sdl2 --enable-bzlib
|
|
||||||
|
|
||||||
[...]
|
|
||||||
|
|
||||||
frame= 8435 fps= 67 q=-1.0 Lsize= 192018kB time=00:05:37.38 bitrate=4662.3kbits/s speed=2.68x
|
|
||||||
video:186494kB audio:5380kB subtitle:0kB other streams:0kB global headers:0kB muxing overhead: 0.074759%
|
|
||||||
[ffmpeg] Downloaded 196626728 bytes
|
|
||||||
[download] Download completed
|
|
||||||
```
|
```
|
||||||
|
|
||||||
The video is now saved under `videos/`, or whatever the `outputDirectory` const points to.
|
The video is now saved under `videos/`, or the path from `--outputDirectory`.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
[4]: https://github.com/ytdl-org/youtube-dl/blob/master/README.md#format-selection
|
|
|
@ -43,7 +43,7 @@ const argv = yargs.options({
|
||||||
},
|
},
|
||||||
}).argv;
|
}).argv;
|
||||||
|
|
||||||
if (argv.simulate){
|
if (argv.simulate) {
|
||||||
console.info('Video URLs: %s', argv.videoUrls);
|
console.info('Video URLs: %s', argv.videoUrls);
|
||||||
console.info('Username: %s', argv.username);
|
console.info('Username: %s', argv.username);
|
||||||
console.info(colors.green('There will be no video downloaded, it\'s only a simulation\n'));
|
console.info(colors.green('There will be no video downloaded, it\'s only a simulation\n'));
|
||||||
|
@ -74,7 +74,7 @@ function sanityChecks() {
|
||||||
console.error('FFmpeg is missing. You need a fairly recent release of FFmpeg in $PATH.');
|
console.error('FFmpeg is missing. You need a fairly recent release of FFmpeg in $PATH.');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!fs.existsSync(argv.outputDirectory)){
|
if (!fs.existsSync(argv.outputDirectory)) {
|
||||||
console.log('Creating output directory: ' +
|
console.log('Creating output directory: ' +
|
||||||
process.cwd() + path.sep + argv.outputDirectory);
|
process.cwd() + path.sep + argv.outputDirectory);
|
||||||
fs.mkdirSync(argv.outputDirectory);
|
fs.mkdirSync(argv.outputDirectory);
|
||||||
|
@ -94,7 +94,7 @@ async function DoInteractiveLogin(username?: string): Promise<Session> {
|
||||||
// This breaks on slow connections, needs more reliable logic
|
// This breaks on slow connections, needs more reliable logic
|
||||||
await page.goto('https://web.microsoftstream.com', { waitUntil: 'networkidle2' });
|
await page.goto('https://web.microsoftstream.com', { waitUntil: 'networkidle2' });
|
||||||
await page.waitForSelector('input[type="email"]');
|
await page.waitForSelector('input[type="email"]');
|
||||||
|
|
||||||
if (username) {
|
if (username) {
|
||||||
await page.keyboard.type(username);
|
await page.keyboard.type(username);
|
||||||
await page.click('input[type="submit"]');
|
await page.click('input[type="submit"]');
|
||||||
|
@ -107,21 +107,21 @@ async function DoInteractiveLogin(username?: string): Promise<Session> {
|
||||||
await sleep(1500);
|
await sleep(1500);
|
||||||
|
|
||||||
console.info('Got cookie. Consuming cookie...');
|
console.info('Got cookie. Consuming cookie...');
|
||||||
|
|
||||||
await sleep(4000);
|
await sleep(4000);
|
||||||
console.info('Calling Microsoft Stream API...');
|
console.info('Calling Microsoft Stream API...');
|
||||||
|
|
||||||
let sessionInfo: any;
|
let sessionInfo: any;
|
||||||
let session = await page.evaluate(
|
let session = await page.evaluate(
|
||||||
() => {
|
() => {
|
||||||
return {
|
return {
|
||||||
AccessToken: sessionInfo.AccessToken,
|
AccessToken: sessionInfo.AccessToken,
|
||||||
ApiGatewayUri: sessionInfo.ApiGatewayUri,
|
ApiGatewayUri: sessionInfo.ApiGatewayUri,
|
||||||
ApiGatewayVersion: sessionInfo.ApiGatewayVersion
|
ApiGatewayVersion: sessionInfo.ApiGatewayVersion
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
tokenCache.Write(session);
|
tokenCache.Write(session);
|
||||||
console.info('Wrote access token to token cache.');
|
console.info('Wrote access token to token cache.');
|
||||||
|
|
||||||
|
@ -151,8 +151,7 @@ function extractVideoGuid(videoUrls: string[]): string[] {
|
||||||
try {
|
try {
|
||||||
guid = url.split('/').pop();
|
guid = url.split('/').pop();
|
||||||
}
|
}
|
||||||
catch (e)
|
catch (e) {
|
||||||
{
|
|
||||||
console.error(`Could not split the video GUID from URL: ${e.message}`);
|
console.error(`Could not split the video GUID from URL: ${e.message}`);
|
||||||
process.exit(25);
|
process.exit(25);
|
||||||
}
|
}
|
||||||
|
@ -160,7 +159,7 @@ function extractVideoGuid(videoUrls: string[]): string[] {
|
||||||
videoGuids.push(guid);
|
videoGuids.push(guid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(videoGuids);
|
console.log(videoGuids);
|
||||||
return videoGuids;
|
return videoGuids;
|
||||||
}
|
}
|
||||||
|
@ -169,42 +168,42 @@ function extractVideoGuid(videoUrls: string[]): string[] {
|
||||||
async function downloadVideo(videoUrls: string[], outputDirectory: string, session: Session) {
|
async function downloadVideo(videoUrls: string[], outputDirectory: string, session: Session) {
|
||||||
console.log(videoUrls);
|
console.log(videoUrls);
|
||||||
const videoGuids = extractVideoGuid(videoUrls);
|
const videoGuids = extractVideoGuid(videoUrls);
|
||||||
|
|
||||||
console.log('Fetching title and HLS URL...');
|
console.log('Fetching title and HLS URL...');
|
||||||
let metadata: Metadata[] = await getVideoMetadata(videoGuids, session);
|
let metadata: Metadata[] = await getVideoMetadata(videoGuids, session);
|
||||||
await Promise.all(metadata.map(async video => {
|
await Promise.all(metadata.map(async video => {
|
||||||
video.title = sanitize(video.title);
|
video.title = sanitize(video.title);
|
||||||
console.log(colors.blue(`\nDownloading Video: ${video.title}\n`));
|
console.log(colors.blue(`\nDownloading Video: ${video.title}\n`));
|
||||||
|
|
||||||
// Very experimental inline thumbnail rendering
|
// Very experimental inline thumbnail rendering
|
||||||
await drawThumbnail(video.posterImage, session.AccessToken);
|
await drawThumbnail(video.posterImage, session.AccessToken);
|
||||||
|
|
||||||
console.info('Spawning ffmpeg with access token and HLS URL...');
|
console.info('Spawning ffmpeg with access token and HLS URL...');
|
||||||
|
|
||||||
const outputPath = outputDirectory + path.sep + video.title + '.mp4';
|
const outputPath = outputDirectory + path.sep + video.title + '.mp4';
|
||||||
|
|
||||||
ffmpeg()
|
ffmpeg()
|
||||||
.input(video.playbackUrl)
|
.input(video.playbackUrl)
|
||||||
.inputOption([
|
.inputOption([
|
||||||
// Never remove those "useless" escapes or ffmpeg will not
|
// Never remove those "useless" escapes or ffmpeg will not
|
||||||
// pick up the header correctly
|
// pick up the header correctly
|
||||||
// eslint-disable-next-line no-useless-escape
|
// eslint-disable-next-line no-useless-escape
|
||||||
'-headers', `Authorization:\ Bearer\ ${session.AccessToken}`
|
'-headers', `Authorization:\ Bearer\ ${session.AccessToken}`
|
||||||
])
|
])
|
||||||
.format('mp4')
|
.format('mp4')
|
||||||
.saveToFile(outputPath)
|
.saveToFile(outputPath)
|
||||||
.on('codecData', data => {
|
.on('codecData', data => {
|
||||||
console.log(`Input is ${data.video} with ${data.audio} audio.`);
|
console.log(`Input is ${data.video} with ${data.audio} audio.`);
|
||||||
})
|
})
|
||||||
.on('progress', progress => {
|
.on('progress', progress => {
|
||||||
console.log(progress);
|
console.log(progress);
|
||||||
})
|
})
|
||||||
.on('error', err => {
|
.on('error', err => {
|
||||||
console.log(`ffmpeg returned an error: ${err.message}`);
|
console.log(`ffmpeg returned an error: ${err.message}`);
|
||||||
})
|
})
|
||||||
.on('end', () => {
|
.on('end', () => {
|
||||||
console.log(`Download finished: ${outputPath}`);
|
console.log(`Download finished: ${outputPath}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
@ -218,8 +217,7 @@ function sleep(ms: number) {
|
||||||
async function main() {
|
async function main() {
|
||||||
sanityChecks();
|
sanityChecks();
|
||||||
let session = tokenCache.Read();
|
let session = tokenCache.Read();
|
||||||
if (session == null)
|
if (session == null) {
|
||||||
{
|
|
||||||
session = await DoInteractiveLogin(argv.username);
|
session = await DoInteractiveLogin(argv.username);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue