diff --git a/Metadata.ts b/Metadata.ts index c9f4d8c..3ab2a1a 100644 --- a/Metadata.ts +++ b/Metadata.ts @@ -1,13 +1,15 @@ import axios, { AxiosError } from 'axios'; import { terminal as term } from 'terminal-kit'; -import { Metadata } from './Types'; +import { Metadata, Session } from './Types'; -export async function getVideoMetadata(videoGuids: string[], session: any): Promise { - let metadata: Metadata[]; +export async function getVideoMetadata(videoGuids: string[], session: Session): Promise { + let metadata: Metadata[] = []; videoGuids.forEach(async guid => { + let apiUrl = `${session.ApiGatewayUri}videos/${guid}?api-version=${session.ApiGatewayVersion}`; + console.log(`Calling ${apiUrl}`); let content = axios.get( - `${session.ApiGatewayUri}videos/${guid}?api-version=${session.ApiGatewayVersion}`, + apiUrl, { headers: { Authorization: `Bearer ${session.AccessToken}` @@ -18,20 +20,18 @@ export async function getVideoMetadata(videoGuids: string[], session: any): Prom }) .catch((error: AxiosError) => { term.red('Error when calling Microsoft Stream API: ' + - `${error.response?.status} ${error.response?.statusText}`); - term.red("This is an unrecoverable error. Exiting..."); + `${error.response?.status} ${error.response?.statusText}\n`); + console.dir(error.response?.data); + term.red("This is an unrecoverable error. Exiting...\n"); process.exit(29); }); - let title = await content.then(data => { + let title: string = await content.then(data => { return data["name"]; }); - let playbackUrl = await content.then(data => { - // if (verbose) { - // console.log(JSON.stringify(data, undefined, 2)); - // } + let playbackUrl: string = await content.then(data => { let playbackUrl = null; try { playbackUrl = data["playbackUrls"] @@ -41,19 +41,24 @@ export async function getVideoMetadata(videoGuids: string[], session: any): Prom { return item["playbackUrl"]; })[0]; } catch (e) { - console.error(`Error fetching HLS URL: ${e}.\n playbackUrl is ${playbackUrl}`); + console.error(`Error fetching HLS URL: ${e.message}.\n playbackUrl is ${playbackUrl}`); process.exit(27); } return playbackUrl; }); + console.log(`title = ${title} \n playbackUrl = ${playbackUrl}`) + metadata.push({ title: title, playbackUrl: playbackUrl }); + }); + console.log(`metadata--------`) + console.dir(metadata); return metadata; } \ No newline at end of file diff --git a/TokenCache.ts b/TokenCache.ts index cebaac0..81fce58 100644 --- a/TokenCache.ts +++ b/TokenCache.ts @@ -1,12 +1,14 @@ import * as fs from 'fs'; +import { Session } from './Types'; const jwtDecode = require('jwt-decode'); export class TokenCache { - public Read(): string | null { - let token = null; + public Read(): Session | null { + let j = null; try { - token = fs.readFileSync(".token_cache", "utf8"); + let f = fs.readFileSync(".token_cache", "utf8"); + j = JSON.parse(f); } catch (e) { console.error(e); @@ -18,7 +20,7 @@ export class TokenCache { [key: string]: any } - const decodedJwt: Jwt = jwtDecode(token); + const decodedJwt: Jwt = jwtDecode(j.accessToken); let now = Math.floor(Date.now() / 1000); let exp = decodedJwt["exp"]; @@ -28,11 +30,18 @@ export class TokenCache { return null; } - return token; + let session: Session = { + AccessToken: j.accessToken, + ApiGatewayUri: j.apiGatewayUri, + ApiGatewayVersion: j.apiGatewayVersion + } + + return session; } - public Write(token: string): void { - fs.writeFile(".token_cache", token, (err: any) => { + public Write(session: Session): void { + let s = JSON.stringify(session, null, 4); + fs.writeFile(".token_cache", s, (err: any) => { if (err) { return console.error(err); } diff --git a/Types.ts b/Types.ts index 54248b2..451c94c 100644 --- a/Types.ts +++ b/Types.ts @@ -1,10 +1,10 @@ -export interface Session { +export type Session = { AccessToken: string; ApiGatewayUri: string; ApiGatewayVersion: string; } -export interface Metadata { +export type Metadata = { title: string; playbackUrl: string; } \ No newline at end of file diff --git a/destreamer.ts b/destreamer.ts index 97ce31e..a0d67c6 100644 --- a/destreamer.ts +++ b/destreamer.ts @@ -1,6 +1,7 @@ -import { BrowserTests } from './BrowserTests'; +import { BrowserTests } from './Tests'; import { TokenCache } from './TokenCache'; -import { Metadata, getVideoMetadata } from './Metadata'; +import { getVideoMetadata } from './Metadata'; +import { Metadata, Session } from './Types'; import { execSync } from 'child_process'; import puppeteer from 'puppeteer'; @@ -80,7 +81,7 @@ function sanityChecks() { } -async function DoInteractiveLogin(username?: string) { +async function DoInteractiveLogin(username?: string): Promise { console.log('Launching headless Chrome to perform the OpenID Connect dance...'); const browser = await puppeteer.launch({ headless: false, @@ -108,8 +109,6 @@ async function DoInteractiveLogin(username?: string) { await sleep(4000); console.log("Calling Microsoft Stream API..."); - - let cookie = await exfiltrateCookie(page); let sessionInfo: any; let session = await page.evaluate( @@ -122,7 +121,7 @@ async function DoInteractiveLogin(username?: string) { } ); - tokenCache.Write(session.AccessToken); + tokenCache.Write(session); console.log("Wrote access token to token cache."); console.log(`ApiGatewayUri: ${session.ApiGatewayUri}`); @@ -136,42 +135,59 @@ async function DoInteractiveLogin(username?: string) { function extractVideoGuid(videoUrls: string[]): string[] { + const first = videoUrls[0] as string; + const isPath = first.substring(first.length - 4) === '.txt'; + let urls: string[]; + + if (isPath) + urls = fs.readFileSync(first).toString('utf-8').split(/[\r\n]/); + else + urls = videoUrls as string[]; let videoGuids: string[] = []; - let guid: string = ""; - for (let url of videoUrls) { + let guid: string | undefined = ""; + for (let url of urls) { + console.log(url); try { - let guid = url.split('/').pop(); + guid = url.split('/').pop(); } catch (e) { console.error(`Could not split the video GUID from URL: ${e.message}`); process.exit(25); } - videoGuids.push(guid); + if (guid) { + videoGuids.push(guid); + } } + console.log(videoGuids); return videoGuids; } -async function rentVideoForLater(videoUrls: string[], outputDirectory: string, session: object) { +async function downloadVideo(videoUrls: string[], outputDirectory: string, session: Session) { + console.log(videoUrls); const videoGuids = extractVideoGuid(videoUrls); + console.log('EXTRACTED videoGuids:'); + console.log(videoGuids); let accessToken = null; + try { - accessToken = tokenCache.Read(); + let tc = tokenCache.Read(); + accessToken = tc?.AccessToken; } catch (e) { console.log("Cache is empty or expired, performing interactive log on..."); } - + console.log("Fetching title and HLS URL..."); - let metadata: Metadata[] = await getVideoMetadata(videoGuids, session: Session); - + let metadata: Metadata[] = await getVideoMetadata(videoGuids, session); + console.log('metadata:'); + console.log(metadata) metadata.forEach(video => { video.title = sanitize(video.title); term.blue(`Video Title: ${video.title}`); - 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 + @@ -183,7 +199,7 @@ async function rentVideoForLater(videoUrls: string[], outputDirectory: string, s } execSync(youtubedlCmd, { stdio: 'inherit' }); - } + }); } @@ -192,35 +208,10 @@ function sleep(ms: number) { } -async function exfiltrateCookie(page: puppeteer.Page) { - var jar = await page.cookies("https://.api.microsoftstream.com"); - var authzCookie = jar.filter(c => c.name === 'Authorization_Api')[0]; - var sigCookie = jar.filter(c => c.name === 'Signature_Api')[0]; - - if (authzCookie == null || sigCookie == null) { - await sleep(5000); - var jar = await page.cookies("https://.api.microsoftstream.com"); - var authzCookie = jar.filter(c => c.name === 'Authorization_Api')[0]; - var sigCookie = jar.filter(c => c.name === 'Signature_Api')[0]; - } - - if (authzCookie == null || sigCookie == null) { - console.error('Unable to read cookies. Try launching one more time, this is not an exact science.'); - process.exit(88); - } - - return `Authorization=${authzCookie.value}; Signature=${sigCookie.value}`; -} - - -// We should probably use Mocha or something -const args: string[] = process.argv.slice(2); -if (args[0] === 'test') -{ - BrowserTests(); -} - -else { +async function main() { sanityChecks(); - rentVideoForLater(argv.videoUrls as string[], argv.outputDirectory, argv.username); + let session = await DoInteractiveLogin(argv.username); + downloadVideo(argv.videoUrls as string[], argv.outputDirectory, session); } + +main(); \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 0c8c01f..9ab7481 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { /* Basic Options */ - "target": "ES2018", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */ + "target": "ES2019", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */ "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ // "lib": [], /* Specify library files to be included in the compilation. */ // "allowJs": true, /* Allow javascript files to be compiled. */