1
0
mirror of https://github.com/snobu/destreamer.git synced 2026-04-17 15:51:46 +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

@@ -49,10 +49,15 @@ $ 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
[array] [required]
--username, -u [string] --username, -u [string]
--outputDirectory, -o [string] [default: "videos"] --outputDirectory, -o [string] [default: "videos"]
--videoUrls, -V List of video urls or path to txt file containing the urls
[array] [required]
--simulate, -s Disable video download and print metadata
information to the console
[boolean] [default: false]
--noThumbnails, --nthumb Do not display video thumbnails
[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]

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,26 +155,45 @@ 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);
} }
if (argv.verbose) {
console.info('Video GUIDs:');
console.info(videoGuids);
} }
console.log(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
if (!argv.noThumbnails)
await drawThumbnail(video.posterImage, session.AccessToken); 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;
}