-
Notifications
You must be signed in to change notification settings - Fork 33
/
index.js
executable file
·176 lines (159 loc) · 5.14 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
#! /usr/bin/env node
require('babel-polyfill')
const program = require('commander')
const chalk = require('chalk')
const rp = require('request-promise')
const request = require('request')
const PleasantProgress = require('pleasant-progress')
const path = require('path')
const fs = require('fs')
let urlValue
let outputDir
const progress = new PleasantProgress()
program
.version('0.0.1')
.arguments('<url> <output-dir>')
.option('-c, --count', 'Add the number of the video to the filename (only for playlists and series)')
.action((url, output) => {
urlValue = url
outputDir = path.resolve(output)
})
program.parse(process.argv)
if (process.argv.slice(2).length < 2) {
program.outputHelp()
process.exit()
}
if (!/egghead.io\/(lessons|series|playlists)\//.test(urlValue)) {
error('unsupported url!')
}
// await is only supported in functions (with the async keyword)
doTheMagic()
async function doTheMagic () {
const videos = await getVideoData()
if (!videos.length) {
error('no video found!')
}
success(`Found ${videos.length} ${(videos.length) > 1 ? 'videos' : 'video'}`)
createOutputDirectoryIfNeeded()
let i = 1
for (const {url, filename} of videos) {
progress.start(`Downloading video ${i} out of ${videos.length}: '${filename}'`)
const p = path.join(outputDir, (program.count ? `${i}-${filename}` : filename))
const stream = fs.createWriteStream(p)
await new Promise((resolve, reject) => {
request(url)
.on('error', () => {
error(`download of '${url}' failed!`, false)
reject()
})
.on('end', () => {
resolve()
})
.pipe(stream)
})
stream.close()
progress.stop(true)
i++
}
success('Done!')
}
// loads the url and parses it, when it's playlist/serie loads the video pages
// too, and returns an array with the video data
async function getVideoData () {
try {
const isLesson = /egghead.io\/lessons\//.test(urlValue)
let source = await rp(urlValue)
if (isLesson) {
success('The URL is a lession')
// process the lesson page
const videoData = parseLessonPage(source)
if (videoData) {
return [videoData]
} else {
error(`failed to parse the lesson page '${urlValue}'}`)
}
} else {
let lessonURLs = []
success('The URL is a playlist or series')
// get the urls of the lessions
const re = /<h4 class="title"><a href="(https:\/\/egghead.io\/lessons\/.+?)">/g
// regexp in js have no matchAll method or something similiar..
let match
while ((match = re.exec(source))) {
lessonURLs.push(match[1])
}
success(`Found ${lessonURLs.length} ${(lessonURLs.length) > 1 ? 'lessons' : 'lesson'}`)
progress.start('Fetching lesson pages')
// fetch and process the lessons, start all requests at the same time to save time.
const promises = lessonURLs.map(processLessonURL)
const result = await Promise.all(promises.map(reflect))
progress.stop(true)
// get the urls that succeded and thos that failed
const videoURLs = result.filter(v => (v.state === 'resolved')).map(v => v.value)
const failed = result.filter(v => (v.state === 'rejected'))
// check if we have some lesson pages that failed (wrong url or paid)
if (failed.length) {
error(`Failed to parse the following lesson pages: ${failed.map(v => `'${v.value}'`).join(',')}`, false)
}
return videoURLs
}
} catch (e) {
error(`fetching the url '${urlValue}' failed!`)
}
}
// fetches the lesson page and calls parseLessonPage on it
function processLessonURL (url) {
return new Promise(async (resolve, reject) => {
rp(url).then((source) => {
const videoData = parseLessonPage(source)
if (videoData) {
resolve(videoData)
} else {
reject(url)
}
}, () => {
reject(url)
})
})
}
// parses the lesson page, returns the video data if found.
function parseLessonPage (source) {
const re = /<meta itemprop="name" content="(.+?)".+<meta itemprop="contentURL" content="https:\/\/embed-ssl.wistia.com\/deliveries\/(.+?)\.bin"/
const result = re.exec(source)
if (result) {
return {
filename: result[1],
url: `https://embed-ssl.wistia.com/deliveries/${result[2]}/file.mp4`
}
}
}
// creates a directory
function createOutputDirectoryIfNeeded () {
try {
const stats = fs.lstatSync(outputDir)
if (!stats.isDirectory()) {
error(`Can't create the output directory '${outputDir}' because a file with the same name exists`)
}
} catch (e) {
try {
fs.mkdirSync(outputDir)
} catch (err) {
error(`Creating the output directory '${outputDir}' failed with error '${err}'`)
}
}
}
// helper functions
function success (message) {
console.log(chalk.green(message))
}
function error (message, exit = true) {
console.log(chalk.red(`Error: ${message}`))
if (exit) {
process.exit(1)
}
}
// wraps a promise in another promise that resolves when the promise either resolves or rejects
function reflect (promise) {
return promise.then(x => ({state: 'resolved', value: x}),
e => ({state: 'rejected', value: e}))
}