1
0
mirror of https://github.com/snobu/destreamer.git synced 2026-04-21 01:21:47 +00:00

Fixes and clean up (#51)

* Simplify main

* Fix init

* Cleaner output for the end user

* Fix extractVideoGuid after sync with dev

* TokenCache: Make variable private

nit: switch to import

* Add option to disable video thumbnails

* Create a unique file name to avoid overwrite

* Remove os dependency

* Reimplement simulate

* Update README

Co-authored-by: @kylon
Co-authored-by: @snobu
This commit is contained in:
kylon
2020-04-10 18:35:57 +02:00
committed by GitHub
parent 177c3dcf71
commit 83fecf2894
8 changed files with 323 additions and 265 deletions

View File

@@ -1,16 +1,28 @@
import axios from 'axios';
import { Metadata, Session } from './Types'; import { Metadata, Session } from './Types';
import axios from 'axios';
export async function getVideoMetadata(videoGuids: string[], session: Session): Promise<Metadata[]> { function publishedDateToString(date: string) {
const dateJs = new Date(date);
const day = dateJs.getDate().toString().padStart(2, '0');
const month = (dateJs.getMonth() + 1).toString(10).padStart(2, '0');
return day+'-'+month+'-'+dateJs.getFullYear();
}
export async function getVideoMetadata(videoGuids: string[], session: Session, verbose: boolean): Promise<Metadata[]> {
let metadata: Metadata[] = []; let metadata: Metadata[] = [];
let title: string; let title: string;
let date: string;
let playbackUrl: string; let playbackUrl: string;
let posterImage: string; let posterImage: string;
await Promise.all(videoGuids.map(async guid => { await Promise.all(videoGuids.map(async guid => {
let apiUrl = `${session.ApiGatewayUri}videos/${guid}?api-version=${session.ApiGatewayVersion}`; let apiUrl = `${session.ApiGatewayUri}videos/${guid}?api-version=${session.ApiGatewayVersion}`;
console.log(`Calling ${apiUrl}`);
if (verbose)
console.info(`Calling ${apiUrl}`);
let response = await axios.get(apiUrl, let response = await axios.get(apiUrl,
{ {
headers: { headers: {
@@ -25,8 +37,10 @@ export async function getVideoMetadata(videoGuids: string[], session: Session):
.map((item: { [x: string]: string }) => { return item['playbackUrl']; })[0]; .map((item: { [x: string]: string }) => { return item['playbackUrl']; })[0];
posterImage = response.data['posterImage']['medium']['url']; posterImage = response.data['posterImage']['medium']['url'];
date = publishedDateToString(response.data['publishedDate']);
metadata.push({ metadata.push({
date: date,
title: title, title: title,
playbackUrl: playbackUrl, playbackUrl: playbackUrl,
posterImage: posterImage posterImage: posterImage

View File

@@ -47,15 +47,20 @@ Destreamer takes a [honeybadger](https://www.youtube.com/watch?v=4r7wHMg5Yjg) ap
$ node ./destreamer.js $ node ./destreamer.js
Options: Options:
--help Show help [boolean] --help Show help [boolean]
--version Show version number [boolean] --version Show version number [boolean]
--videoUrls, -V List of video urls or path to txt file containing the urls --username, -u [string]
[array] [required] --outputDirectory, -o [string] [default: "videos"]
--username, -u [string] --videoUrls, -V List of video urls or path to txt file containing the urls
--outputDirectory, -o [string] [default: "videos"] [array] [required]
--verbose, -v Print additional information to the console --simulate, -s Disable video download and print metadata
(use this before opening an issue on GitHub) information to the console
[boolean] [default: false] [boolean] [default: false]
--noThumbnails, --nthumb Do not display video thumbnails
[boolean] [default: false]
--verbose, -v Print additional information to the console
(use this before opening an issue on GitHub)
[boolean] [default: false]
``` ```
Make sure you use the right escape char for your shell if using line breaks (as this example shows). Make sure you use the right escape char for your shell if using line breaks (as this example shows).

View File

@@ -1,21 +1,19 @@
import * as fs from 'fs'; import * as fs from 'fs';
import { Session } from './Types'; import { Session } from './Types';
import { bgGreen, bgYellow, green } from 'colors'; import { bgGreen, bgYellow, green } from 'colors';
const jwtDecode = require('jwt-decode'); import jwtDecode from 'jwt-decode';
const tokenCacheFile = '.token_cache';
export class TokenCache { export class TokenCache {
private tokenCacheFile: string = '.token_cache';
public Read(): Session | null { public Read(): Session | null {
let j = null; let j = null;
if(!fs.existsSync(tokenCacheFile)) { if(!fs.existsSync(this.tokenCacheFile)) {
console.warn(bgYellow.black(`${tokenCacheFile} not found.\n`)); console.warn(bgYellow.black(`${this.tokenCacheFile} not found.\n`));
return null; return null;
} }
let f = fs.readFileSync(tokenCacheFile, 'utf8'); let f = fs.readFileSync(this.tokenCacheFile, 'utf8');
j = JSON.parse(f); j = JSON.parse(f);
interface Jwt { interface Jwt {

View File

@@ -5,6 +5,7 @@ export type Session = {
} }
export type Metadata = { export type Metadata = {
date: string;
title: string; title: string;
playbackUrl: string; playbackUrl: string;
posterImage: string; posterImage: string;

View File

@@ -1,4 +1,4 @@
import { sleep, parseVideoUrls, checkRequirements } from './utils'; import { sleep, parseVideoUrls, checkRequirements, makeUniqueTitle } from './utils';
import { TokenCache } from './TokenCache'; import { TokenCache } from './TokenCache';
import { getVideoMetadata } from './Metadata'; import { getVideoMetadata } from './Metadata';
import { Metadata, Session } from './Types'; import { Metadata, Session } from './Types';
@@ -9,7 +9,6 @@ import puppeteer from 'puppeteer';
import { execSync } from 'child_process'; import { execSync } from 'child_process';
import colors from 'colors'; import colors from 'colors';
import fs from 'fs'; import fs from 'fs';
import os from 'os';
import path from 'path'; import path from 'path';
import yargs from 'yargs'; import yargs from 'yargs';
import sanitize from 'sanitize-filename'; import sanitize from 'sanitize-filename';
@@ -45,23 +44,39 @@ const argv = yargs.options({
}, },
simulate: { simulate: {
alias: 's', alias: 's',
describe: `If this is set to true no video will be downloaded and the script describe: `Disable video download and print metadata information to the console`,
will log the video info (default: false)`, type: 'boolean',
default: false,
demandOption: false
},
noThumbnails: {
alias: 'nthumb',
describe: `Do not display video thumbnails`,
type: 'boolean', type: 'boolean',
default: false, default: false,
demandOption: false demandOption: false
}, },
verbose: { verbose: {
alias: 'v', alias: 'v',
describe: `Print additional information to the console describe: `Print additional information to the console (use this before opening an issue on GitHub)`,
(use this before opening an issue on GitHub)`,
type: 'boolean', type: 'boolean',
default: false, default: false,
demandOption: false demandOption: false
} }
}).argv; }).argv;
function init() { async function init() {
const isValidUser = !(await isElevated());
if (!isValidUser) {
const usrName = process.platform === 'win32' ? 'Admin':'root';
console.error(colors.red(
'\nERROR: Destreamer does not run as '+usrName+'!\nPlease run destreamer with a non-privileged user.\n'
));
process.exit(-1);
}
// create output directory // create output directory
if (!fs.existsSync(argv.outputDirectory)) { if (!fs.existsSync(argv.outputDirectory)) {
console.log('Creating output directory: ' + console.log('Creating output directory: ' +
@@ -69,12 +84,18 @@ function init() {
fs.mkdirSync(argv.outputDirectory); fs.mkdirSync(argv.outputDirectory);
} }
console.info('Video URLs: %s', argv.videoUrls);
console.info('Username: %s', argv.username);
console.info('Output Directory: %s', argv.outputDirectory); console.info('Output Directory: %s', argv.outputDirectory);
if (argv.username)
console.info('Username: %s', argv.username);
if (argv.simulate) if (argv.simulate)
console.info(colors.blue("There will be no video downloaded, it's only a simulation\n")); console.info(colors.yellow('Simulate mode, there will be no video download.\n'));
if (argv.verbose) {
console.info('Video URLs:');
console.info(argv.videoUrls);
}
} }
async function DoInteractiveLogin(url: string, username?: string): Promise<Session> { async function DoInteractiveLogin(url: string, username?: string): Promise<Session> {
@@ -123,18 +144,10 @@ async function DoInteractiveLogin(url: string, username?: string): Promise<Sessi
} }
function extractVideoGuid(videoUrls: string[]): 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 videoGuids: string[] = [];
let guid: string | undefined = ''; let guid: string | undefined = '';
for (let url of urls) {
console.log(url); for (const url of videoUrls) {
try { try {
guid = url.split('/').pop(); guid = url.split('/').pop();
@@ -142,27 +155,46 @@ function extractVideoGuid(videoUrls: string[]): string[] {
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);
} }
if (guid) {
if (guid)
videoGuids.push(guid); videoGuids.push(guid);
}
} }
console.log(videoGuids); if (argv.verbose) {
console.info('Video GUIDs:');
console.info(videoGuids);
}
return videoGuids; return videoGuids;
} }
async function downloadVideo(videoUrls: string[], outputDirectory: string, session: Session) { async function downloadVideo(videoUrls: string[], outputDirectory: string, session: Session) {
console.log(videoUrls);
const videoGuids = extractVideoGuid(videoUrls); const videoGuids = extractVideoGuid(videoUrls);
console.log('Fetching title and HLS URL...'); console.log('Fetching metadata...');
let metadata: Metadata[] = await getVideoMetadata(videoGuids, session);
const metadata: Metadata[] = await getVideoMetadata(videoGuids, session, argv.verbose);
if (argv.simulate) {
metadata.forEach(video => {
console.log(
colors.yellow('\n\nTitle: ') + colors.green(video.title) +
colors.yellow('\nPublished Date: ') + colors.green(video.date) +
colors.yellow('\nPlayback URL: ') + colors.green(video.playbackUrl)
);
});
return;
}
await Promise.all(metadata.map(async video => { await Promise.all(metadata.map(async video => {
video.title = sanitize(video.title);
console.log(colors.blue(`\nDownloading Video: ${video.title}\n`)); console.log(colors.blue(`\nDownloading Video: ${video.title}\n`));
video.title = makeUniqueTitle(sanitize(video.title) + ' - ' + video.date, argv.outputDirectory);
// Very experimental inline thumbnail rendering // Very experimental inline thumbnail rendering
await drawThumbnail(video.posterImage, session.AccessToken); if (!argv.noThumbnails)
await drawThumbnail(video.posterImage, session.AccessToken);
console.info('Spawning ffmpeg with access token and HLS URL. This may take a few seconds...\n'); console.info('Spawning ffmpeg with access token and HLS URL. This may take a few seconds...\n');
@@ -213,31 +245,22 @@ process.on('unhandledRejection', (reason) => {
}); });
async function main() { async function main() {
const isValidUser = !(await isElevated()); checkRequirements();
let videoUrls: string[]; await init();
if (!isValidUser) { const videoUrls: string[] = parseVideoUrls(argv.videoUrls);
const usrName = os.platform() === 'win32' ? 'Admin':'root';
console.error(colors.red('\nERROR: Destreamer does not run as '+usrName+'!\nPlease run destreamer with a non-privileged user.\n'));
process.exit(-1);
}
videoUrls = parseVideoUrls(argv.videoUrls);
if (videoUrls.length === 0) { if (videoUrls.length === 0) {
console.error(colors.red('\nERROR: No valid URL has been found!\n')); console.error(colors.red('\nERROR: No valid URL has been found!\n'));
process.exit(-1); process.exit(-1);
} }
checkRequirements();
let session = tokenCache.Read(); let session = tokenCache.Read();
if (session == null) { if (session == null) {
session = await DoInteractiveLogin(videoUrls[0], argv.username); session = await DoInteractiveLogin(videoUrls[0], argv.username);
} }
init();
downloadVideo(videoUrls, argv.outputDirectory, session); downloadVideo(videoUrls, argv.outputDirectory, session);
} }

5
package-lock.json generated
View File

@@ -133,6 +133,11 @@
"integrity": "sha512-8+KAKzEvSUdeo+kmqnKrqgeE+LcA0tjYWFY7RPProVYwnqDjukzO+3b6dLD56rYX5TdWejnEOLJYOIeh4CXKuA==", "integrity": "sha512-8+KAKzEvSUdeo+kmqnKrqgeE+LcA0tjYWFY7RPProVYwnqDjukzO+3b6dLD56rYX5TdWejnEOLJYOIeh4CXKuA==",
"dev": true "dev": true
}, },
"@types/jwt-decode": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/@types/jwt-decode/-/jwt-decode-2.2.1.tgz",
"integrity": "sha512-aWw2YTtAdT7CskFyxEX2K21/zSDStuf/ikI3yBqmwpwJF0pS+/IX5DWv+1UFffZIbruP6cnT9/LAJV1gFwAT1A=="
},
"@types/mime-types": { "@types/mime-types": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-2.1.0.tgz", "resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-2.1.0.tgz",

View File

@@ -29,6 +29,7 @@
}, },
"dependencies": { "dependencies": {
"@types/fluent-ffmpeg": "^2.1.14", "@types/fluent-ffmpeg": "^2.1.14",
"@types/jwt-decode": "^2.2.1",
"axios": "^0.19.2", "axios": "^0.19.2",
"colors": "^1.4.0", "colors": "^1.4.0",
"ffmpeg-progressbar-cli": "^1.5.0", "ffmpeg-progressbar-cli": "^1.5.0",

View File

@@ -1,6 +1,7 @@
import { execSync } from 'child_process'; import { execSync } from 'child_process';
import colors from 'colors'; import colors from 'colors';
import fs from 'fs'; import fs from 'fs';
import path from 'path';
function sanitizeUrls(urls: string[]) { function sanitizeUrls(urls: string[]) {
const rex = new RegExp(/(?:https:\/\/)?.*\/video\/[a-z0-9]{8}-(?:[a-z0-9]{4}\-){3}[a-z0-9]{12}$/, 'i'); const rex = new RegExp(/(?:https:\/\/)?.*\/video\/[a-z0-9]{8}-(?:[a-z0-9]{4}\-){3}[a-z0-9]{12}$/, 'i');
@@ -13,7 +14,7 @@ function sanitizeUrls(urls: string[]) {
if (!rex.test(url)) { if (!rex.test(url)) {
if (url !== '') if (url !== '')
console.warn(colors.yellow('Invalid URL at line ' + (i+1) + ', skip..\n')); console.warn(colors.yellow('Invalid URL at line ' + (i+1) + ', skip..'));
continue; continue;
} }
@@ -57,3 +58,13 @@ export function checkRequirements() {
process.exit(22); process.exit(22);
} }
} }
export function makeUniqueTitle(title: string, outDir: string) {
let ntitle = title;
let k = 0;
while (fs.existsSync(outDir + path.sep + ntitle + '.mp4'))
ntitle = title + ' - ' + (++k).toString();
return ntitle;
}