diff options
author | Chocobozzz <me@florianbigard.com> | 2021-05-27 16:12:41 +0200 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2021-05-27 16:12:41 +0200 |
commit | 8f608a4cb22ab232cfab20665050764b38bac9c7 (patch) | |
tree | 6a6785aae79bf5939ad7b7a50a1bd8031268d2b4 /server/helpers | |
parent | 030ccfce59a8cb8f2fee6ea8dd363ba635c5c5c2 (diff) | |
parent | c215e627b575d2c4085ccb222f4ca8d0237b7552 (diff) | |
download | PeerTube-8f608a4cb22ab232cfab20665050764b38bac9c7.tar.gz PeerTube-8f608a4cb22ab232cfab20665050764b38bac9c7.tar.zst PeerTube-8f608a4cb22ab232cfab20665050764b38bac9c7.zip |
Merge branch 'develop' into shorter-URLs-channels-accounts
Diffstat (limited to 'server/helpers')
-rw-r--r-- | server/helpers/actor.ts | 2 | ||||
-rw-r--r-- | server/helpers/audit-logger.ts | 2 | ||||
-rw-r--r-- | server/helpers/custom-validators/activitypub/actor.ts | 39 | ||||
-rw-r--r-- | server/helpers/custom-validators/misc.ts | 7 | ||||
-rw-r--r-- | server/helpers/custom-validators/videos.ts | 9 | ||||
-rw-r--r-- | server/helpers/database-utils.ts | 22 | ||||
-rw-r--r-- | server/helpers/express-utils.ts | 30 | ||||
-rw-r--r-- | server/helpers/ffmpeg-utils.ts | 11 | ||||
-rw-r--r-- | server/helpers/ffprobe-utils.ts | 3 | ||||
-rw-r--r-- | server/helpers/markdown.ts | 8 | ||||
-rw-r--r-- | server/helpers/middlewares/accounts.ts | 2 | ||||
-rw-r--r-- | server/helpers/signup.ts | 2 | ||||
-rw-r--r-- | server/helpers/upload.ts | 21 | ||||
-rw-r--r-- | server/helpers/utils.ts | 4 | ||||
-rw-r--r-- | server/helpers/webfinger.ts | 6 | ||||
-rw-r--r-- | server/helpers/youtube-dl.ts | 553 |
16 files changed, 379 insertions, 342 deletions
diff --git a/server/helpers/actor.ts b/server/helpers/actor.ts index a60d3ed5d..5f742505b 100644 --- a/server/helpers/actor.ts +++ b/server/helpers/actor.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | 1 | ||
2 | import { ActorModel } from '../models/activitypub/actor' | 2 | import { ActorModel } from '../models/actor/actor' |
3 | import { MActorAccountChannelId, MActorFull } from '../types/models' | 3 | import { MActorAccountChannelId, MActorFull } from '../types/models' |
4 | 4 | ||
5 | type ActorFetchByUrlType = 'all' | 'association-ids' | 5 | type ActorFetchByUrlType = 'all' | 'association-ids' |
diff --git a/server/helpers/audit-logger.ts b/server/helpers/audit-logger.ts index 6aae5e821..884bd187d 100644 --- a/server/helpers/audit-logger.ts +++ b/server/helpers/audit-logger.ts | |||
@@ -7,7 +7,7 @@ import * as winston from 'winston' | |||
7 | import { AUDIT_LOG_FILENAME } from '@server/initializers/constants' | 7 | import { AUDIT_LOG_FILENAME } from '@server/initializers/constants' |
8 | import { AdminAbuse, User, VideoChannel, VideoDetails, VideoImport } from '../../shared' | 8 | import { AdminAbuse, User, VideoChannel, VideoDetails, VideoImport } from '../../shared' |
9 | import { CustomConfig } from '../../shared/models/server/custom-config.model' | 9 | import { CustomConfig } from '../../shared/models/server/custom-config.model' |
10 | import { VideoComment } from '../../shared/models/videos/video-comment.model' | 10 | import { VideoComment } from '../../shared/models/videos/comment/video-comment.model' |
11 | import { CONFIG } from '../initializers/config' | 11 | import { CONFIG } from '../initializers/config' |
12 | import { jsonLoggerFormat, labelFormatter } from './logger' | 12 | import { jsonLoggerFormat, labelFormatter } from './logger' |
13 | 13 | ||
diff --git a/server/helpers/custom-validators/activitypub/actor.ts b/server/helpers/custom-validators/activitypub/actor.ts index 877345157..675a7b663 100644 --- a/server/helpers/custom-validators/activitypub/actor.ts +++ b/server/helpers/custom-validators/activitypub/actor.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import validator from 'validator' | 1 | import validator from 'validator' |
2 | import { CONSTRAINTS_FIELDS } from '../../../initializers/constants' | 2 | import { CONSTRAINTS_FIELDS } from '../../../initializers/constants' |
3 | import { exists, isArray } from '../misc' | 3 | import { exists, isArray, isDateValid } from '../misc' |
4 | import { isActivityPubUrlValid, isBaseActivityValid, setValidAttributedTo } from './misc' | 4 | import { isActivityPubUrlValid, isBaseActivityValid, setValidAttributedTo } from './misc' |
5 | import { isHostValid } from '../servers' | 5 | import { isHostValid } from '../servers' |
6 | import { peertubeTruncate } from '@server/helpers/core-utils' | 6 | import { peertubeTruncate } from '@server/helpers/core-utils' |
@@ -47,7 +47,21 @@ function isActorPrivateKeyValid (privateKey: string) { | |||
47 | validator.isLength(privateKey, CONSTRAINTS_FIELDS.ACTORS.PRIVATE_KEY) | 47 | validator.isLength(privateKey, CONSTRAINTS_FIELDS.ACTORS.PRIVATE_KEY) |
48 | } | 48 | } |
49 | 49 | ||
50 | function isActorObjectValid (actor: any) { | 50 | function isActorFollowingCountValid (value: string) { |
51 | return exists(value) && validator.isInt('' + value, { min: 0 }) | ||
52 | } | ||
53 | |||
54 | function isActorFollowersCountValid (value: string) { | ||
55 | return exists(value) && validator.isInt('' + value, { min: 0 }) | ||
56 | } | ||
57 | |||
58 | function isActorDeleteActivityValid (activity: any) { | ||
59 | return isBaseActivityValid(activity, 'Delete') | ||
60 | } | ||
61 | |||
62 | function sanitizeAndCheckActorObject (actor: any) { | ||
63 | normalizeActor(actor) | ||
64 | |||
51 | return exists(actor) && | 65 | return exists(actor) && |
52 | isActivityPubUrlValid(actor.id) && | 66 | isActivityPubUrlValid(actor.id) && |
53 | isActorTypeValid(actor.type) && | 67 | isActorTypeValid(actor.type) && |
@@ -68,24 +82,6 @@ function isActorObjectValid (actor: any) { | |||
68 | (actor.type !== 'Group' || actor.attributedTo.length !== 0) | 82 | (actor.type !== 'Group' || actor.attributedTo.length !== 0) |
69 | } | 83 | } |
70 | 84 | ||
71 | function isActorFollowingCountValid (value: string) { | ||
72 | return exists(value) && validator.isInt('' + value, { min: 0 }) | ||
73 | } | ||
74 | |||
75 | function isActorFollowersCountValid (value: string) { | ||
76 | return exists(value) && validator.isInt('' + value, { min: 0 }) | ||
77 | } | ||
78 | |||
79 | function isActorDeleteActivityValid (activity: any) { | ||
80 | return isBaseActivityValid(activity, 'Delete') | ||
81 | } | ||
82 | |||
83 | function sanitizeAndCheckActorObject (object: any) { | ||
84 | normalizeActor(object) | ||
85 | |||
86 | return isActorObjectValid(object) | ||
87 | } | ||
88 | |||
89 | function normalizeActor (actor: any) { | 85 | function normalizeActor (actor: any) { |
90 | if (!actor) return | 86 | if (!actor) return |
91 | 87 | ||
@@ -95,6 +91,8 @@ function normalizeActor (actor: any) { | |||
95 | actor.url = actor.url.href || actor.url.url | 91 | actor.url = actor.url.href || actor.url.url |
96 | } | 92 | } |
97 | 93 | ||
94 | if (!isDateValid(actor.published)) actor.published = undefined | ||
95 | |||
98 | if (actor.summary && typeof actor.summary === 'string') { | 96 | if (actor.summary && typeof actor.summary === 'string') { |
99 | actor.summary = peertubeTruncate(actor.summary, { length: CONSTRAINTS_FIELDS.USERS.DESCRIPTION.max }) | 97 | actor.summary = peertubeTruncate(actor.summary, { length: CONSTRAINTS_FIELDS.USERS.DESCRIPTION.max }) |
100 | 98 | ||
@@ -135,7 +133,6 @@ export { | |||
135 | isActorPublicKeyValid, | 133 | isActorPublicKeyValid, |
136 | isActorPreferredUsernameValid, | 134 | isActorPreferredUsernameValid, |
137 | isActorPrivateKeyValid, | 135 | isActorPrivateKeyValid, |
138 | isActorObjectValid, | ||
139 | isActorFollowingCountValid, | 136 | isActorFollowingCountValid, |
140 | isActorFollowersCountValid, | 137 | isActorFollowersCountValid, |
141 | isActorDeleteActivityValid, | 138 | isActorDeleteActivityValid, |
diff --git a/server/helpers/custom-validators/misc.ts b/server/helpers/custom-validators/misc.ts index effdd98cb..229e9f03c 100644 --- a/server/helpers/custom-validators/misc.ts +++ b/server/helpers/custom-validators/misc.ts | |||
@@ -1,6 +1,7 @@ | |||
1 | import 'multer' | 1 | import 'multer' |
2 | import validator from 'validator' | 2 | import { UploadFilesForCheck } from 'express' |
3 | import { sep } from 'path' | 3 | import { sep } from 'path' |
4 | import validator from 'validator' | ||
4 | 5 | ||
5 | function exists (value: any) { | 6 | function exists (value: any) { |
6 | return value !== undefined && value !== null | 7 | return value !== undefined && value !== null |
@@ -13,7 +14,7 @@ function isSafePath (p: string) { | |||
13 | }) | 14 | }) |
14 | } | 15 | } |
15 | 16 | ||
16 | function isArray (value: any) { | 17 | function isArray (value: any): value is any[] { |
17 | return Array.isArray(value) | 18 | return Array.isArray(value) |
18 | } | 19 | } |
19 | 20 | ||
@@ -108,7 +109,7 @@ function isFileFieldValid ( | |||
108 | } | 109 | } |
109 | 110 | ||
110 | function isFileMimeTypeValid ( | 111 | function isFileMimeTypeValid ( |
111 | files: { [ fieldname: string ]: Express.Multer.File[] } | Express.Multer.File[], | 112 | files: UploadFilesForCheck, |
112 | mimeTypeRegex: string, | 113 | mimeTypeRegex: string, |
113 | field: string, | 114 | field: string, |
114 | optional = false | 115 | optional = false |
diff --git a/server/helpers/custom-validators/videos.ts b/server/helpers/custom-validators/videos.ts index 87966798f..b33e088eb 100644 --- a/server/helpers/custom-validators/videos.ts +++ b/server/helpers/custom-validators/videos.ts | |||
@@ -1,4 +1,6 @@ | |||
1 | import { UploadFilesForCheck } from 'express' | ||
1 | import { values } from 'lodash' | 2 | import { values } from 'lodash' |
3 | import * as magnetUtil from 'magnet-uri' | ||
2 | import validator from 'validator' | 4 | import validator from 'validator' |
3 | import { VideoFilter, VideoPrivacy, VideoRateType } from '../../../shared' | 5 | import { VideoFilter, VideoPrivacy, VideoRateType } from '../../../shared' |
4 | import { | 6 | import { |
@@ -6,13 +8,12 @@ import { | |||
6 | MIMETYPES, | 8 | MIMETYPES, |
7 | VIDEO_CATEGORIES, | 9 | VIDEO_CATEGORIES, |
8 | VIDEO_LICENCES, | 10 | VIDEO_LICENCES, |
11 | VIDEO_LIVE, | ||
9 | VIDEO_PRIVACIES, | 12 | VIDEO_PRIVACIES, |
10 | VIDEO_RATE_TYPES, | 13 | VIDEO_RATE_TYPES, |
11 | VIDEO_STATES, | 14 | VIDEO_STATES |
12 | VIDEO_LIVE | ||
13 | } from '../../initializers/constants' | 15 | } from '../../initializers/constants' |
14 | import { exists, isArray, isDateValid, isFileMimeTypeValid, isFileValid } from './misc' | 16 | import { exists, isArray, isDateValid, isFileMimeTypeValid, isFileValid } from './misc' |
15 | import * as magnetUtil from 'magnet-uri' | ||
16 | 17 | ||
17 | const VIDEOS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEOS | 18 | const VIDEOS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEOS |
18 | 19 | ||
@@ -81,7 +82,7 @@ function isVideoFileExtnameValid (value: string) { | |||
81 | return exists(value) && (value === VIDEO_LIVE.EXTENSION || MIMETYPES.VIDEO.EXT_MIMETYPE[value] !== undefined) | 82 | return exists(value) && (value === VIDEO_LIVE.EXTENSION || MIMETYPES.VIDEO.EXT_MIMETYPE[value] !== undefined) |
82 | } | 83 | } |
83 | 84 | ||
84 | function isVideoFileMimeTypeValid (files: { [ fieldname: string ]: Express.Multer.File[] } | Express.Multer.File[]) { | 85 | function isVideoFileMimeTypeValid (files: UploadFilesForCheck) { |
85 | return isFileMimeTypeValid(files, MIMETYPES.VIDEO.MIMETYPES_REGEX, 'videofile') | 86 | return isFileMimeTypeValid(files, MIMETYPES.VIDEO.MIMETYPES_REGEX, 'videofile') |
86 | } | 87 | } |
87 | 88 | ||
diff --git a/server/helpers/database-utils.ts b/server/helpers/database-utils.ts index 2b916efc2..7befa2c49 100644 --- a/server/helpers/database-utils.ts +++ b/server/helpers/database-utils.ts | |||
@@ -1,8 +1,9 @@ | |||
1 | import * as retry from 'async/retry' | 1 | import * as retry from 'async/retry' |
2 | import * as Bluebird from 'bluebird' | 2 | import * as Bluebird from 'bluebird' |
3 | import { QueryTypes, Transaction } from 'sequelize' | ||
3 | import { Model } from 'sequelize-typescript' | 4 | import { Model } from 'sequelize-typescript' |
5 | import { sequelizeTypescript } from '@server/initializers/database' | ||
4 | import { logger } from './logger' | 6 | import { logger } from './logger' |
5 | import { Transaction } from 'sequelize' | ||
6 | 7 | ||
7 | function retryTransactionWrapper <T, A, B, C, D> ( | 8 | function retryTransactionWrapper <T, A, B, C, D> ( |
8 | functionToRetry: (arg1: A, arg2: B, arg3: C, arg4: D) => Promise<T> | Bluebird<T>, | 9 | functionToRetry: (arg1: A, arg2: B, arg3: C, arg4: D) => Promise<T> | Bluebird<T>, |
@@ -67,7 +68,7 @@ function transactionRetryer <T> (func: (err: any, data: T) => any) { | |||
67 | }) | 68 | }) |
68 | } | 69 | } |
69 | 70 | ||
70 | function updateInstanceWithAnother <T extends Model<T>> (instanceToUpdate: Model<T>, baseInstance: Model<T>) { | 71 | function updateInstanceWithAnother <M, T extends U, U extends Model<M>> (instanceToUpdate: T, baseInstance: U) { |
71 | const obj = baseInstance.toJSON() | 72 | const obj = baseInstance.toJSON() |
72 | 73 | ||
73 | for (const key of Object.keys(obj)) { | 74 | for (const key of Object.keys(obj)) { |
@@ -87,7 +88,7 @@ function afterCommitIfTransaction (t: Transaction, fn: Function) { | |||
87 | return fn() | 88 | return fn() |
88 | } | 89 | } |
89 | 90 | ||
90 | function deleteNonExistingModels <T extends { hasSameUniqueKeysThan (other: T): boolean } & Model<T>> ( | 91 | function deleteNonExistingModels <T extends { hasSameUniqueKeysThan (other: T): boolean } & Pick<Model, 'destroy'>> ( |
91 | fromDatabase: T[], | 92 | fromDatabase: T[], |
92 | newModels: T[], | 93 | newModels: T[], |
93 | t: Transaction | 94 | t: Transaction |
@@ -96,6 +97,18 @@ function deleteNonExistingModels <T extends { hasSameUniqueKeysThan (other: T): | |||
96 | .map(f => f.destroy({ transaction: t })) | 97 | .map(f => f.destroy({ transaction: t })) |
97 | } | 98 | } |
98 | 99 | ||
100 | // Sequelize always skip the update if we only update updatedAt field | ||
101 | function setAsUpdated (table: string, id: number, transaction?: Transaction) { | ||
102 | return sequelizeTypescript.query( | ||
103 | `UPDATE "${table}" SET "updatedAt" = :updatedAt WHERE id = :id`, | ||
104 | { | ||
105 | replacements: { table, id, updatedAt: new Date() }, | ||
106 | type: QueryTypes.UPDATE, | ||
107 | transaction | ||
108 | } | ||
109 | ) | ||
110 | } | ||
111 | |||
99 | // --------------------------------------------------------------------------- | 112 | // --------------------------------------------------------------------------- |
100 | 113 | ||
101 | export { | 114 | export { |
@@ -104,5 +117,6 @@ export { | |||
104 | transactionRetryer, | 117 | transactionRetryer, |
105 | updateInstanceWithAnother, | 118 | updateInstanceWithAnother, |
106 | afterCommitIfTransaction, | 119 | afterCommitIfTransaction, |
107 | deleteNonExistingModels | 120 | deleteNonExistingModels, |
121 | setAsUpdated | ||
108 | } | 122 | } |
diff --git a/server/helpers/express-utils.ts b/server/helpers/express-utils.ts index c0d3f8f32..010c6961a 100644 --- a/server/helpers/express-utils.ts +++ b/server/helpers/express-utils.ts | |||
@@ -1,13 +1,13 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import * as multer from 'multer' | 2 | import * as multer from 'multer' |
3 | import { REMOTE_SCHEME } from '../initializers/constants' | ||
4 | import { logger } from './logger' | ||
5 | import { deleteFileAsync, generateRandomString } from './utils' | ||
6 | import { extname } from 'path' | 3 | import { extname } from 'path' |
7 | import { isArray } from './custom-validators/misc' | 4 | import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes' |
8 | import { CONFIG } from '../initializers/config' | 5 | import { CONFIG } from '../initializers/config' |
6 | import { REMOTE_SCHEME } from '../initializers/constants' | ||
7 | import { isArray } from './custom-validators/misc' | ||
8 | import { logger } from './logger' | ||
9 | import { deleteFileAndCatch, generateRandomString } from './utils' | ||
9 | import { getExtFromMimetype } from './video' | 10 | import { getExtFromMimetype } from './video' |
10 | import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes' | ||
11 | 11 | ||
12 | function buildNSFWFilter (res?: express.Response, paramNSFW?: string) { | 12 | function buildNSFWFilter (res?: express.Response, paramNSFW?: string) { |
13 | if (paramNSFW === 'true') return true | 13 | if (paramNSFW === 'true') return true |
@@ -30,21 +30,21 @@ function buildNSFWFilter (res?: express.Response, paramNSFW?: string) { | |||
30 | return null | 30 | return null |
31 | } | 31 | } |
32 | 32 | ||
33 | function cleanUpReqFiles (req: { files: { [fieldname: string]: Express.Multer.File[] } | Express.Multer.File[] }) { | 33 | function cleanUpReqFiles ( |
34 | const files = req.files | 34 | req: { files: { [fieldname: string]: Express.Multer.File[] } | Express.Multer.File[] } |
35 | 35 | ) { | |
36 | if (!files) return | 36 | const filesObject = req.files |
37 | if (!filesObject) return | ||
37 | 38 | ||
38 | if (isArray(files)) { | 39 | if (isArray(filesObject)) { |
39 | (files as Express.Multer.File[]).forEach(f => deleteFileAsync(f.path)) | 40 | filesObject.forEach(f => deleteFileAndCatch(f.path)) |
40 | return | 41 | return |
41 | } | 42 | } |
42 | 43 | ||
43 | for (const key of Object.keys(files)) { | 44 | for (const key of Object.keys(filesObject)) { |
44 | const file = files[key] | 45 | const files = filesObject[key] |
45 | 46 | ||
46 | if (isArray(file)) file.forEach(f => deleteFileAsync(f.path)) | 47 | files.forEach(f => deleteFileAndCatch(f.path)) |
47 | else deleteFileAsync(file.path) | ||
48 | } | 48 | } |
49 | } | 49 | } |
50 | 50 | ||
diff --git a/server/helpers/ffmpeg-utils.ts b/server/helpers/ffmpeg-utils.ts index 75297df8f..e328c49ac 100644 --- a/server/helpers/ffmpeg-utils.ts +++ b/server/helpers/ffmpeg-utils.ts | |||
@@ -236,7 +236,6 @@ async function getLiveTranscodingCommand (options: { | |||
236 | } | 236 | } |
237 | ] | 237 | ] |
238 | 238 | ||
239 | command.outputOption('-preset superfast') | ||
240 | command.outputOption('-sc_threshold 0') | 239 | command.outputOption('-sc_threshold 0') |
241 | 240 | ||
242 | addDefaultEncoderGlobalParams({ command }) | 241 | addDefaultEncoderGlobalParams({ command }) |
@@ -679,10 +678,16 @@ function getFFmpegVersion () { | |||
679 | 678 | ||
680 | return execPromise(`${ffmpegPath} -version`) | 679 | return execPromise(`${ffmpegPath} -version`) |
681 | .then(stdout => { | 680 | .then(stdout => { |
682 | const parsed = stdout.match(/ffmpeg version .?(\d+\.\d+\.\d+)/) | 681 | const parsed = stdout.match(/ffmpeg version .?(\d+\.\d+(\.\d+)?)/) |
683 | if (!parsed || !parsed[1]) return rej(new Error(`Could not find ffmpeg version in ${stdout}`)) | 682 | if (!parsed || !parsed[1]) return rej(new Error(`Could not find ffmpeg version in ${stdout}`)) |
684 | 683 | ||
685 | return res(parsed[1]) | 684 | // Fix ffmpeg version that does not include patch version (4.4 for example) |
685 | let version = parsed[1] | ||
686 | if (version.match(/^\d+\.\d+$/)) { | ||
687 | version += '.0' | ||
688 | } | ||
689 | |||
690 | return res(version) | ||
686 | }) | 691 | }) |
687 | .catch(err => rej(err)) | 692 | .catch(err => rej(err)) |
688 | }) | 693 | }) |
diff --git a/server/helpers/ffprobe-utils.ts b/server/helpers/ffprobe-utils.ts index 40eaafd57..ef2aa3f89 100644 --- a/server/helpers/ffprobe-utils.ts +++ b/server/helpers/ffprobe-utils.ts | |||
@@ -1,6 +1,5 @@ | |||
1 | import * as ffmpeg from 'fluent-ffmpeg' | 1 | import * as ffmpeg from 'fluent-ffmpeg' |
2 | import { VideoFileMetadata } from '@shared/models/videos/video-file-metadata' | 2 | import { getMaxBitrate, VideoFileMetadata, VideoResolution } from '../../shared/models/videos' |
3 | import { getMaxBitrate, VideoResolution } from '../../shared/models/videos' | ||
4 | import { CONFIG } from '../initializers/config' | 3 | import { CONFIG } from '../initializers/config' |
5 | import { VIDEO_TRANSCODING_FPS } from '../initializers/constants' | 4 | import { VIDEO_TRANSCODING_FPS } from '../initializers/constants' |
6 | import { logger } from './logger' | 5 | import { logger } from './logger' |
diff --git a/server/helpers/markdown.ts b/server/helpers/markdown.ts index 2126bb752..41e57d857 100644 --- a/server/helpers/markdown.ts +++ b/server/helpers/markdown.ts | |||
@@ -1,4 +1,6 @@ | |||
1 | import { SANITIZE_OPTIONS, TEXT_WITH_HTML_RULES } from '@shared/core-utils' | 1 | import { getSanitizeOptions, TEXT_WITH_HTML_RULES } from '@shared/core-utils' |
2 | |||
3 | const sanitizeOptions = getSanitizeOptions() | ||
2 | 4 | ||
3 | const sanitizeHtml = require('sanitize-html') | 5 | const sanitizeHtml = require('sanitize-html') |
4 | const markdownItEmoji = require('markdown-it-emoji/light') | 6 | const markdownItEmoji = require('markdown-it-emoji/light') |
@@ -18,7 +20,7 @@ const toSafeHtml = text => { | |||
18 | const html = markdownIt.render(textWithLineFeed) | 20 | const html = markdownIt.render(textWithLineFeed) |
19 | 21 | ||
20 | // Convert to safe Html | 22 | // Convert to safe Html |
21 | return sanitizeHtml(html, SANITIZE_OPTIONS) | 23 | return sanitizeHtml(html, sanitizeOptions) |
22 | } | 24 | } |
23 | 25 | ||
24 | const mdToPlainText = text => { | 26 | const mdToPlainText = text => { |
@@ -28,7 +30,7 @@ const mdToPlainText = text => { | |||
28 | const html = markdownIt.render(text) | 30 | const html = markdownIt.render(text) |
29 | 31 | ||
30 | // Convert to safe Html | 32 | // Convert to safe Html |
31 | const safeHtml = sanitizeHtml(html, SANITIZE_OPTIONS) | 33 | const safeHtml = sanitizeHtml(html, sanitizeOptions) |
32 | 34 | ||
33 | return safeHtml.replace(/<[^>]+>/g, '') | 35 | return safeHtml.replace(/<[^>]+>/g, '') |
34 | .replace(/\n$/, '') | 36 | .replace(/\n$/, '') |
diff --git a/server/helpers/middlewares/accounts.ts b/server/helpers/middlewares/accounts.ts index 13ae6cdf4..5addd3e1a 100644 --- a/server/helpers/middlewares/accounts.ts +++ b/server/helpers/middlewares/accounts.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import { Response } from 'express' | 1 | import { Response } from 'express' |
2 | import { UserModel } from '@server/models/account/user' | 2 | import { UserModel } from '@server/models/user/user' |
3 | import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' | 3 | import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' |
4 | import { AccountModel } from '../../models/account/account' | 4 | import { AccountModel } from '../../models/account/account' |
5 | import { MAccountDefault } from '../../types/models' | 5 | import { MAccountDefault } from '../../types/models' |
diff --git a/server/helpers/signup.ts b/server/helpers/signup.ts index ed872539b..8fa81e601 100644 --- a/server/helpers/signup.ts +++ b/server/helpers/signup.ts | |||
@@ -1,4 +1,4 @@ | |||
1 | import { UserModel } from '../models/account/user' | 1 | import { UserModel } from '../models/user/user' |
2 | import * as ipaddr from 'ipaddr.js' | 2 | import * as ipaddr from 'ipaddr.js' |
3 | import { CONFIG } from '../initializers/config' | 3 | import { CONFIG } from '../initializers/config' |
4 | 4 | ||
diff --git a/server/helpers/upload.ts b/server/helpers/upload.ts new file mode 100644 index 000000000..030a6b7d5 --- /dev/null +++ b/server/helpers/upload.ts | |||
@@ -0,0 +1,21 @@ | |||
1 | import { METAFILE_EXTNAME } from '@uploadx/core' | ||
2 | import { remove } from 'fs-extra' | ||
3 | import { join } from 'path' | ||
4 | import { RESUMABLE_UPLOAD_DIRECTORY } from '../initializers/constants' | ||
5 | |||
6 | function getResumableUploadPath (filename?: string) { | ||
7 | if (filename) return join(RESUMABLE_UPLOAD_DIRECTORY, filename) | ||
8 | |||
9 | return RESUMABLE_UPLOAD_DIRECTORY | ||
10 | } | ||
11 | |||
12 | function deleteResumableUploadMetaFile (filepath: string) { | ||
13 | return remove(filepath + METAFILE_EXTNAME) | ||
14 | } | ||
15 | |||
16 | // --------------------------------------------------------------------------- | ||
17 | |||
18 | export { | ||
19 | getResumableUploadPath, | ||
20 | deleteResumableUploadMetaFile | ||
21 | } | ||
diff --git a/server/helpers/utils.ts b/server/helpers/utils.ts index 0545e8996..6c95a43b6 100644 --- a/server/helpers/utils.ts +++ b/server/helpers/utils.ts | |||
@@ -6,7 +6,7 @@ import { CONFIG } from '../initializers/config' | |||
6 | import { execPromise, execPromise2, randomBytesPromise, sha256 } from './core-utils' | 6 | import { execPromise, execPromise2, randomBytesPromise, sha256 } from './core-utils' |
7 | import { logger } from './logger' | 7 | import { logger } from './logger' |
8 | 8 | ||
9 | function deleteFileAsync (path: string) { | 9 | function deleteFileAndCatch (path: string) { |
10 | remove(path) | 10 | remove(path) |
11 | .catch(err => logger.error('Cannot delete the file %s asynchronously.', path, { err })) | 11 | .catch(err => logger.error('Cannot delete the file %s asynchronously.', path, { err })) |
12 | } | 12 | } |
@@ -83,7 +83,7 @@ function getUUIDFromFilename (filename: string) { | |||
83 | // --------------------------------------------------------------------------- | 83 | // --------------------------------------------------------------------------- |
84 | 84 | ||
85 | export { | 85 | export { |
86 | deleteFileAsync, | 86 | deleteFileAndCatch, |
87 | generateRandomString, | 87 | generateRandomString, |
88 | getFormattedObjects, | 88 | getFormattedObjects, |
89 | getSecureTorrentName, | 89 | getSecureTorrentName, |
diff --git a/server/helpers/webfinger.ts b/server/helpers/webfinger.ts index da7e88077..33367f651 100644 --- a/server/helpers/webfinger.ts +++ b/server/helpers/webfinger.ts | |||
@@ -1,10 +1,10 @@ | |||
1 | import * as WebFinger from 'webfinger.js' | 1 | import * as WebFinger from 'webfinger.js' |
2 | import { WebFingerData } from '../../shared' | 2 | import { WebFingerData } from '../../shared' |
3 | import { ActorModel } from '../models/activitypub/actor' | ||
4 | import { isTestInstance } from './core-utils' | ||
5 | import { isActivityPubUrlValid } from './custom-validators/activitypub/misc' | ||
6 | import { WEBSERVER } from '../initializers/constants' | 3 | import { WEBSERVER } from '../initializers/constants' |
4 | import { ActorModel } from '../models/actor/actor' | ||
7 | import { MActorFull } from '../types/models' | 5 | import { MActorFull } from '../types/models' |
6 | import { isTestInstance } from './core-utils' | ||
7 | import { isActivityPubUrlValid } from './custom-validators/activitypub/misc' | ||
8 | 8 | ||
9 | const webfinger = new WebFinger({ | 9 | const webfinger = new WebFinger({ |
10 | webfist_fallback: false, | 10 | webfist_fallback: false, |
diff --git a/server/helpers/youtube-dl.ts b/server/helpers/youtube-dl.ts index fac3da6ba..d003ea3cf 100644 --- a/server/helpers/youtube-dl.ts +++ b/server/helpers/youtube-dl.ts | |||
@@ -6,7 +6,6 @@ import { CONFIG } from '@server/initializers/config' | |||
6 | import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes' | 6 | import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes' |
7 | import { VideoResolution } from '../../shared/models/videos' | 7 | import { VideoResolution } from '../../shared/models/videos' |
8 | import { CONSTRAINTS_FIELDS, VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES } from '../initializers/constants' | 8 | import { CONSTRAINTS_FIELDS, VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES } from '../initializers/constants' |
9 | import { getEnabledResolutions } from '../lib/video-transcoding' | ||
10 | import { peertubeTruncate, pipelinePromise, root } from './core-utils' | 9 | import { peertubeTruncate, pipelinePromise, root } from './core-utils' |
11 | import { isVideoFileExtnameValid } from './custom-validators/videos' | 10 | import { isVideoFileExtnameValid } from './custom-validators/videos' |
12 | import { logger } from './logger' | 11 | import { logger } from './logger' |
@@ -35,361 +34,359 @@ const processOptions = { | |||
35 | maxBuffer: 1024 * 1024 * 10 // 10MB | 34 | maxBuffer: 1024 * 1024 * 10 // 10MB |
36 | } | 35 | } |
37 | 36 | ||
38 | function getYoutubeDLInfo (url: string, opts?: string[]): Promise<YoutubeDLInfo> { | 37 | class YoutubeDL { |
39 | return new Promise<YoutubeDLInfo>((res, rej) => { | ||
40 | let args = opts || [ '-j', '--flat-playlist' ] | ||
41 | 38 | ||
42 | if (CONFIG.IMPORT.VIDEOS.HTTP.FORCE_IPV4) { | 39 | constructor (private readonly url: string = '', private readonly enabledResolutions: number[] = []) { |
43 | args.push('--force-ipv4') | ||
44 | } | ||
45 | 40 | ||
46 | args = wrapWithProxyOptions(args) | 41 | } |
47 | args = [ '-f', getYoutubeDLVideoFormat() ].concat(args) | ||
48 | 42 | ||
49 | safeGetYoutubeDL() | 43 | getYoutubeDLInfo (opts?: string[]): Promise<YoutubeDLInfo> { |
50 | .then(youtubeDL => { | 44 | return new Promise<YoutubeDLInfo>((res, rej) => { |
51 | youtubeDL.getInfo(url, args, processOptions, (err, info) => { | 45 | let args = opts || [ '-j', '--flat-playlist' ] |
52 | if (err) return rej(err) | ||
53 | if (info.is_live === true) return rej(new Error('Cannot download a live streaming.')) | ||
54 | 46 | ||
55 | const obj = buildVideoInfo(normalizeObject(info)) | 47 | if (CONFIG.IMPORT.VIDEOS.HTTP.FORCE_IPV4) { |
56 | if (obj.name && obj.name.length < CONSTRAINTS_FIELDS.VIDEOS.NAME.min) obj.name += ' video' | 48 | args.push('--force-ipv4') |
49 | } | ||
57 | 50 | ||
58 | return res(obj) | 51 | args = this.wrapWithProxyOptions(args) |
59 | }) | 52 | args = [ '-f', this.getYoutubeDLVideoFormat() ].concat(args) |
60 | }) | ||
61 | .catch(err => rej(err)) | ||
62 | }) | ||
63 | } | ||
64 | 53 | ||
65 | function getYoutubeDLSubs (url: string, opts?: object): Promise<YoutubeDLSubs> { | 54 | YoutubeDL.safeGetYoutubeDL() |
66 | return new Promise<YoutubeDLSubs>((res, rej) => { | 55 | .then(youtubeDL => { |
67 | const cwd = CONFIG.STORAGE.TMP_DIR | 56 | youtubeDL.getInfo(this.url, args, processOptions, (err, info) => { |
68 | const options = opts || { all: true, format: 'vtt', cwd } | 57 | if (err) return rej(err) |
69 | 58 | if (info.is_live === true) return rej(new Error('Cannot download a live streaming.')) | |
70 | safeGetYoutubeDL() | ||
71 | .then(youtubeDL => { | ||
72 | youtubeDL.getSubs(url, options, (err, files) => { | ||
73 | if (err) return rej(err) | ||
74 | if (!files) return [] | ||
75 | |||
76 | logger.debug('Get subtitles from youtube dl.', { url, files }) | ||
77 | |||
78 | const subtitles = files.reduce((acc, filename) => { | ||
79 | const matched = filename.match(/\.([a-z]{2})(-[a-z]+)?\.(vtt|ttml)/i) | ||
80 | if (!matched || !matched[1]) return acc | ||
81 | |||
82 | return [ | ||
83 | ...acc, | ||
84 | { | ||
85 | language: matched[1], | ||
86 | path: join(cwd, filename), | ||
87 | filename | ||
88 | } | ||
89 | ] | ||
90 | }, []) | ||
91 | 59 | ||
92 | return res(subtitles) | 60 | const obj = this.buildVideoInfo(this.normalizeObject(info)) |
61 | if (obj.name && obj.name.length < CONSTRAINTS_FIELDS.VIDEOS.NAME.min) obj.name += ' video' | ||
62 | |||
63 | return res(obj) | ||
64 | }) | ||
93 | }) | 65 | }) |
94 | }) | 66 | .catch(err => rej(err)) |
95 | .catch(err => rej(err)) | 67 | }) |
96 | }) | 68 | } |
97 | } | ||
98 | 69 | ||
99 | function getYoutubeDLVideoFormat () { | 70 | getYoutubeDLSubs (opts?: object): Promise<YoutubeDLSubs> { |
100 | /** | 71 | return new Promise<YoutubeDLSubs>((res, rej) => { |
101 | * list of format selectors in order or preference | 72 | const cwd = CONFIG.STORAGE.TMP_DIR |
102 | * see https://github.com/ytdl-org/youtube-dl#format-selection | 73 | const options = opts || { all: true, format: 'vtt', cwd } |
103 | * | 74 | |
104 | * case #1 asks for a mp4 using h264 (avc1) and the exact resolution in the hope | 75 | YoutubeDL.safeGetYoutubeDL() |
105 | * of being able to do a "quick-transcode" | 76 | .then(youtubeDL => { |
106 | * case #2 is the first fallback. No "quick-transcode" means we can get anything else (like vp9) | 77 | youtubeDL.getSubs(this.url, options, (err, files) => { |
107 | * case #3 is the resolution-degraded equivalent of #1, and already a pretty safe fallback | 78 | if (err) return rej(err) |
108 | * | 79 | if (!files) return [] |
109 | * in any case we avoid AV1, see https://github.com/Chocobozzz/PeerTube/issues/3499 | 80 | |
110 | **/ | 81 | logger.debug('Get subtitles from youtube dl.', { url: this.url, files }) |
111 | const enabledResolutions = getEnabledResolutions('vod') | 82 | |
112 | const resolution = enabledResolutions.length === 0 | 83 | const subtitles = files.reduce((acc, filename) => { |
113 | ? VideoResolution.H_720P | 84 | const matched = filename.match(/\.([a-z]{2})(-[a-z]+)?\.(vtt|ttml)/i) |
114 | : Math.max(...enabledResolutions) | 85 | if (!matched || !matched[1]) return acc |
115 | 86 | ||
116 | return [ | 87 | return [ |
117 | `bestvideo[vcodec^=avc1][height=${resolution}]+bestaudio[ext=m4a]`, // case #1 | 88 | ...acc, |
118 | `bestvideo[vcodec!*=av01][vcodec!*=vp9.2][height=${resolution}]+bestaudio`, // case #2 | 89 | { |
119 | `bestvideo[vcodec^=avc1][height<=${resolution}]+bestaudio[ext=m4a]`, // case #3 | 90 | language: matched[1], |
120 | `bestvideo[vcodec!*=av01][vcodec!*=vp9.2]+bestaudio`, | 91 | path: join(cwd, filename), |
121 | 'best[vcodec!*=av01][vcodec!*=vp9.2]', // case fallback for known formats | 92 | filename |
122 | 'best' // Ultimate fallback | 93 | } |
123 | ].join('/') | 94 | ] |
124 | } | 95 | }, []) |
96 | |||
97 | return res(subtitles) | ||
98 | }) | ||
99 | }) | ||
100 | .catch(err => rej(err)) | ||
101 | }) | ||
102 | } | ||
125 | 103 | ||
126 | function downloadYoutubeDLVideo (url: string, fileExt: string, timeout: number) { | 104 | getYoutubeDLVideoFormat () { |
127 | // Leave empty the extension, youtube-dl will add it | 105 | /** |
128 | const pathWithoutExtension = generateVideoImportTmpPath(url, '') | 106 | * list of format selectors in order or preference |
107 | * see https://github.com/ytdl-org/youtube-dl#format-selection | ||
108 | * | ||
109 | * case #1 asks for a mp4 using h264 (avc1) and the exact resolution in the hope | ||
110 | * of being able to do a "quick-transcode" | ||
111 | * case #2 is the first fallback. No "quick-transcode" means we can get anything else (like vp9) | ||
112 | * case #3 is the resolution-degraded equivalent of #1, and already a pretty safe fallback | ||
113 | * | ||
114 | * in any case we avoid AV1, see https://github.com/Chocobozzz/PeerTube/issues/3499 | ||
115 | **/ | ||
116 | const resolution = this.enabledResolutions.length === 0 | ||
117 | ? VideoResolution.H_720P | ||
118 | : Math.max(...this.enabledResolutions) | ||
119 | |||
120 | return [ | ||
121 | `bestvideo[vcodec^=avc1][height=${resolution}]+bestaudio[ext=m4a]`, // case #1 | ||
122 | `bestvideo[vcodec!*=av01][vcodec!*=vp9.2][height=${resolution}]+bestaudio`, // case #2 | ||
123 | `bestvideo[vcodec^=avc1][height<=${resolution}]+bestaudio[ext=m4a]`, // case #3 | ||
124 | `bestvideo[vcodec!*=av01][vcodec!*=vp9.2]+bestaudio`, | ||
125 | 'best[vcodec!*=av01][vcodec!*=vp9.2]', // case fallback for known formats | ||
126 | 'best' // Ultimate fallback | ||
127 | ].join('/') | ||
128 | } | ||
129 | 129 | ||
130 | let timer | 130 | downloadYoutubeDLVideo (fileExt: string, timeout: number) { |
131 | // Leave empty the extension, youtube-dl will add it | ||
132 | const pathWithoutExtension = generateVideoImportTmpPath(this.url, '') | ||
131 | 133 | ||
132 | logger.info('Importing youtubeDL video %s to %s', url, pathWithoutExtension) | 134 | let timer |
133 | 135 | ||
134 | let options = [ '-f', getYoutubeDLVideoFormat(), '-o', pathWithoutExtension ] | 136 | logger.info('Importing youtubeDL video %s to %s', this.url, pathWithoutExtension) |
135 | options = wrapWithProxyOptions(options) | ||
136 | 137 | ||
137 | if (process.env.FFMPEG_PATH) { | 138 | let options = [ '-f', this.getYoutubeDLVideoFormat(), '-o', pathWithoutExtension ] |
138 | options = options.concat([ '--ffmpeg-location', process.env.FFMPEG_PATH ]) | 139 | options = this.wrapWithProxyOptions(options) |
139 | } | ||
140 | 140 | ||
141 | logger.debug('YoutubeDL options for %s.', url, { options }) | 141 | if (process.env.FFMPEG_PATH) { |
142 | options = options.concat([ '--ffmpeg-location', process.env.FFMPEG_PATH ]) | ||
143 | } | ||
142 | 144 | ||
143 | return new Promise<string>((res, rej) => { | 145 | logger.debug('YoutubeDL options for %s.', this.url, { options }) |
144 | safeGetYoutubeDL() | ||
145 | .then(youtubeDL => { | ||
146 | youtubeDL.exec(url, options, processOptions, async err => { | ||
147 | clearTimeout(timer) | ||
148 | 146 | ||
149 | try { | 147 | return new Promise<string>((res, rej) => { |
150 | // If youtube-dl did not guess an extension for our file, just use .mp4 as default | 148 | YoutubeDL.safeGetYoutubeDL() |
151 | if (await pathExists(pathWithoutExtension)) { | 149 | .then(youtubeDL => { |
152 | await move(pathWithoutExtension, pathWithoutExtension + '.mp4') | 150 | youtubeDL.exec(this.url, options, processOptions, async err => { |
153 | } | 151 | clearTimeout(timer) |
152 | |||
153 | try { | ||
154 | // If youtube-dl did not guess an extension for our file, just use .mp4 as default | ||
155 | if (await pathExists(pathWithoutExtension)) { | ||
156 | await move(pathWithoutExtension, pathWithoutExtension + '.mp4') | ||
157 | } | ||
154 | 158 | ||
155 | const path = await guessVideoPathWithExtension(pathWithoutExtension, fileExt) | 159 | const path = await this.guessVideoPathWithExtension(pathWithoutExtension, fileExt) |
156 | 160 | ||
157 | if (err) { | 161 | if (err) { |
158 | remove(path) | 162 | remove(path) |
159 | .catch(err => logger.error('Cannot delete path on YoutubeDL error.', { err })) | 163 | .catch(err => logger.error('Cannot delete path on YoutubeDL error.', { err })) |
160 | 164 | ||
165 | return rej(err) | ||
166 | } | ||
167 | |||
168 | return res(path) | ||
169 | } catch (err) { | ||
161 | return rej(err) | 170 | return rej(err) |
162 | } | 171 | } |
163 | 172 | }) | |
164 | return res(path) | 173 | |
165 | } catch (err) { | 174 | timer = setTimeout(() => { |
166 | return rej(err) | 175 | const err = new Error('YoutubeDL download timeout.') |
167 | } | 176 | |
177 | this.guessVideoPathWithExtension(pathWithoutExtension, fileExt) | ||
178 | .then(path => remove(path)) | ||
179 | .finally(() => rej(err)) | ||
180 | .catch(err => { | ||
181 | logger.error('Cannot remove file in youtubeDL timeout.', { err }) | ||
182 | return rej(err) | ||
183 | }) | ||
184 | }, timeout) | ||
168 | }) | 185 | }) |
186 | .catch(err => rej(err)) | ||
187 | }) | ||
188 | } | ||
169 | 189 | ||
170 | timer = setTimeout(() => { | 190 | buildOriginallyPublishedAt (obj: any) { |
171 | const err = new Error('YoutubeDL download timeout.') | 191 | let originallyPublishedAt: Date = null |
172 | 192 | ||
173 | guessVideoPathWithExtension(pathWithoutExtension, fileExt) | 193 | const uploadDateMatcher = /^(\d{4})(\d{2})(\d{2})$/.exec(obj.upload_date) |
174 | .then(path => remove(path)) | 194 | if (uploadDateMatcher) { |
175 | .finally(() => rej(err)) | 195 | originallyPublishedAt = new Date() |
176 | .catch(err => { | 196 | originallyPublishedAt.setHours(0, 0, 0, 0) |
177 | logger.error('Cannot remove file in youtubeDL timeout.', { err }) | ||
178 | return rej(err) | ||
179 | }) | ||
180 | }, timeout) | ||
181 | }) | ||
182 | .catch(err => rej(err)) | ||
183 | }) | ||
184 | } | ||
185 | |||
186 | // Thanks: https://github.com/przemyslawpluta/node-youtube-dl/blob/master/lib/downloader.js | ||
187 | // We rewrote it to avoid sync calls | ||
188 | async function updateYoutubeDLBinary () { | ||
189 | logger.info('Updating youtubeDL binary.') | ||
190 | 197 | ||
191 | const binDirectory = join(root(), 'node_modules', 'youtube-dl', 'bin') | 198 | const year = parseInt(uploadDateMatcher[1], 10) |
192 | const bin = join(binDirectory, 'youtube-dl') | 199 | // Month starts from 0 |
193 | const detailsPath = join(binDirectory, 'details') | 200 | const month = parseInt(uploadDateMatcher[2], 10) - 1 |
194 | const url = process.env.YOUTUBE_DL_DOWNLOAD_HOST || 'https://yt-dl.org/downloads/latest/youtube-dl' | 201 | const day = parseInt(uploadDateMatcher[3], 10) |
195 | 202 | ||
196 | await ensureDir(binDirectory) | 203 | originallyPublishedAt.setFullYear(year, month, day) |
204 | } | ||
197 | 205 | ||
198 | try { | 206 | return originallyPublishedAt |
199 | const result = await got(url, { followRedirect: false }) | 207 | } |
200 | 208 | ||
201 | if (result.statusCode !== HttpStatusCode.FOUND_302) { | 209 | private async guessVideoPathWithExtension (tmpPath: string, sourceExt: string) { |
202 | logger.error('youtube-dl update error: did not get redirect for the latest version link. Status %d', result.statusCode) | 210 | if (!isVideoFileExtnameValid(sourceExt)) { |
203 | return | 211 | throw new Error('Invalid video extension ' + sourceExt) |
204 | } | 212 | } |
205 | 213 | ||
206 | const newUrl = result.headers.location | 214 | const extensions = [ sourceExt, '.mp4', '.mkv', '.webm' ] |
207 | const newVersion = /yt-dl\.org\/downloads\/(\d{4}\.\d\d\.\d\d(\.\d)?)\/youtube-dl/.exec(newUrl)[1] | ||
208 | 215 | ||
209 | const downloadFileStream = got.stream(newUrl) | 216 | for (const extension of extensions) { |
210 | const writeStream = createWriteStream(bin, { mode: 493 }) | 217 | const path = tmpPath + extension |
211 | 218 | ||
212 | await pipelinePromise( | 219 | if (await pathExists(path)) return path |
213 | downloadFileStream, | 220 | } |
214 | writeStream | ||
215 | ) | ||
216 | |||
217 | const details = JSON.stringify({ version: newVersion, path: bin, exec: 'youtube-dl' }) | ||
218 | await writeFile(detailsPath, details, { encoding: 'utf8' }) | ||
219 | 221 | ||
220 | logger.info('youtube-dl updated to version %s.', newVersion) | 222 | throw new Error('Cannot guess path of ' + tmpPath) |
221 | } catch (err) { | ||
222 | logger.error('Cannot update youtube-dl.', { err }) | ||
223 | } | 223 | } |
224 | } | ||
225 | 224 | ||
226 | async function safeGetYoutubeDL () { | 225 | private normalizeObject (obj: any) { |
227 | let youtubeDL | 226 | const newObj: any = {} |
228 | 227 | ||
229 | try { | 228 | for (const key of Object.keys(obj)) { |
230 | youtubeDL = require('youtube-dl') | 229 | // Deprecated key |
231 | } catch (e) { | 230 | if (key === 'resolution') continue |
232 | // Download binary | ||
233 | await updateYoutubeDLBinary() | ||
234 | youtubeDL = require('youtube-dl') | ||
235 | } | ||
236 | 231 | ||
237 | return youtubeDL | 232 | const value = obj[key] |
238 | } | ||
239 | 233 | ||
240 | function buildOriginallyPublishedAt (obj: any) { | 234 | if (typeof value === 'string') { |
241 | let originallyPublishedAt: Date = null | 235 | newObj[key] = value.normalize() |
236 | } else { | ||
237 | newObj[key] = value | ||
238 | } | ||
239 | } | ||
242 | 240 | ||
243 | const uploadDateMatcher = /^(\d{4})(\d{2})(\d{2})$/.exec(obj.upload_date) | 241 | return newObj |
244 | if (uploadDateMatcher) { | 242 | } |
245 | originallyPublishedAt = new Date() | ||
246 | originallyPublishedAt.setHours(0, 0, 0, 0) | ||
247 | 243 | ||
248 | const year = parseInt(uploadDateMatcher[1], 10) | 244 | private buildVideoInfo (obj: any): YoutubeDLInfo { |
249 | // Month starts from 0 | 245 | return { |
250 | const month = parseInt(uploadDateMatcher[2], 10) - 1 | 246 | name: this.titleTruncation(obj.title), |
251 | const day = parseInt(uploadDateMatcher[3], 10) | 247 | description: this.descriptionTruncation(obj.description), |
248 | category: this.getCategory(obj.categories), | ||
249 | licence: this.getLicence(obj.license), | ||
250 | language: this.getLanguage(obj.language), | ||
251 | nsfw: this.isNSFW(obj), | ||
252 | tags: this.getTags(obj.tags), | ||
253 | thumbnailUrl: obj.thumbnail || undefined, | ||
254 | originallyPublishedAt: this.buildOriginallyPublishedAt(obj), | ||
255 | ext: obj.ext | ||
256 | } | ||
257 | } | ||
252 | 258 | ||
253 | originallyPublishedAt.setFullYear(year, month, day) | 259 | private titleTruncation (title: string) { |
260 | return peertubeTruncate(title, { | ||
261 | length: CONSTRAINTS_FIELDS.VIDEOS.NAME.max, | ||
262 | separator: /,? +/, | ||
263 | omission: ' […]' | ||
264 | }) | ||
254 | } | 265 | } |
255 | 266 | ||
256 | return originallyPublishedAt | 267 | private descriptionTruncation (description: string) { |
257 | } | 268 | if (!description || description.length < CONSTRAINTS_FIELDS.VIDEOS.DESCRIPTION.min) return undefined |
258 | 269 | ||
259 | // --------------------------------------------------------------------------- | 270 | return peertubeTruncate(description, { |
271 | length: CONSTRAINTS_FIELDS.VIDEOS.DESCRIPTION.max, | ||
272 | separator: /,? +/, | ||
273 | omission: ' […]' | ||
274 | }) | ||
275 | } | ||
260 | 276 | ||
261 | export { | 277 | private isNSFW (info: any) { |
262 | updateYoutubeDLBinary, | 278 | return info.age_limit && info.age_limit >= 16 |
263 | getYoutubeDLVideoFormat, | 279 | } |
264 | downloadYoutubeDLVideo, | ||
265 | getYoutubeDLSubs, | ||
266 | getYoutubeDLInfo, | ||
267 | safeGetYoutubeDL, | ||
268 | buildOriginallyPublishedAt | ||
269 | } | ||
270 | 280 | ||
271 | // --------------------------------------------------------------------------- | 281 | private getTags (tags: any) { |
282 | if (Array.isArray(tags) === false) return [] | ||
272 | 283 | ||
273 | async function guessVideoPathWithExtension (tmpPath: string, sourceExt: string) { | 284 | return tags |
274 | if (!isVideoFileExtnameValid(sourceExt)) { | 285 | .filter(t => t.length < CONSTRAINTS_FIELDS.VIDEOS.TAG.max && t.length > CONSTRAINTS_FIELDS.VIDEOS.TAG.min) |
275 | throw new Error('Invalid video extension ' + sourceExt) | 286 | .map(t => t.normalize()) |
287 | .slice(0, 5) | ||
276 | } | 288 | } |
277 | 289 | ||
278 | const extensions = [ sourceExt, '.mp4', '.mkv', '.webm' ] | 290 | private getLicence (licence: string) { |
291 | if (!licence) return undefined | ||
279 | 292 | ||
280 | for (const extension of extensions) { | 293 | if (licence.includes('Creative Commons Attribution')) return 1 |
281 | const path = tmpPath + extension | ||
282 | 294 | ||
283 | if (await pathExists(path)) return path | 295 | for (const key of Object.keys(VIDEO_LICENCES)) { |
284 | } | 296 | const peertubeLicence = VIDEO_LICENCES[key] |
297 | if (peertubeLicence.toLowerCase() === licence.toLowerCase()) return parseInt(key, 10) | ||
298 | } | ||
285 | 299 | ||
286 | throw new Error('Cannot guess path of ' + tmpPath) | 300 | return undefined |
287 | } | 301 | } |
288 | 302 | ||
289 | function normalizeObject (obj: any) { | 303 | private getCategory (categories: string[]) { |
290 | const newObj: any = {} | 304 | if (!categories) return undefined |
291 | 305 | ||
292 | for (const key of Object.keys(obj)) { | 306 | const categoryString = categories[0] |
293 | // Deprecated key | 307 | if (!categoryString || typeof categoryString !== 'string') return undefined |
294 | if (key === 'resolution') continue | ||
295 | 308 | ||
296 | const value = obj[key] | 309 | if (categoryString === 'News & Politics') return 11 |
297 | 310 | ||
298 | if (typeof value === 'string') { | 311 | for (const key of Object.keys(VIDEO_CATEGORIES)) { |
299 | newObj[key] = value.normalize() | 312 | const category = VIDEO_CATEGORIES[key] |
300 | } else { | 313 | if (categoryString.toLowerCase() === category.toLowerCase()) return parseInt(key, 10) |
301 | newObj[key] = value | ||
302 | } | 314 | } |
303 | } | ||
304 | 315 | ||
305 | return newObj | 316 | return undefined |
306 | } | ||
307 | |||
308 | function buildVideoInfo (obj: any): YoutubeDLInfo { | ||
309 | return { | ||
310 | name: titleTruncation(obj.title), | ||
311 | description: descriptionTruncation(obj.description), | ||
312 | category: getCategory(obj.categories), | ||
313 | licence: getLicence(obj.license), | ||
314 | language: getLanguage(obj.language), | ||
315 | nsfw: isNSFW(obj), | ||
316 | tags: getTags(obj.tags), | ||
317 | thumbnailUrl: obj.thumbnail || undefined, | ||
318 | originallyPublishedAt: buildOriginallyPublishedAt(obj), | ||
319 | ext: obj.ext | ||
320 | } | 317 | } |
321 | } | ||
322 | 318 | ||
323 | function titleTruncation (title: string) { | 319 | private getLanguage (language: string) { |
324 | return peertubeTruncate(title, { | 320 | return VIDEO_LANGUAGES[language] ? language : undefined |
325 | length: CONSTRAINTS_FIELDS.VIDEOS.NAME.max, | 321 | } |
326 | separator: /,? +/, | ||
327 | omission: ' […]' | ||
328 | }) | ||
329 | } | ||
330 | 322 | ||
331 | function descriptionTruncation (description: string) { | 323 | private wrapWithProxyOptions (options: string[]) { |
332 | if (!description || description.length < CONSTRAINTS_FIELDS.VIDEOS.DESCRIPTION.min) return undefined | 324 | if (CONFIG.IMPORT.VIDEOS.HTTP.PROXY.ENABLED) { |
325 | logger.debug('Using proxy for YoutubeDL') | ||
333 | 326 | ||
334 | return peertubeTruncate(description, { | 327 | return [ '--proxy', CONFIG.IMPORT.VIDEOS.HTTP.PROXY.URL ].concat(options) |
335 | length: CONSTRAINTS_FIELDS.VIDEOS.DESCRIPTION.max, | 328 | } |
336 | separator: /,? +/, | ||
337 | omission: ' […]' | ||
338 | }) | ||
339 | } | ||
340 | 329 | ||
341 | function isNSFW (info: any) { | 330 | return options |
342 | return info.age_limit && info.age_limit >= 16 | 331 | } |
343 | } | ||
344 | 332 | ||
345 | function getTags (tags: any) { | 333 | // Thanks: https://github.com/przemyslawpluta/node-youtube-dl/blob/master/lib/downloader.js |
346 | if (Array.isArray(tags) === false) return [] | 334 | // We rewrote it to avoid sync calls |
335 | static async updateYoutubeDLBinary () { | ||
336 | logger.info('Updating youtubeDL binary.') | ||
347 | 337 | ||
348 | return tags | 338 | const binDirectory = join(root(), 'node_modules', 'youtube-dl', 'bin') |
349 | .filter(t => t.length < CONSTRAINTS_FIELDS.VIDEOS.TAG.max && t.length > CONSTRAINTS_FIELDS.VIDEOS.TAG.min) | 339 | const bin = join(binDirectory, 'youtube-dl') |
350 | .map(t => t.normalize()) | 340 | const detailsPath = join(binDirectory, 'details') |
351 | .slice(0, 5) | 341 | const url = process.env.YOUTUBE_DL_DOWNLOAD_HOST || 'https://yt-dl.org/downloads/latest/youtube-dl' |
352 | } | ||
353 | 342 | ||
354 | function getLicence (licence: string) { | 343 | await ensureDir(binDirectory) |
355 | if (!licence) return undefined | ||
356 | 344 | ||
357 | if (licence.includes('Creative Commons Attribution')) return 1 | 345 | try { |
346 | const result = await got(url, { followRedirect: false }) | ||
358 | 347 | ||
359 | for (const key of Object.keys(VIDEO_LICENCES)) { | 348 | if (result.statusCode !== HttpStatusCode.FOUND_302) { |
360 | const peertubeLicence = VIDEO_LICENCES[key] | 349 | logger.error('youtube-dl update error: did not get redirect for the latest version link. Status %d', result.statusCode) |
361 | if (peertubeLicence.toLowerCase() === licence.toLowerCase()) return parseInt(key, 10) | 350 | return |
362 | } | 351 | } |
363 | 352 | ||
364 | return undefined | 353 | const newUrl = result.headers.location |
365 | } | 354 | const newVersion = /yt-dl\.org\/downloads\/(\d{4}\.\d\d\.\d\d(\.\d)?)\/youtube-dl/.exec(newUrl)[1] |
366 | 355 | ||
367 | function getCategory (categories: string[]) { | 356 | const downloadFileStream = got.stream(newUrl) |
368 | if (!categories) return undefined | 357 | const writeStream = createWriteStream(bin, { mode: 493 }) |
369 | 358 | ||
370 | const categoryString = categories[0] | 359 | await pipelinePromise( |
371 | if (!categoryString || typeof categoryString !== 'string') return undefined | 360 | downloadFileStream, |
361 | writeStream | ||
362 | ) | ||
372 | 363 | ||
373 | if (categoryString === 'News & Politics') return 11 | 364 | const details = JSON.stringify({ version: newVersion, path: bin, exec: 'youtube-dl' }) |
365 | await writeFile(detailsPath, details, { encoding: 'utf8' }) | ||
374 | 366 | ||
375 | for (const key of Object.keys(VIDEO_CATEGORIES)) { | 367 | logger.info('youtube-dl updated to version %s.', newVersion) |
376 | const category = VIDEO_CATEGORIES[key] | 368 | } catch (err) { |
377 | if (categoryString.toLowerCase() === category.toLowerCase()) return parseInt(key, 10) | 369 | logger.error('Cannot update youtube-dl.', { err }) |
370 | } | ||
378 | } | 371 | } |
379 | 372 | ||
380 | return undefined | 373 | static async safeGetYoutubeDL () { |
381 | } | 374 | let youtubeDL |
382 | |||
383 | function getLanguage (language: string) { | ||
384 | return VIDEO_LANGUAGES[language] ? language : undefined | ||
385 | } | ||
386 | 375 | ||
387 | function wrapWithProxyOptions (options: string[]) { | 376 | try { |
388 | if (CONFIG.IMPORT.VIDEOS.HTTP.PROXY.ENABLED) { | 377 | youtubeDL = require('youtube-dl') |
389 | logger.debug('Using proxy for YoutubeDL') | 378 | } catch (e) { |
379 | // Download binary | ||
380 | await this.updateYoutubeDLBinary() | ||
381 | youtubeDL = require('youtube-dl') | ||
382 | } | ||
390 | 383 | ||
391 | return [ '--proxy', CONFIG.IMPORT.VIDEOS.HTTP.PROXY.URL ].concat(options) | 384 | return youtubeDL |
392 | } | 385 | } |
386 | } | ||
387 | |||
388 | // --------------------------------------------------------------------------- | ||
393 | 389 | ||
394 | return options | 390 | export { |
391 | YoutubeDL | ||
395 | } | 392 | } |