From ac81d1a06d57b9ae86663831e7f5edcef57b0fa4 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Tue, 13 Feb 2018 18:17:05 +0100 Subject: [PATCH] Add ability to set video thumbnail/preview --- .../app/shared/video/abstract-video-list.ts | 2 +- server.ts | 2 +- server/controllers/api/users.ts | 19 +-- server/controllers/api/videos/index.ts | 89 ++++++++--- server/helpers/custom-validators/misc.ts | 27 +++- server/helpers/custom-validators/users.ts | 26 ++- server/helpers/custom-validators/videos.ts | 29 ++-- server/helpers/image-utils.ts | 21 +++ server/helpers/utils.ts | 18 ++- server/initializers/constants.ts | 10 +- server/lib/activitypub/actor.ts | 6 +- server/middlewares/validators/videos.ts | 51 +++++- server/tests/api/check-params/users.ts | 8 +- server/tests/api/check-params/videos.ts | 150 +++++++++++++++--- server/tests/api/fixtures/preview.jpg | Bin 0 -> 4215 bytes server/tests/api/fixtures/thumbnail.jpg | Bin 0 -> 1457 bytes server/tests/api/videos/multiple-servers.ts | 16 +- server/tests/utils/miscs/miscs.ts | 15 +- server/tests/utils/requests/requests.ts | 19 ++- server/tests/utils/users/users.ts | 4 +- server/tests/utils/videos/videos.ts | 83 +++++++--- server/tools/import-youtube.ts | 2 - 22 files changed, 454 insertions(+), 143 deletions(-) create mode 100644 server/helpers/image-utils.ts create mode 100644 server/tests/api/fixtures/preview.jpg create mode 100644 server/tests/api/fixtures/thumbnail.jpg diff --git a/client/src/app/shared/video/abstract-video-list.ts b/client/src/app/shared/video/abstract-video-list.ts index 034d0d879..16ff38558 100644 --- a/client/src/app/shared/video/abstract-video-list.ts +++ b/client/src/app/shared/video/abstract-video-list.ts @@ -1,4 +1,4 @@ -import { ElementRef, OnInit, ViewChild, ViewChildren } from '@angular/core' +import { ElementRef, OnInit, ViewChild } from '@angular/core' import { ActivatedRoute, Router } from '@angular/router' import { isInMobileView } from '@app/shared/misc/utils' import { InfiniteScrollerDirective } from '@app/shared/video/infinite-scroller.directive' diff --git a/server.ts b/server.ts index dc7a71d60..529194a5e 100644 --- a/server.ts +++ b/server.ts @@ -158,7 +158,7 @@ app.use(function (req, res, next) { }) app.use(function (err, req, res, next) { - logger.error(err, err) + logger.error('Error in controller.', { error: err.stack || err.message || err }) res.sendStatus(err.status || 500) }) diff --git a/server/controllers/api/users.ts b/server/controllers/api/users.ts index 6e5d09695..e3067584e 100644 --- a/server/controllers/api/users.ts +++ b/server/controllers/api/users.ts @@ -1,13 +1,13 @@ import * as express from 'express' +import 'multer' import { extname, join } from 'path' -import * as sharp from 'sharp' import * as uuidv4 from 'uuid/v4' import { UserCreate, UserRight, UserRole, UserUpdate, UserUpdateMe, UserVideoRate as FormattedUserVideoRate } from '../../../shared' -import { unlinkPromise } from '../../helpers/core-utils' import { retryTransactionWrapper } from '../../helpers/database-utils' +import { processImage } from '../../helpers/image-utils' import { logger } from '../../helpers/logger' import { createReqFiles, getFormattedObjects } from '../../helpers/utils' -import { AVATAR_MIMETYPE_EXT, AVATARS_SIZE, CONFIG, sequelizeTypescript } from '../../initializers' +import { AVATARS_SIZE, CONFIG, IMAGE_MIMETYPE_EXT, sequelizeTypescript } from '../../initializers' import { updateActorAvatarInstance } from '../../lib/activitypub' import { sendUpdateUser } from '../../lib/activitypub/send' import { Emailer } from '../../lib/emailer' @@ -42,7 +42,7 @@ import { UserModel } from '../../models/account/user' import { OAuthTokenModel } from '../../models/oauth/oauth-token' import { VideoModel } from '../../models/video/video' -const reqAvatarFile = createReqFiles('avatarfile', CONFIG.STORAGE.AVATARS_DIR, AVATAR_MIMETYPE_EXT) +const reqAvatarFile = createReqFiles([ 'avatarfile' ], IMAGE_MIMETYPE_EXT, { avatarfile: CONFIG.STORAGE.AVATARS_DIR }) const usersRouter = express.Router() @@ -288,17 +288,10 @@ async function updateMyAvatar (req: express.Request, res: express.Response, next const user = res.locals.oauth.token.user const actor = user.Account.Actor - const avatarDir = CONFIG.STORAGE.AVATARS_DIR - const source = join(avatarDir, avatarPhysicalFile.filename) const extension = extname(avatarPhysicalFile.filename) const avatarName = uuidv4() + extension - const destination = join(avatarDir, avatarName) - - await sharp(source) - .resize(AVATARS_SIZE.width, AVATARS_SIZE.height) - .toFile(destination) - - await unlinkPromise(source) + const destination = join(CONFIG.STORAGE.AVATARS_DIR, avatarName) + await processImage(avatarPhysicalFile, destination, AVATARS_SIZE) const avatar = await sequelizeTypescript.transaction(async t => { const updatedActor = await updateActorAvatarInstance(actor, avatarName, t) diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts index 459795141..1a4de081f 100644 --- a/server/controllers/api/videos/index.ts +++ b/server/controllers/api/videos/index.ts @@ -4,18 +4,36 @@ import { VideoCreate, VideoPrivacy, VideoUpdate } from '../../../../shared' import { renamePromise } from '../../../helpers/core-utils' import { retryTransactionWrapper } from '../../../helpers/database-utils' import { getVideoFileHeight } from '../../../helpers/ffmpeg-utils' +import { processImage } from '../../../helpers/image-utils' import { logger } from '../../../helpers/logger' import { createReqFiles, getFormattedObjects, getServerActor, resetSequelizeInstance } from '../../../helpers/utils' import { - CONFIG, sequelizeTypescript, VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_MIMETYPE_EXT, + CONFIG, + IMAGE_MIMETYPE_EXT, + PREVIEWS_SIZE, + sequelizeTypescript, + THUMBNAILS_SIZE, + VIDEO_CATEGORIES, + VIDEO_LANGUAGES, + VIDEO_LICENCES, + VIDEO_MIMETYPE_EXT, VIDEO_PRIVACIES } from '../../../initializers' import { fetchRemoteVideoDescription, getVideoActivityPubUrl, shareVideoByServerAndChannel } from '../../../lib/activitypub' import { sendCreateVideo, sendCreateViewToOrigin, sendCreateViewToVideoFollowers, sendUpdateVideo } from '../../../lib/activitypub/send' import { JobQueue } from '../../../lib/job-queue' import { - asyncMiddleware, authenticate, paginationValidator, setDefaultSort, setDefaultPagination, videosAddValidator, videosGetValidator, - videosRemoveValidator, videosSearchValidator, videosSortValidator, videosUpdateValidator + asyncMiddleware, + authenticate, + paginationValidator, + setDefaultPagination, + setDefaultSort, + videosAddValidator, + videosGetValidator, + videosRemoveValidator, + videosSearchValidator, + videosSortValidator, + videosUpdateValidator } from '../../../middlewares' import { TagModel } from '../../../models/video/tag' import { VideoModel } from '../../../models/video/video' @@ -28,7 +46,23 @@ import { rateVideoRouter } from './rate' const videosRouter = express.Router() -const reqVideoFile = createReqFiles('videofile', CONFIG.STORAGE.VIDEOS_DIR, VIDEO_MIMETYPE_EXT) +const reqVideoFileAdd = createReqFiles( + [ 'videofile', 'thumbnailfile', 'previewfile' ], + Object.assign({}, VIDEO_MIMETYPE_EXT, IMAGE_MIMETYPE_EXT), + { + videofile: CONFIG.STORAGE.VIDEOS_DIR, + thumbnailfile: CONFIG.STORAGE.THUMBNAILS_DIR, + previewfile: CONFIG.STORAGE.PREVIEWS_DIR + } +) +const reqVideoFileUpdate = createReqFiles( + [ 'thumbnailfile', 'previewfile' ], + IMAGE_MIMETYPE_EXT, + { + thumbnailfile: CONFIG.STORAGE.THUMBNAILS_DIR, + previewfile: CONFIG.STORAGE.PREVIEWS_DIR + } +) videosRouter.use('/', abuseVideoRouter) videosRouter.use('/', blacklistRouter) @@ -58,12 +92,13 @@ videosRouter.get('/search', ) videosRouter.put('/:id', authenticate, + reqVideoFileUpdate, asyncMiddleware(videosUpdateValidator), asyncMiddleware(updateVideoRetryWrapper) ) videosRouter.post('/upload', authenticate, - reqVideoFile, + reqVideoFileAdd, asyncMiddleware(videosAddValidator), asyncMiddleware(addVideoRetryWrapper) ) @@ -150,8 +185,7 @@ async function addVideo (req: express.Request, res: express.Response, videoPhysi const video = new VideoModel(videoData) video.url = getVideoActivityPubUrl(video) - const videoFilePath = join(CONFIG.STORAGE.VIDEOS_DIR, videoPhysicalFile.filename) - const videoFileHeight = await getVideoFileHeight(videoFilePath) + const videoFileHeight = await getVideoFileHeight(videoPhysicalFile.path) const videoFileData = { extname: extname(videoPhysicalFile.filename), @@ -160,21 +194,28 @@ async function addVideo (req: express.Request, res: express.Response, videoPhysi } const videoFile = new VideoFileModel(videoFileData) const videoDir = CONFIG.STORAGE.VIDEOS_DIR - const source = join(videoDir, videoPhysicalFile.filename) const destination = join(videoDir, video.getVideoFilename(videoFile)) + await renamePromise(videoPhysicalFile.path, destination) - await renamePromise(source, destination) - // This is important in case if there is another attempt in the retry process - videoPhysicalFile.filename = video.getVideoFilename(videoFile) + // Process thumbnail or create it from the video + const thumbnailField = req.files['thumbnailfile'] + if (thumbnailField) { + const thumbnailPhysicalFile = thumbnailField[0] + await processImage(thumbnailPhysicalFile, join(CONFIG.STORAGE.THUMBNAILS_DIR, video.getThumbnailName()), THUMBNAILS_SIZE) + } else { + await video.createThumbnail(videoFile) + } - const tasks = [] + // Process preview or create it from the video + const previewField = req.files['previewfile'] + if (previewField) { + const previewPhysicalFile = previewField[0] + await processImage(previewPhysicalFile, join(CONFIG.STORAGE.PREVIEWS_DIR, video.getPreviewName()), PREVIEWS_SIZE) + } else { + await video.createPreview(videoFile) + } - tasks.push( - video.createTorrentAndSetInfoHash(videoFile), - video.createThumbnail(videoFile), - video.createPreview(videoFile) - ) - await Promise.all(tasks) + await video.createTorrentAndSetInfoHash(videoFile) const videoCreated = await sequelizeTypescript.transaction(async t => { const sequelizeOptions = { transaction: t } @@ -237,6 +278,18 @@ async function updateVideo (req: express.Request, res: express.Response) { const videoInfoToUpdate: VideoUpdate = req.body const wasPrivateVideo = videoInstance.privacy === VideoPrivacy.PRIVATE + // Process thumbnail or create it from the video + if (req.files && req.files['thumbnailfile']) { + const thumbnailPhysicalFile = req.files['thumbnailfile'][0] + await processImage(thumbnailPhysicalFile, join(CONFIG.STORAGE.THUMBNAILS_DIR, videoInstance.getThumbnailName()), THUMBNAILS_SIZE) + } + + // Process preview or create it from the video + if (req.files && req.files['previewfile']) { + const previewPhysicalFile = req.files['previewfile'][0] + await processImage(previewPhysicalFile, join(CONFIG.STORAGE.PREVIEWS_DIR, videoInstance.getPreviewName()), PREVIEWS_SIZE) + } + try { await sequelizeTypescript.transaction(async t => { const sequelizeOptions = { diff --git a/server/helpers/custom-validators/misc.ts b/server/helpers/custom-validators/misc.ts index 3903884ea..8a270b777 100644 --- a/server/helpers/custom-validators/misc.ts +++ b/server/helpers/custom-validators/misc.ts @@ -1,3 +1,4 @@ +import 'multer' import * as validator from 'validator' function exists (value: any) { @@ -28,6 +29,29 @@ function isBooleanValid (value: string) { return typeof value === 'boolean' || (typeof value === 'string' && validator.isBoolean(value)) } +function isFileValid ( + files: { [ fieldname: string ]: Express.Multer.File[] } | Express.Multer.File[], + mimeTypeRegex: string, + field: string, + optional = false +) { + // Should have files + if (!files) return optional + if (isArray(files)) return optional + + // Should have a file + const fileArray = files[ field ] + if (!fileArray || fileArray.length === 0) { + return optional + } + + // The file should exist + const file = fileArray[ 0 ] + if (!file || !file.originalname) return false + + return new RegExp(`^${mimeTypeRegex}$`, 'i').test(file.mimetype) +} + // --------------------------------------------------------------------------- export { @@ -37,5 +61,6 @@ export { isUUIDValid, isIdOrUUIDValid, isDateValid, - isBooleanValid + isBooleanValid, + isFileValid } diff --git a/server/helpers/custom-validators/users.ts b/server/helpers/custom-validators/users.ts index 6ed60c1c4..e805313f8 100644 --- a/server/helpers/custom-validators/users.ts +++ b/server/helpers/custom-validators/users.ts @@ -1,9 +1,9 @@ -import * as validator from 'validator' import 'express-validator' - -import { exists, isArray } from './misc' -import { CONSTRAINTS_FIELDS } from '../../initializers' +import * as validator from 'validator' import { UserRole } from '../../../shared' +import { CONSTRAINTS_FIELDS } from '../../initializers' + +import { exists, isFileValid } from './misc' const USERS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.USERS @@ -37,20 +37,12 @@ function isUserRoleValid (value: any) { return exists(value) && validator.isInt('' + value) && UserRole[value] !== undefined } +const avatarMimeTypes = CONSTRAINTS_FIELDS.ACTORS.AVATAR.EXTNAME + .map(v => v.replace('.', '')) + .join('|') +const avatarMimeTypesRegex = `image/(${avatarMimeTypes})` function isAvatarFile (files: { [ fieldname: string ]: Express.Multer.File[] } | Express.Multer.File[]) { - // Should have files - if (!files) return false - if (isArray(files)) return false - - // Should have videofile file - const avatarfile = files['avatarfile'] - if (!avatarfile || avatarfile.length === 0) return false - - // The file should exist - const file = avatarfile[0] - if (!file || !file.originalname) return false - - return new RegExp('^image/(png|jpeg)$', 'i').test(file.mimetype) + return isFileValid(files, avatarMimeTypesRegex, 'avatarfile') } // --------------------------------------------------------------------------- diff --git a/server/helpers/custom-validators/videos.ts b/server/helpers/custom-validators/videos.ts index 0e8a2aab2..8ef3a3c64 100644 --- a/server/helpers/custom-validators/videos.ts +++ b/server/helpers/custom-validators/videos.ts @@ -8,12 +8,12 @@ import { CONSTRAINTS_FIELDS, VIDEO_CATEGORIES, VIDEO_LANGUAGES, - VIDEO_LICENCES, + VIDEO_LICENCES, VIDEO_MIMETYPE_EXT, VIDEO_PRIVACIES, VIDEO_RATE_TYPES } from '../../initializers' import { VideoModel } from '../../models/video/video' -import { exists, isArray } from './misc' +import { exists, isArray, isFileValid } from './misc' const VIDEOS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEOS const VIDEO_ABUSES_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEO_ABUSES @@ -68,20 +68,18 @@ function isVideoRatingTypeValid (value: string) { return value === 'none' || values(VIDEO_RATE_TYPES).indexOf(value as VideoRateType) !== -1 } +const videoFileTypes = Object.keys(VIDEO_MIMETYPE_EXT).map(m => `(${m})`) +const videoFileTypesRegex = videoFileTypes.join('|') function isVideoFile (files: { [ fieldname: string ]: Express.Multer.File[] } | Express.Multer.File[]) { - // Should have files - if (!files) return false - if (isArray(files)) return false - - // Should have videofile file - const videofile = files['videofile'] - if (!videofile || videofile.length === 0) return false - - // The file should exist - const file = videofile[0] - if (!file || !file.originalname) return false + return isFileValid(files, videoFileTypesRegex, 'videofile') +} - return new RegExp('^video/(webm|mp4|ogg)$', 'i').test(file.mimetype) +const videoImageTypes = CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME + .map(v => v.replace('.', '')) + .join('|') +const videoImageTypesRegex = `image/(${videoImageTypes})` +function isVideoImage (files: { [ fieldname: string ]: Express.Multer.File[] } | Express.Multer.File[], field: string) { + return isFileValid(files, videoImageTypesRegex, field, true) } function isVideoPrivacyValid (value: string) { @@ -141,5 +139,6 @@ export { isVideoPrivacyValid, isVideoFileResolutionValid, isVideoFileSizeValid, - isVideoExist + isVideoExist, + isVideoImage } diff --git a/server/helpers/image-utils.ts b/server/helpers/image-utils.ts new file mode 100644 index 000000000..ba57b5812 --- /dev/null +++ b/server/helpers/image-utils.ts @@ -0,0 +1,21 @@ +import 'multer' +import * as sharp from 'sharp' +import { unlinkPromise } from './core-utils' + +async function processImage ( + physicalFile: Express.Multer.File, + destination: string, + newSize: { width: number, height: number } +) { + await sharp(physicalFile.path) + .resize(newSize.width, newSize.height) + .toFile(destination) + + await unlinkPromise(physicalFile.path) +} + +// --------------------------------------------------------------------------- + +export { + processImage +} diff --git a/server/helpers/utils.ts b/server/helpers/utils.ts index 79c3b5858..3b618360b 100644 --- a/server/helpers/utils.ts +++ b/server/helpers/utils.ts @@ -27,10 +27,14 @@ function badRequest (req: express.Request, res: express.Response, next: express. return res.type('json').status(400).end() } -function createReqFiles (fieldName: string, storageDir: string, mimeTypes: { [ id: string ]: string }) { +function createReqFiles ( + fieldNames: string[], + mimeTypes: { [ id: string ]: string }, + destinations: { [ fieldName: string ]: string } +) { const storage = multer.diskStorage({ destination: (req, file, cb) => { - cb(null, storageDir) + cb(null, destinations[file.fieldname]) }, filename: async (req, file, cb) => { @@ -48,7 +52,15 @@ function createReqFiles (fieldName: string, storageDir: string, mimeTypes: { [ i } }) - return multer({ storage }).fields([{ name: fieldName, maxCount: 1 }]) + const fields = [] + for (const fieldName of fieldNames) { + fields.push({ + name: fieldName, + maxCount: 1 + }) + } + + return multer({ storage }).fields(fields) } async function generateRandomString (size: number) { diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index e531c4c39..91fbbde75 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts @@ -182,6 +182,12 @@ const CONSTRAINTS_FIELDS = { NAME: { min: 3, max: 120 }, // Length TRUNCATED_DESCRIPTION: { min: 3, max: 250 }, // Length DESCRIPTION: { min: 3, max: 3000 }, // Length + IMAGE: { + EXTNAME: [ '.jpg', '.jpeg' ], + FILE_SIZE: { + max: 2 * 1024 * 1024 // 2MB + } + }, EXTNAME: [ '.mp4', '.ogv', '.webm' ], INFO_HASH: { min: 40, max: 40 }, // Length, info hash is 20 bytes length but we represent it in hexadecimal so 20 * 2 DURATION: { min: 1 }, // Number @@ -285,7 +291,7 @@ const VIDEO_MIMETYPE_EXT = { 'video/mp4': '.mp4' } -const AVATAR_MIMETYPE_EXT = { +const IMAGE_MIMETYPE_EXT = { 'image/png': '.png', 'image/jpg': '.jpg', 'image/jpeg': '.jpg' @@ -427,7 +433,7 @@ export { VIDEO_RATE_TYPES, VIDEO_MIMETYPE_EXT, USER_PASSWORD_RESET_LIFETIME, - AVATAR_MIMETYPE_EXT, + IMAGE_MIMETYPE_EXT, SCHEDULER_INTERVAL, JOB_COMPLETED_LIFETIME } diff --git a/server/lib/activitypub/actor.ts b/server/lib/activitypub/actor.ts index 712de7d0d..c3255d8ca 100644 --- a/server/lib/activitypub/actor.ts +++ b/server/lib/activitypub/actor.ts @@ -12,7 +12,7 @@ import { logger } from '../../helpers/logger' import { createPrivateAndPublicKeys } from '../../helpers/peertube-crypto' import { doRequest, doRequestAndSaveToFile } from '../../helpers/requests' import { getUrlFromWebfinger } from '../../helpers/webfinger' -import { AVATAR_MIMETYPE_EXT, CONFIG, sequelizeTypescript } from '../../initializers' +import { IMAGE_MIMETYPE_EXT, CONFIG, sequelizeTypescript } from '../../initializers' import { AccountModel } from '../../models/account/account' import { ActorModel } from '../../models/activitypub/actor' import { AvatarModel } from '../../models/avatar/avatar' @@ -147,10 +147,10 @@ async function fetchActorTotalItems (url: string) { async function fetchAvatarIfExists (actorJSON: ActivityPubActor) { if ( - actorJSON.icon && actorJSON.icon.type === 'Image' && AVATAR_MIMETYPE_EXT[actorJSON.icon.mediaType] !== undefined && + actorJSON.icon && actorJSON.icon.type === 'Image' && IMAGE_MIMETYPE_EXT[actorJSON.icon.mediaType] !== undefined && isActivityPubUrlValid(actorJSON.icon.url) ) { - const extension = AVATAR_MIMETYPE_EXT[actorJSON.icon.mediaType] + const extension = IMAGE_MIMETYPE_EXT[actorJSON.icon.mediaType] const avatarName = uuidv4() + extension const destPath = join(CONFIG.STORAGE.AVATARS_DIR, avatarName) diff --git a/server/middlewares/validators/videos.ts b/server/middlewares/validators/videos.ts index a365ed217..6d4fb907b 100644 --- a/server/middlewares/validators/videos.ts +++ b/server/middlewares/validators/videos.ts @@ -4,8 +4,18 @@ import { body, param, query } from 'express-validator/check' import { UserRight, VideoPrivacy } from '../../../shared' import { isBooleanValid, isIdOrUUIDValid, isIdValid, isUUIDValid } from '../../helpers/custom-validators/misc' import { - isVideoAbuseReasonValid, isVideoCategoryValid, isVideoDescriptionValid, isVideoExist, isVideoFile, isVideoLanguageValid, - isVideoLicenceValid, isVideoNameValid, isVideoPrivacyValid, isVideoRatingTypeValid, isVideoTagsValid + isVideoAbuseReasonValid, + isVideoCategoryValid, + isVideoDescriptionValid, + isVideoExist, + isVideoFile, + isVideoImage, + isVideoLanguageValid, + isVideoLicenceValid, + isVideoNameValid, + isVideoPrivacyValid, + isVideoRatingTypeValid, + isVideoTagsValid } from '../../helpers/custom-validators/videos' import { getDurationFromVideoFile } from '../../helpers/ffmpeg-utils' import { logger } from '../../helpers/logger' @@ -22,6 +32,14 @@ const videosAddValidator = [ 'This file is not supported. Please, make sure it is of the following type : ' + CONSTRAINTS_FIELDS.VIDEOS.EXTNAME.join(', ') ), + body('thumbnailfile').custom((value, { req }) => isVideoImage(req.files, 'thumbnailfile')).withMessage( + 'This thumbnail file is not supported. Please, make sure it is of the following type : ' + + CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ') + ), + body('previewfile').custom((value, { req }) => isVideoImage(req.files, 'previewfile')).withMessage( + 'This preview file is not supported. Please, make sure it is of the following type : ' + + CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ') + ), body('name').custom(isVideoNameValid).withMessage('Should have a valid name'), body('category').optional().custom(isVideoCategoryValid).withMessage('Should have a valid category'), body('licence').optional().custom(isVideoLicenceValid).withMessage('Should have a valid licence'), @@ -37,6 +55,7 @@ const videosAddValidator = [ logger.debug('Checking videosAdd parameters', { parameters: req.body, files: req.files }) if (areValidationErrors(req, res)) return + if (areErrorsInVideoImageFiles(req, res)) return const videoFile: Express.Multer.File = req.files['videofile'][0] const user = res.locals.oauth.token.User @@ -82,6 +101,14 @@ const videosAddValidator = [ const videosUpdateValidator = [ param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), + body('thumbnailfile').custom((value, { req }) => isVideoImage(req.files, 'thumbnailfile')).withMessage( + 'This thumbnail file is not supported. Please, make sure it is of the following type : ' + + CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ') + ), + body('previewfile').custom((value, { req }) => isVideoImage(req.files, 'previewfile')).withMessage( + 'This preview file is not supported. Please, make sure it is of the following type : ' + + CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ') + ), body('name').optional().custom(isVideoNameValid).withMessage('Should have a valid name'), body('category').optional().custom(isVideoCategoryValid).withMessage('Should have a valid category'), body('licence').optional().custom(isVideoLicenceValid).withMessage('Should have a valid licence'), @@ -96,6 +123,7 @@ const videosUpdateValidator = [ logger.debug('Checking videosUpdate parameters', { parameters: req.body }) if (areValidationErrors(req, res)) return + if (areErrorsInVideoImageFiles(req, res)) return if (!await isVideoExist(req.params.id, res)) return const video = res.locals.video @@ -274,3 +302,22 @@ function checkUserCanDeleteVideo (user: UserModel, video: VideoModel, res: expre return true } + +function areErrorsInVideoImageFiles (req: express.Request, res: express.Response) { + // Files are optional + if (!req.files) return false + + for (const imageField of [ 'thumbnail', 'preview' ]) { + if (!req.files[ imageField ]) continue + + const imageFile = req.files[ imageField ][ 0 ] as Express.Multer.File + if (imageFile.size > CONSTRAINTS_FIELDS.VIDEOS.IMAGE.FILE_SIZE.max) { + res.status(400) + .send({ error: `The size of the ${imageField} is too big (>${CONSTRAINTS_FIELDS.VIDEOS.IMAGE.FILE_SIZE.max}).` }) + .end() + return true + } + } + + return false +} diff --git a/server/tests/api/check-params/users.ts b/server/tests/api/check-params/users.ts index 0fbc414c9..d9dea0713 100644 --- a/server/tests/api/check-params/users.ts +++ b/server/tests/api/check-params/users.ts @@ -7,7 +7,7 @@ import { UserRole } from '../../../../shared' import { createUser, flushTests, getMyUserInformation, getMyUserVideoRating, getUsersList, immutableAssign, killallServers, makeGetRequest, - makePostBodyRequest, makePostUploadRequest, makePutBodyRequest, registerUser, removeUser, runServer, ServerInfo, setAccessTokensToServers, + makePostBodyRequest, makeUploadRequest, makePutBodyRequest, registerUser, removeUser, runServer, ServerInfo, setAccessTokensToServers, updateUser, uploadVideo, userLogin } from '../../utils' import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '../../utils/requests/check-api-params' @@ -273,7 +273,7 @@ describe('Test users API validators', function () { const attaches = { 'avatarfile': join(__dirname, '..', 'fixtures', 'video_short.mp4') } - await makePostUploadRequest({ url: server.url, path: path + '/me/avatar/pick', token: server.accessToken, fields, attaches }) + await makeUploadRequest({ url: server.url, path: path + '/me/avatar/pick', token: server.accessToken, fields, attaches }) }) it('Should fail with a big file', async function () { @@ -281,7 +281,7 @@ describe('Test users API validators', function () { const attaches = { 'avatarfile': join(__dirname, '..', 'fixtures', 'avatar-big.png') } - await makePostUploadRequest({ url: server.url, path: path + '/me/avatar/pick', token: server.accessToken, fields, attaches }) + await makeUploadRequest({ url: server.url, path: path + '/me/avatar/pick', token: server.accessToken, fields, attaches }) }) it('Should succeed with the correct params', async function () { @@ -289,7 +289,7 @@ describe('Test users API validators', function () { const attaches = { 'avatarfile': join(__dirname, '..', 'fixtures', 'avatar.png') } - await makePostUploadRequest({ + await makeUploadRequest({ url: server.url, path: path + '/me/avatar/pick', token: server.accessToken, diff --git a/server/tests/api/check-params/videos.ts b/server/tests/api/check-params/videos.ts index f25e3f595..aa30b721b 100644 --- a/server/tests/api/check-params/videos.ts +++ b/server/tests/api/check-params/videos.ts @@ -7,7 +7,7 @@ import { join } from 'path' import { VideoPrivacy } from '../../../../shared/models/videos/video-privacy.enum' import { createUser, flushTests, getMyUserInformation, getVideo, getVideosList, immutableAssign, killallServers, makeDeleteRequest, - makeGetRequest, makePostUploadRequest, makePutBodyRequest, removeVideo, runServer, ServerInfo, setAccessTokensToServers, userLogin + makeGetRequest, makeUploadRequest, makePutBodyRequest, removeVideo, runServer, ServerInfo, setAccessTokensToServers, userLogin } from '../../utils' import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '../../utils/requests/check-api-params' @@ -111,91 +111,91 @@ describe('Test videos API validator', function () { it('Should fail with nothing', async function () { const fields = {} const attaches = {} - await makePostUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) + await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) }) it('Should fail without name', async function () { const fields = omit(baseCorrectParams, 'name') const attaches = baseCorrectAttaches - await makePostUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) + await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) }) it('Should fail with a long name', async function () { const fields = immutableAssign(baseCorrectParams, { name: 'super'.repeat(65) }) const attaches = baseCorrectAttaches - await makePostUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) + await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) }) it('Should fail with a bad category', async function () { const fields = immutableAssign(baseCorrectParams, { category: 125 }) const attaches = baseCorrectAttaches - await makePostUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) + await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) }) it('Should fail with a bad licence', async function () { const fields = immutableAssign(baseCorrectParams, { licence: 125 }) const attaches = baseCorrectAttaches - await makePostUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) + await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) }) it('Should fail with a bad language', async function () { const fields = immutableAssign(baseCorrectParams, { language: 125 }) const attaches = baseCorrectAttaches - await makePostUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) + await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) }) it('Should fail without nsfw attribute', async function () { const fields = omit(baseCorrectParams, 'nsfw') const attaches = baseCorrectAttaches - await makePostUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) + await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) }) it('Should fail with a bad nsfw attribute', async function () { const fields = immutableAssign(baseCorrectParams, { nsfw: 2 }) const attaches = baseCorrectAttaches - await makePostUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) + await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) }) it('Should fail without commentsEnabled attribute', async function () { const fields = omit(baseCorrectParams, 'commentsEnabled') const attaches = baseCorrectAttaches - await makePostUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) + await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) }) it('Should fail with a bad commentsEnabled attribute', async function () { const fields = immutableAssign(baseCorrectParams, { commentsEnabled: 2 }) const attaches = baseCorrectAttaches - await makePostUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) + await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) }) it('Should fail with a long description', async function () { const fields = immutableAssign(baseCorrectParams, { description: 'super'.repeat(1500) }) const attaches = baseCorrectAttaches - await makePostUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) + await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) }) it('Should fail without a channel', async function () { const fields = omit(baseCorrectParams, 'channelId') const attaches = baseCorrectAttaches - await makePostUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) + await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) }) it('Should fail with a bad channel', async function () { const fields = immutableAssign(baseCorrectParams, { channelId: 545454 }) const attaches = baseCorrectAttaches - await makePostUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) + await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) }) it('Should fail with another user channel', async function () { @@ -212,34 +212,34 @@ describe('Test videos API validator', function () { const fields = immutableAssign(baseCorrectParams, { channelId: customChannelId }) const attaches = baseCorrectAttaches - await makePostUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) + await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) }) it('Should fail with too many tags', async function () { const fields = immutableAssign(baseCorrectParams, { tags: [ 'tag1', 'tag2', 'tag3', 'tag4', 'tag5', 'tag6' ] }) const attaches = baseCorrectAttaches - await makePostUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) + await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) }) it('Should fail with a tag length too low', async function () { const fields = immutableAssign(baseCorrectParams, { tags: [ 'tag1', 't' ] }) const attaches = baseCorrectAttaches - await makePostUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) + await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) }) it('Should fail with a tag length too big', async function () { const fields = immutableAssign(baseCorrectParams, { tags: [ 'tag1', 'my_super_tag_too_long_long_long_long_long_long' ] }) const attaches = baseCorrectAttaches - await makePostUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) + await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) }) it('Should fail without an input file', async function () { const fields = baseCorrectParams const attaches = {} - await makePostUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) + await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) }) it('Should fail without an incorrect input file', async function () { @@ -247,7 +247,47 @@ describe('Test videos API validator', function () { const attaches = { 'videofile': join(__dirname, '..', 'fixtures', 'video_short_fake.webm') } - await makePostUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) + await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) + }) + + it('Should fail with an incorrect thumbnail file', async function () { + const fields = baseCorrectParams + const attaches = { + 'thumbnailfile': join(__dirname, '..', 'fixtures', 'avatar.png'), + 'videofile': join(__dirname, '..', 'fixtures', 'video_short_fake.webm') + } + + await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) + }) + + it('Should fail with a big thumbnail file', async function () { + const fields = baseCorrectParams + const attaches = { + 'thumbnailfile': join(__dirname, '..', 'fixtures', 'avatar-big.png'), + 'videofile': join(__dirname, '..', 'fixtures', 'video_short_fake.webm') + } + + await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) + }) + + it('Should fail with an incorrect preview file', async function () { + const fields = baseCorrectParams + const attaches = { + 'previewfile': join(__dirname, '..', 'fixtures', 'avatar.png'), + 'videofile': join(__dirname, '..', 'fixtures', 'video_short_fake.webm') + } + + await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) + }) + + it('Should fail with a big preview file', async function () { + const fields = baseCorrectParams + const attaches = { + 'previewfile': join(__dirname, '..', 'fixtures', 'avatar-big.png'), + 'videofile': join(__dirname, '..', 'fixtures', 'video_short_fake.webm') + } + + await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) }) it('Should succeed with the correct parameters', async function () { @@ -257,7 +297,7 @@ describe('Test videos API validator', function () { { const attaches = baseCorrectAttaches - await makePostUploadRequest({ + await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, @@ -272,7 +312,7 @@ describe('Test videos API validator', function () { videofile: join(__dirname, '..', 'fixtures', 'video_short.mp4') }) - await makePostUploadRequest({ + await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, @@ -287,7 +327,7 @@ describe('Test videos API validator', function () { videofile: join(__dirname, '..', 'fixtures', 'video_short.ogv') }) - await makePostUploadRequest({ + await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, @@ -400,6 +440,70 @@ describe('Test videos API validator', function () { await makePutBodyRequest({ url: server.url, path: path + videoId, token: server.accessToken, fields }) }) + it('Should fail with an incorrect thumbnail file', async function () { + const fields = baseCorrectParams + const attaches = { + 'thumbnailfile': join(__dirname, '..', 'fixtures', 'avatar.png') + } + + await makeUploadRequest({ + url: server.url, + method: 'PUT', + path: path + videoId, + token: server.accessToken, + fields, + attaches + }) + }) + + it('Should fail with a big thumbnail file', async function () { + const fields = baseCorrectParams + const attaches = { + 'thumbnailfile': join(__dirname, '..', 'fixtures', 'avatar-big.png') + } + + await makeUploadRequest({ + url: server.url, + method: 'PUT', + path: path + videoId, + token: server.accessToken, + fields, + attaches + }) + }) + + it('Should fail with an incorrect preview file', async function () { + const fields = baseCorrectParams + const attaches = { + 'previewfile': join(__dirname, '..', 'fixtures', 'avatar.png') + } + + await makeUploadRequest({ + url: server.url, + method: 'PUT', + path: path + videoId, + token: server.accessToken, + fields, + attaches + }) + }) + + it('Should fail with a big preview file', async function () { + const fields = baseCorrectParams + const attaches = { + 'previewfile': join(__dirname, '..', 'fixtures', 'avatar-big.png') + } + + await makeUploadRequest({ + url: server.url, + method: 'PUT', + path: path + videoId, + token: server.accessToken, + fields, + attaches + }) + }) + it('Should fail with a video of another user') it('Should fail with a video of another server') diff --git a/server/tests/api/fixtures/preview.jpg b/server/tests/api/fixtures/preview.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c40ece838e9679508f7d1c8e03585ba2ff05dc6b GIT binary patch literal 4215 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 minLength && data.length < maxLength } else { @@ -55,6 +55,14 @@ async function testImage (url: string, imageName: string, imagePath: string, ext } } +function buildAbsoluteFixturePath (path: string) { + if (isAbsolute(path)) { + return path + } + + return join(__dirname, '..', '..', 'api', 'fixtures', path) +} + // --------------------------------------------------------------------------- export { @@ -63,5 +71,6 @@ export { webtorrentAdd, immutableAssign, testImage, + buildAbsoluteFixturePath, root } diff --git a/server/tests/utils/requests/requests.ts b/server/tests/utils/requests/requests.ts index 840072430..a9b1dff9a 100644 --- a/server/tests/utils/requests/requests.ts +++ b/server/tests/utils/requests/requests.ts @@ -1,4 +1,5 @@ import * as request from 'supertest' +import { buildAbsoluteFixturePath } from '../' function makeGetRequest (options: { url: string, @@ -40,8 +41,9 @@ function makeDeleteRequest (options: { .expect(options.statusCodeExpected) } -function makePostUploadRequest (options: { +function makeUploadRequest (options: { url: string, + method?: 'POST' | 'PUT', path: string, token: string, fields: { [ fieldName: string ]: any }, @@ -50,9 +52,14 @@ function makePostUploadRequest (options: { }) { if (!options.statusCodeExpected) options.statusCodeExpected = 400 - const req = request(options.url) - .post(options.path) - .set('Accept', 'application/json') + let req: request.Test + if (options.method === 'PUT') { + req = request(options.url).put(options.path) + } else { + req = request(options.url).post(options.path) + } + + req.set('Accept', 'application/json') if (options.token) req.set('Authorization', 'Bearer ' + options.token) @@ -70,7 +77,7 @@ function makePostUploadRequest (options: { Object.keys(options.attaches).forEach(attach => { const value = options.attaches[attach] - req.attach(attach, value) + req.attach(attach, buildAbsoluteFixturePath(value)) }) return req.expect(options.statusCodeExpected) @@ -119,7 +126,7 @@ function makePutBodyRequest (options: { export { makeGetRequest, - makePostUploadRequest, + makeUploadRequest, makePostBodyRequest, makePutBodyRequest, makeDeleteRequest diff --git a/server/tests/utils/users/users.ts b/server/tests/utils/users/users.ts index 9e33e6796..3c9d46246 100644 --- a/server/tests/utils/users/users.ts +++ b/server/tests/utils/users/users.ts @@ -1,6 +1,6 @@ import { isAbsolute, join } from 'path' import * as request from 'supertest' -import { makePostBodyRequest, makePostUploadRequest, makePutBodyRequest } from '../' +import { makePostBodyRequest, makeUploadRequest, makePutBodyRequest } from '../' import { UserRole } from '../../../../shared/index' @@ -162,7 +162,7 @@ function updateMyAvatar (options: { filePath = join(__dirname, '..', '..', 'api', 'fixtures', options.fixture) } - return makePostUploadRequest({ + return makeUploadRequest({ url: options.url, path, token: options.accessToken, diff --git a/server/tests/utils/videos/videos.ts b/server/tests/utils/videos/videos.ts index 9105b5f13..9d4267db8 100644 --- a/server/tests/utils/videos/videos.ts +++ b/server/tests/utils/videos/videos.ts @@ -5,7 +5,16 @@ import { existsSync, readFile } from 'fs' import * as parseTorrent from 'parse-torrent' import { extname, isAbsolute, join } from 'path' import * as request from 'supertest' -import { getMyUserInformation, makeGetRequest, root, ServerInfo, testImage } from '../' +import { + buildAbsoluteFixturePath, + getMyUserInformation, + makeGetRequest, + makePutBodyRequest, + makeUploadRequest, + root, + ServerInfo, + testImage +} from '../' import { VideoPrivacy } from '../../../../shared/models/videos' import { readdirPromise } from '../../../helpers/core-utils' import { VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_PRIVACIES } from '../../../initializers' @@ -23,6 +32,8 @@ type VideoAttributes = { channelId?: number privacy?: VideoPrivacy fixture?: string + thumbnailfile?: string + previewfile?: string } function getVideoCategories (url: string) { @@ -228,8 +239,8 @@ async function uploadVideo (url: string, accessToken: string, videoAttributesArg defaultChannelId = res.body.videoChannels[0].id } catch (e) { /* empty */ } - // Default attributes - let attributes = { + // Override default attributes + const attributes = Object.assign({ name: 'my super video', category: 5, licence: 4, @@ -241,8 +252,7 @@ async function uploadVideo (url: string, accessToken: string, videoAttributesArg privacy: VideoPrivacy.PUBLIC, commentsEnabled: true, fixture: 'video_short.webm' - } - attributes = Object.assign(attributes, videoAttributesArg) + }, videoAttributesArg) const req = request(url) .post(path) @@ -267,22 +277,22 @@ async function uploadVideo (url: string, accessToken: string, videoAttributesArg req.field('licence', attributes.licence.toString()) } - for (let i = 0; i < attributes.tags.length; i++) { - req.field('tags[' + i + ']', attributes.tags[i]) + if (attributes.thumbnailfile !== undefined) { + req.attach('thumbnailfile', buildAbsoluteFixturePath(attributes.thumbnailfile)) + } + if (attributes.previewfile !== undefined) { + req.attach('previewfile', buildAbsoluteFixturePath(attributes.previewfile)) } - let filePath = '' - if (isAbsolute(attributes.fixture)) { - filePath = attributes.fixture - } else { - filePath = join(__dirname, '..', '..', 'api', 'fixtures', attributes.fixture) + for (let i = 0; i < attributes.tags.length; i++) { + req.field('tags[' + i + ']', attributes.tags[i]) } - return req.attach('videofile', filePath) + return req.attach('videofile', buildAbsoluteFixturePath(attributes.fixture)) .expect(specialStatus) } -function updateVideo (url: string, accessToken: string, id: number | string, attributes: VideoAttributes, specialStatus = 204) { +function updateVideo (url: string, accessToken: string, id: number | string, attributes: VideoAttributes, statusCodeExpected = 204) { const path = '/api/v1/videos/' + id const body = {} @@ -296,12 +306,30 @@ function updateVideo (url: string, accessToken: string, id: number | string, att if (attributes.tags) body['tags'] = attributes.tags if (attributes.privacy) body['privacy'] = attributes.privacy - return request(url) - .put(path) - .send(body) - .set('Accept', 'application/json') - .set('Authorization', 'Bearer ' + accessToken) - .expect(specialStatus) + // Upload request + if (attributes.thumbnailfile || attributes.previewfile) { + const attaches: any = {} + if (attributes.thumbnailfile) attaches.thumbnailfile = attributes.thumbnailfile + if (attributes.previewfile) attaches.previewfile = attributes.previewfile + + return makeUploadRequest({ + url, + method: 'PUT', + path, + token: accessToken, + fields: body, + attaches, + statusCodeExpected + }) + } + + return makePutBodyRequest({ + url, + path, + fields: body, + token: accessToken, + statusCodeExpected + }) } function rateVideo (url: string, accessToken: string, id: number, rating: string, specialStatus = 204) { @@ -355,7 +383,9 @@ async function completeVideoCheck ( files: { resolution: number size: number - }[] + }[], + thumbnailfile?: string + previewfile?: string } ) { if (!attributes.likes) attributes.likes = 0 @@ -414,8 +444,15 @@ async function completeVideoCheck ( const maxSize = attributeFile.size + ((10 * attributeFile.size) / 100) expect(file.size).to.be.above(minSize).and.below(maxSize) - const test = await testImage(url, attributes.fixture, videoDetails.thumbnailPath) - expect(test).to.equal(true) + { + const test = await testImage(url, attributes.thumbnailfile || attributes.fixture, videoDetails.thumbnailPath) + expect(test).to.equal(true) + } + + if (attributes.previewfile) { + const test = await testImage(url, attributes.previewfile, videoDetails.previewPath) + expect(test).to.equal(true) + } const torrent = await webtorrentAdd(magnetUri, true) expect(torrent.files).to.be.an('array') diff --git a/server/tools/import-youtube.ts b/server/tools/import-youtube.ts index 96bce29b5..ccbc71029 100644 --- a/server/tools/import-youtube.ts +++ b/server/tools/import-youtube.ts @@ -1,7 +1,5 @@ import * as program from 'commander' -import { createWriteStream } from 'fs' import { join } from 'path' -import { cursorTo } from 'readline' import * as youtubeDL from 'youtube-dl' import { VideoPrivacy } from '../../shared/models/videos' import { unlinkPromise } from '../helpers/core-utils' -- 2.41.0