diff --git a/.gitignore b/.gitignore index 1c0c3bd..d14b7cd 100644 --- a/.gitignore +++ b/.gitignore @@ -3,10 +3,12 @@ *.log *.js *.zip +*.xml + +yarn.lock .chrome_data node_modules videos release build -yarn.lock \ No newline at end of file diff --git a/README.md b/README.md index dc9aba8..0951fd9 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,7 @@ Hopefully this doesn't break the end user agreement for Microsoft Stream. Since - [**Node.js**][node]: You'll need Node.js version 8.0 or higher. A GitHub Action runs tests on all major Node versions on every commit. One caveat for Node 8, if you get a `Parse Error` with `code: HPE_HEADER_OVERFLOW` you're out of luck and you'll need to upgrade to Node 10+. PLEASE NOTE WE NO LONGER TEST BUILDS AGAINST NODE 8.x. YOU ARE ON YOUR OWN. - **npm**: usually comes with Node.js, type `npm` in your terminal to check for its presence - [**ffmpeg**][ffmpeg]: a recent version (year 2019 or above), in `$PATH` or in the same directory as this README file (project root). +- [**aria2**][aria2]: aria2 is a utility for downloading files with multiple threads, fast. - [**git**][git]: one or more npm dependencies require git. Destreamer takes a [honeybadger](https://www.youtube.com/watch?v=4r7wHMg5Yjg) approach towards the OS it's running on. We've successfully tested it on Windows, macOS and Linux. @@ -241,6 +242,7 @@ Please open an [issue](https://github.com/snobu/destreamer/issues) and we'll loo [ffmpeg]: https://www.ffmpeg.org/download.html +[aria2]: https://github.com/aria2/aria2/releases [xming]: https://sourceforge.net/projects/xming/ [node]: https://nodejs.org/en/download/ [git]: https://git-scm.com/downloads diff --git a/package.json b/package.json index 76639f5..e22c084 100644 --- a/package.json +++ b/package.json @@ -1,50 +1,51 @@ { - "name": "destreamer", - "repository": { - "type": "git", - "url": "git://github.com/snobu/destreamer.git" - }, - "version": "2.1.0", - "description": "Save Microsoft Stream videos for offline enjoyment.", - "main": "build/src/destreamer.js", - "bin": "build/src/destreamer.js", - "scripts": { - "build": "echo Transpiling TypeScript to JavaScript... && node node_modules/typescript/bin/tsc && echo Destreamer was built successfully.", - "test": "mocha build/test", - "lint": "eslint src/*.ts" - }, - "keywords": [], - "author": "snobu", - "license": "MIT", - "devDependencies": { - "@types/mocha": "^8.0.4", - "@types/puppeteer": "^5.4.0", - "@types/readline-sync": "^1.4.3", - "@types/tmp": "^0.2.0", - "@types/yargs": "^15.0.11", - "@typescript-eslint/eslint-plugin": "^4.9.0", - "@typescript-eslint/parser": "^4.9.0", - "eslint": "^7.14.0", - "mocha": "^8.2.1", - "tmp": "^0.2.1" - }, - "dependencies": { - "@tedconf/fessonia": "^2.1.2", - "@types/cli-progress": "^3.8.0", - "@types/jwt-decode": "^2.2.1", - "axios": "^0.21.2", - "axios-retry": "^3.1.9", - "cli-progress": "^3.8.2", - "colors": "^1.4.0", - "is-elevated": "^3.0.0", - "iso8601-duration": "^1.3.0", - "jwt-decode": "^3.1.2", - "puppeteer": "5.5.0", - "readline-sync": "^1.4.10", - "sanitize-filename": "^1.6.3", - "terminal-image": "^1.2.1", - "typescript": "^4.1.2", - "winston": "^3.3.3", - "yargs": "^16.1.1" - } + "name": "destreamer", + "repository": { + "type": "git", + "url": "git://github.com/snobu/destreamer.git" + }, + "version": "2.1.0", + "description": "Save Microsoft Stream videos for offline enjoyment.", + "main": "build/src/destreamer.js", + "bin": "build/src/destreamer.js", + "scripts": { + "build": "echo Transpiling TypeScript to JavaScript... && tsc && echo Destreamer was built successfully.", + "watch": "tsc --watch", + "test": "mocha build/test", + "lint": "eslint src/*.ts" + }, + "keywords": [], + "author": "snobu", + "license": "MIT", + "devDependencies": { + "@types/mocha": "^8.0.4", + "@types/puppeteer": "^5.4.0", + "@types/readline-sync": "^1.4.3", + "@types/tmp": "^0.2.0", + "@types/yargs": "^15.0.11", + "@typescript-eslint/eslint-plugin": "^4.9.0", + "@typescript-eslint/parser": "^4.9.0", + "eslint": "^7.14.0", + "mocha": "^8.2.1", + "tmp": "^0.2.1" + }, + "dependencies": { + "@tedconf/fessonia": "^2.1.2", + "@types/cli-progress": "^3.8.0", + "@types/jwt-decode": "^2.2.1", + "axios": "^0.21.2", + "axios-retry": "^3.1.9", + "cli-progress": "^3.8.2", + "colors": "^1.4.0", + "is-elevated": "^3.0.0", + "iso8601-duration": "^1.3.0", + "jwt-decode": "^3.1.2", + "puppeteer": "5.5.0", + "readline-sync": "^1.4.10", + "sanitize-filename": "^1.6.3", + "terminal-image": "^1.2.1", + "typescript": "^4.1.2", + "winston": "^3.3.3", + "yargs": "^16.1.1" + } } diff --git a/src/ApiClient.ts b/src/ApiClient.ts index ac7a7b6..a8dab50 100644 --- a/src/ApiClient.ts +++ b/src/ApiClient.ts @@ -1,16 +1,18 @@ import { logger } from './Logger'; -import { Session } from './Types'; +import { ShareSession, StreamSession, Video } from './Types'; +import { publishedDateToString, publishedTimeToString } from './VideoUtils'; import axios, { AxiosRequestConfig, AxiosResponse, AxiosInstance, AxiosError } from 'axios'; import axiosRetry, { isNetworkOrIdempotentRequestError } from 'axios-retry'; +// import fs from 'fs'; -export class ApiClient { - private static instance: ApiClient; +export class StreamApiClient { + private static instance: StreamApiClient; private axiosInstance?: AxiosInstance; - private session?: Session; + private session?: StreamSession; - private constructor(session?: Session) { + private constructor(session?: StreamSession) { this.session = session; this.axiosInstance = axios.create({ baseURL: session?.ApiGatewayUri, @@ -50,16 +52,16 @@ export class ApiClient { * * @param session used if initializing */ - public static getInstance(session?: Session): ApiClient { - if (!ApiClient.instance) { - ApiClient.instance = new ApiClient(session); + public static getInstance(session?: StreamSession): StreamApiClient { + if (!StreamApiClient.instance) { + StreamApiClient.instance = new StreamApiClient(session); } - return ApiClient.instance; + return StreamApiClient.instance; } - public setSession(session: Session): void { - if (!ApiClient.instance) { + public setSession(session: StreamSession): void { + if (!StreamApiClient.instance) { logger.warn("Trying to update ApiCient session when it's not initialized!"); } @@ -113,3 +115,134 @@ export class ApiClient { }); } } + +export class ShareApiClient { + private axiosInstance: AxiosInstance; + private site: string; + + public constructor(domain: string, site: string, session: ShareSession) { + this.axiosInstance = axios.create({ + baseURL: domain, + // timeout: 7000, + headers: { + 'User-Agent': 'destreamer/3.0 ALPHA', + 'Cookie': `rtFa=${session.rtFa}; FedAuth=${session.FedAuth}` + } + }); + this.site = site; + + + // FIXME: disabled because it was messing with the direct download check + // axiosRetry(this.axiosInstance, { + // // The following option is not working. + // // We should open an issue on the relative GitHub + // shouldResetTimeout: true, + // retries: 6, + // retryDelay: (retryCount: number) => { + // return retryCount * 2000; + // }, + // retryCondition: (err: AxiosError) => { + // const retryCodes: Array = [429, 500, 502, 503]; + // if (isNetworkOrIdempotentRequestError(err)) { + // logger.warn(`${err}. Retrying request...`); + + // return true; + // } + // logger.warn(`Got HTTP code ${err?.response?.status ?? undefined}.`); + // logger.warn('Here is the error message: '); + // console.dir(err.response?.data); + // logger.warn('We called this URL: ' + err.response?.config.baseURL + err.response?.config.url); + + // const shouldRetry: boolean = retryCodes.includes(err?.response?.status ?? 0); + + // return shouldRetry; + // } + // }); + } + + + public async getVideoInfo(filePath: string, outPath: string): Promise