Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@iczero
Last active January 15, 2021 02:28
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save iczero/e8b06c4bd2d6869852650debbb26c20f to your computer and use it in GitHub Desktop.
Save iczero/e8b06c4bd2d6869852650debbb26c20f to your computer and use it in GitHub Desktop.
Script to get information and download mediasite videos
#!/usr/bin/env node
// @ts-check
// $ npm install got path-to-regexp yargs cli-progress
// put value of MediasiteAuth cookie in a file named mediasite-auth-cookie.txt
// in the same directory as this script
const fs = require('fs');
const fsP = fs.promises;
const path = require('path');
const childProcess = require('child_process');
const got = require('got');
const GOT_VERSION = require('got/package.json').version;
const pathToRegexp = require('path-to-regexp');
const yargs = require('yargs');
const cliProgress = require('cli-progress');
const INFO_ENDPOINT = '/Mediasite/PlayerService/PlayerService.svc/json/GetPlayerOptions';
/** @type {pathToRegexp.MatchFunction<{ id: string }>} */
const parsePlayEndpoint = pathToRegexp.match('/Mediasite/Play/:id');
const MPV_FORMAT_PRIORITY = ['video/x-mpeg-dash', 'video/mp4', 'audio/x-mpegurl'];
const SOURCE_URL = 'https://gist.github.com/iczero/e8b06c4bd2d6869852650debbb26c20f';
const VERSION = '0.1.0';
const USER_AGENT = `mediasite-fetch/${VERSION} (${process.platform}; +${SOURCE_URL}) Node.js/${process.version} got/${GOT_VERSION}`;
async function main() {
const argv = yargs(process.argv.slice(2))
.usage('$0 <url>', 'Retrieve mediasite presentation', yargs => {
yargs
.positional('url', {
description: 'Presentation URL',
type: 'string'
})
.option('verbose', {
description: 'Enable more logging',
alias: 'v',
type: 'boolean',
default: false
})
.option('download', {
description: 'Enable downloading of videos',
alias: 'd',
type: 'boolean',
default: false
})
.option('play', {
description: 'Open provided stream with mpv',
alias: 'p',
type: 'array'
});
}).argv;
if (argv.play) argv.play = argv.play.map(Number).filter(n => !Number.isNaN(n));
else argv.play = [];
let authCookie = (await fsP.readFile(path.join(__dirname, 'mediasite-auth-cookie.txt')))
.toString()
.replace(/[\r\n]+$/, '');
let url;
try {
url = new URL(/** @type {string} */ (argv.url));
} catch (err) {
if (err.code === 'ERR_INVALID_URL') {
console.error('error: invalid url provided');
} else console.error('error: parsing url:', err);
return 1;
}
let parsed = parsePlayEndpoint(url.pathname);
if (!parsed) {
console.error('error: invalid url', url.toString());
return 1;
}
let resourceId = parsed.params.id;
console.log('fetching resource', resourceId);
let infoEndpoint = new URL(INFO_ENDPOINT, url.origin);
let result = await got.post(infoEndpoint, {
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'User-Agent': USER_AGENT,
'Cookie': 'MediasiteAuth=' + authCookie // pure laziness
},
json: {
getPlayerOptionsRequest: {
ResourceId: resourceId,
QueryString: '',
UseScreenReader: false,
UrlReferrer: ''
}
}
}).json();
if (argv.verbose) console.log('response:', result.d || result);
if (!result.d) {
console.error('malformed response!');
return 1;
}
let data = result.d;
if (data.PlayerPresentationStatus === 6) {
console.error('error: not authorized');
console.error('error: your cookie may be invalid');
} else if (data.PlayerPresentationStatus !== 1) {
console.warn('warning: unknown PlayerPresentationStatus', data.PlayerPresentationStatus);
}
if (data.PlayerPresentationStatusMessage) {
console.log('message from server:', data.PlayerPresentationStatusMessage);
}
let presentation = data.Presentation;
if (!presentation) {
console.error('error: no presentation found');
return 1;
}
console.log('');
console.log('title:', presentation.Title);
console.log('description:', presentation.Description);
console.log('date:', new Date(presentation.UnixTime).toString());
console.log('');
let streams = data.Presentation.Streams;
if (argv.verbose) console.log('streams:', streams);
if (!streams || !streams.length) {
console.error('error: no streams found');
return 1;
}
/** @type {{ filename: string, url: string }[]} */
let toDownload = [];
let digitsLength = Math.floor(Math.log10(streams.length)) + 1;
for (let [i, stream] of streams.entries()) {
console.group(`Stream ${i}`);
if (stream.HasSlideContent) {
console.log('stream has slides');
console.log(`slides url: ${stream.SlideBaseUrl}${stream.SlideImageFileNameTemplate}` +
`?playbackTicket=${stream.SlidePlaybackTicketId}`);
if (stream.Slides.length > 0) {
console.log(`slide numbers: ${stream.Slides[0].Number}..${stream.Slides[stream.Slides.length - 1].Number}`);
} else {
console.log('no slides in slides array?');
}
console.log('');
}
let types = new Map();
for (let videoSource of stream.VideoUrls) types.set(videoSource.MimeType, videoSource.Location);
for (let [mime, url] of types) console.log(`mime: ${mime}, url: ${url}`);
if (argv.download) {
console.log('');
let url = types.get('video/mp4');
if (url) {
let filename = `${presentation.Title}-s${i.toString().padStart(digitsLength, '0')}-${presentation.PresentationId}.mp4`;
filename = filename.replace(/\//g, '_');
if (process.platform === 'win32') filename = filename.replace(/[<>:"\\|?*]/g, '_');
console.log(`will download [${filename}]`);
toDownload.push({ filename, url });
} else {
console.log('warning: no media type video/mp4 found, will not download');
}
}
if (argv.play.includes(i)) {
let targetFormat = null;
let url = null;
for (let format of MPV_FORMAT_PRIORITY) {
if (types.has(format)) {
targetFormat = format;
url = types.get(format);
break;
}
}
if (!url) {
console.warn('\nwarning: no compatible formats found for playback');
console.warn('warning: available formats are', [...types.keys()]);
} else {
console.log('\nstarting mpv for format', targetFormat);
childProcess.spawn('mpv', ['--', url], {
stdio: ['ignore', 'inherit', 'inherit'],
detached: true
});
}
}
console.groupEnd();
console.log('');
}
if (argv.download) await download(toDownload);
return 0;
}
/**
* Download multiple files
* @param {{ filename: string, url: string }[]} urls
*/
async function download(urls) {
console.log('');
console.log(`downloading ${urls.length} file(s)...`);
let bar = new cliProgress.MultiBar({
format: '{filename} | [{bar}] {percentage}% | {value}/{total}',
hideCursor: false,
clearOnComplete: false
});
/** @type {Promise<void>[]} */
let promises = [];
for (let { filename, url } of urls) {
let progress = bar.create(1, 0, { filename });
let writeStream = fs.createWriteStream(filename);
let source = got.stream(url, { headers: { 'User-Agent': USER_AGENT } });
source.on('downloadProgress', s => {
progress.update(s.transferred);
progress.setTotal(s.total);
});
source.pipe(writeStream);
promises.push(new Promise((resolve, reject) => {
writeStream.on('finish', resolve);
writeStream.on('error', reject);
source.on('error', reject);
}));
}
await Promise.all(promises);
bar.stop();
console.log('\nDone');
}
main()
.then(code => process.exit(code))
.catch(err => console.error('error: uncaught exception:', err));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment