diff options
Diffstat (limited to 'server/helpers/youtube-dl.ts')
-rw-r--r-- | server/helpers/youtube-dl.ts | 142 |
1 files changed, 142 insertions, 0 deletions
diff --git a/server/helpers/youtube-dl.ts b/server/helpers/youtube-dl.ts new file mode 100644 index 000000000..c59ab9de0 --- /dev/null +++ b/server/helpers/youtube-dl.ts | |||
@@ -0,0 +1,142 @@ | |||
1 | import * as youtubeDL from 'youtube-dl' | ||
2 | import { truncate } from 'lodash' | ||
3 | import { CONFIG, CONSTRAINTS_FIELDS, VIDEO_CATEGORIES } from '../initializers' | ||
4 | import { join } from 'path' | ||
5 | import * as crypto from 'crypto' | ||
6 | import { logger } from './logger' | ||
7 | |||
8 | export type YoutubeDLInfo = { | ||
9 | name: string | ||
10 | description: string | ||
11 | category: number | ||
12 | licence: number | ||
13 | nsfw: boolean | ||
14 | tags: string[] | ||
15 | thumbnailUrl: string | ||
16 | } | ||
17 | |||
18 | function getYoutubeDLInfo (url: string): Promise<YoutubeDLInfo> { | ||
19 | return new Promise<YoutubeDLInfo>((res, rej) => { | ||
20 | const options = [ '-j', '--flat-playlist' ] | ||
21 | |||
22 | youtubeDL.getInfo(url, options, (err, info) => { | ||
23 | if (err) return rej(err) | ||
24 | |||
25 | const obj = normalizeObject(info) | ||
26 | |||
27 | return res(buildVideoInfo(obj)) | ||
28 | }) | ||
29 | }) | ||
30 | } | ||
31 | |||
32 | function downloadYoutubeDLVideo (url: string) { | ||
33 | const hash = crypto.createHash('sha256').update(url).digest('hex') | ||
34 | const path = join(CONFIG.STORAGE.VIDEOS_DIR, hash + '-import.mp4') | ||
35 | |||
36 | logger.info('Importing video %s', url) | ||
37 | |||
38 | const options = [ '-f', 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/best', '-o', path ] | ||
39 | |||
40 | return new Promise<string>((res, rej) => { | ||
41 | youtubeDL.exec(url, options, async (err, output) => { | ||
42 | if (err) return rej(err) | ||
43 | |||
44 | return res(path) | ||
45 | }) | ||
46 | }) | ||
47 | } | ||
48 | |||
49 | // --------------------------------------------------------------------------- | ||
50 | |||
51 | export { | ||
52 | downloadYoutubeDLVideo, | ||
53 | getYoutubeDLInfo | ||
54 | } | ||
55 | |||
56 | // --------------------------------------------------------------------------- | ||
57 | |||
58 | function normalizeObject (obj: any) { | ||
59 | const newObj: any = {} | ||
60 | |||
61 | for (const key of Object.keys(obj)) { | ||
62 | // Deprecated key | ||
63 | if (key === 'resolution') continue | ||
64 | |||
65 | const value = obj[key] | ||
66 | |||
67 | if (typeof value === 'string') { | ||
68 | newObj[key] = value.normalize() | ||
69 | } else { | ||
70 | newObj[key] = value | ||
71 | } | ||
72 | } | ||
73 | |||
74 | return newObj | ||
75 | } | ||
76 | |||
77 | function buildVideoInfo (obj: any) { | ||
78 | return { | ||
79 | name: titleTruncation(obj.title), | ||
80 | description: descriptionTruncation(obj.description), | ||
81 | category: getCategory(obj.categories), | ||
82 | licence: getLicence(obj.license), | ||
83 | nsfw: isNSFW(obj), | ||
84 | tags: getTags(obj.tags), | ||
85 | thumbnailUrl: obj.thumbnail || undefined | ||
86 | } | ||
87 | } | ||
88 | |||
89 | function titleTruncation (title: string) { | ||
90 | return truncate(title, { | ||
91 | 'length': CONSTRAINTS_FIELDS.VIDEOS.NAME.max, | ||
92 | 'separator': /,? +/, | ||
93 | 'omission': ' […]' | ||
94 | }) | ||
95 | } | ||
96 | |||
97 | function descriptionTruncation (description: string) { | ||
98 | if (!description || description.length < CONSTRAINTS_FIELDS.VIDEOS.DESCRIPTION.min) return undefined | ||
99 | |||
100 | return truncate(description, { | ||
101 | 'length': CONSTRAINTS_FIELDS.VIDEOS.DESCRIPTION.max, | ||
102 | 'separator': /,? +/, | ||
103 | 'omission': ' […]' | ||
104 | }) | ||
105 | } | ||
106 | |||
107 | function isNSFW (info: any) { | ||
108 | return info.age_limit && info.age_limit >= 16 | ||
109 | } | ||
110 | |||
111 | function getTags (tags: any) { | ||
112 | if (Array.isArray(tags) === false) return [] | ||
113 | |||
114 | return tags | ||
115 | .filter(t => t.length < CONSTRAINTS_FIELDS.VIDEOS.TAG.max && t.length > CONSTRAINTS_FIELDS.VIDEOS.TAG.min) | ||
116 | .map(t => t.normalize()) | ||
117 | .slice(0, 5) | ||
118 | } | ||
119 | |||
120 | function getLicence (licence: string) { | ||
121 | if (!licence) return undefined | ||
122 | |||
123 | if (licence.indexOf('Creative Commons Attribution') !== -1) return 1 | ||
124 | |||
125 | return undefined | ||
126 | } | ||
127 | |||
128 | function getCategory (categories: string[]) { | ||
129 | if (!categories) return undefined | ||
130 | |||
131 | const categoryString = categories[0] | ||
132 | if (!categoryString || typeof categoryString !== 'string') return undefined | ||
133 | |||
134 | if (categoryString === 'News & Politics') return 11 | ||
135 | |||
136 | for (const key of Object.keys(VIDEO_CATEGORIES)) { | ||
137 | const category = VIDEO_CATEGORIES[key] | ||
138 | if (categoryString.toLowerCase() === category.toLowerCase()) return parseInt(key, 10) | ||
139 | } | ||
140 | |||
141 | return undefined | ||
142 | } | ||