From 93e1258c7cbc0d1235ca6d2a1f7c1875985328b8 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Fri, 25 Aug 2017 11:36:23 +0200 Subject: [PATCH] Move video file metadata in their own table Will be used for user video quotas and multiple video resolutions --- .gitignore | 1 + client/src/app/videos/shared/video.model.ts | 21 +- .../video-watch/video-magnet.component.html | 2 +- .../video-watch/video-watch.component.ts | 10 +- client/src/standalone/videos/embed.ts | 6 +- package.json | 1 + scripts/dev/index.sh | 5 + scripts/update-host.ts | 16 +- server.ts | 20 +- server/controllers/api/remote/videos.ts | 42 +- server/controllers/api/videos/index.ts | 65 ++- .../custom-validators/remote/videos.ts | 19 +- server/helpers/custom-validators/videos.ts | 39 +- server/initializers/constants.ts | 14 +- server/initializers/database.ts | 2 + .../migrations/0060-video-file.ts | 34 ++ .../migrations/0065-video-file-size.ts | 46 ++ server/initializers/migrator.ts | 19 +- server/lib/jobs/handlers/video-transcoder.ts | 4 +- server/models/video/index.ts | 1 + server/models/video/video-file-interface.ts | 24 + server/models/video/video-file.ts | 89 ++++ server/models/video/video-interface.ts | 67 +-- server/models/video/video.ts | 434 +++++++++--------- server/tests/api/multiple-pods.js | 69 ++- server/tests/api/single-pod.js | 71 ++- server/tests/api/video-transcoder.js | 10 +- .../remote-video-create-request.model.ts | 8 +- .../remote-video-update-request.model.ts | 6 + shared/models/videos/video.model.ts | 9 +- 30 files changed, 816 insertions(+), 338 deletions(-) create mode 100755 scripts/dev/index.sh create mode 100644 server/initializers/migrations/0060-video-file.ts create mode 100644 server/initializers/migrations/0065-video-file-size.ts create mode 100644 server/models/video/video-file-interface.ts create mode 100644 server/models/video/video-file.ts diff --git a/.gitignore b/.gitignore index 42d4f5926..b4c1de87d 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,4 @@ /*.sublime-workspace /dist /.idea +/PeerTube.iml diff --git a/client/src/app/videos/shared/video.model.ts b/client/src/app/videos/shared/video.model.ts index f0556343f..438791368 100644 --- a/client/src/app/videos/shared/video.model.ts +++ b/client/src/app/videos/shared/video.model.ts @@ -1,4 +1,4 @@ -import { Video as VideoServerModel } from '../../../../../shared' +import { Video as VideoServerModel, VideoFile } from '../../../../../shared' import { User } from '../../shared' export class Video implements VideoServerModel { @@ -17,7 +17,6 @@ export class Video implements VideoServerModel { id: number uuid: string isLocal: boolean - magnetUri: string name: string podHost: string tags: string[] @@ -29,6 +28,7 @@ export class Video implements VideoServerModel { likes: number dislikes: number nsfw: boolean + files: VideoFile[] private static createByString (author: string, podHost: string) { return author + '@' + podHost @@ -57,7 +57,6 @@ export class Video implements VideoServerModel { id: number, uuid: string, isLocal: boolean, - magnetUri: string, name: string, podHost: string, tags: string[], @@ -66,7 +65,8 @@ export class Video implements VideoServerModel { views: number, likes: number, dislikes: number, - nsfw: boolean + nsfw: boolean, + files: VideoFile[] }) { this.author = hash.author this.createdAt = new Date(hash.createdAt) @@ -82,7 +82,6 @@ export class Video implements VideoServerModel { this.id = hash.id this.uuid = hash.uuid this.isLocal = hash.isLocal - this.magnetUri = hash.magnetUri this.name = hash.name this.podHost = hash.podHost this.tags = hash.tags @@ -94,6 +93,7 @@ export class Video implements VideoServerModel { this.likes = hash.likes this.dislikes = hash.dislikes this.nsfw = hash.nsfw + this.files = hash.files this.by = Video.createByString(hash.author, hash.podHost) } @@ -115,6 +115,13 @@ export class Video implements VideoServerModel { return (this.nsfw && (!user || user.displayNSFW === false)) } + getDefaultMagnetUri () { + if (this.files === undefined || this.files.length === 0) return '' + + // TODO: choose the original file + return this.files[0].magnetUri + } + patch (values: Object) { Object.keys(values).forEach((key) => { this[key] = values[key] @@ -132,7 +139,6 @@ export class Video implements VideoServerModel { duration: this.duration, id: this.id, isLocal: this.isLocal, - magnetUri: this.magnetUri, name: this.name, podHost: this.podHost, tags: this.tags, @@ -140,7 +146,8 @@ export class Video implements VideoServerModel { views: this.views, likes: this.likes, dislikes: this.dislikes, - nsfw: this.nsfw + nsfw: this.nsfw, + files: this.files } } } diff --git a/client/src/app/videos/video-watch/video-magnet.component.html b/client/src/app/videos/video-watch/video-magnet.component.html index 3fa82f1be..5b0324e37 100644 --- a/client/src/app/videos/video-watch/video-magnet.component.html +++ b/client/src/app/videos/video-watch/video-magnet.component.html @@ -10,7 +10,7 @@ diff --git a/client/src/app/videos/video-watch/video-watch.component.ts b/client/src/app/videos/video-watch/video-watch.component.ts index cd11aa33c..255757692 100644 --- a/client/src/app/videos/video-watch/video-watch.component.ts +++ b/client/src/app/videos/video-watch/video-watch.component.ts @@ -90,8 +90,8 @@ export class VideoWatchComponent implements OnInit, OnDestroy { window.clearInterval(this.torrentInfosInterval) window.clearTimeout(this.errorTimer) - if (this.video !== null && this.webTorrentService.has(this.video.magnetUri)) { - this.webTorrentService.remove(this.video.magnetUri) + if (this.video !== null && this.webTorrentService.has(this.video.getDefaultMagnetUri())) { + this.webTorrentService.remove(this.video.getDefaultMagnetUri()) } // Remove player @@ -108,13 +108,13 @@ export class VideoWatchComponent implements OnInit, OnDestroy { // We are loading the video this.loading = true - console.log('Adding ' + this.video.magnetUri + '.') + console.log('Adding ' + this.video.getDefaultMagnetUri() + '.') // The callback might never return if there are network issues // So we create a timer to inform the user the load is abnormally long this.errorTimer = window.setTimeout(() => this.loadTooLong(), VideoWatchComponent.LOADTIME_TOO_LONG) - const torrent = this.webTorrentService.add(this.video.magnetUri, torrent => { + const torrent = this.webTorrentService.add(this.video.getDefaultMagnetUri(), torrent => { // Clear the error timer window.clearTimeout(this.errorTimer) // Maybe the error was fired by the timer, so reset it @@ -123,7 +123,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy { // We are not loading the video anymore this.loading = false - console.log('Added ' + this.video.magnetUri + '.') + console.log('Added ' + this.video.getDefaultMagnetUri() + '.') torrent.files[0].renderTo(this.playerElement, (err) => { if (err) { this.notificationsService.error('Error', 'Cannot append the file in the video element.') diff --git a/client/src/standalone/videos/embed.ts b/client/src/standalone/videos/embed.ts index 64a0f0798..0698344b0 100644 --- a/client/src/standalone/videos/embed.ts +++ b/client/src/standalone/videos/embed.ts @@ -57,7 +57,11 @@ loadVideoInfos(videoId, (err, videoInfos) => { return } - const magnetUri = videoInfos.magnetUri + let magnetUri = '' + if (videoInfos.files !== undefined && videoInfos.files.length !== 0) { + magnetUri = videoInfos.files[0].magnetUri + } + const videoContainer = document.getElementById('video-container') as HTMLVideoElement const previewUrl = window.location.origin + videoInfos.previewPath videoContainer.poster = previewUrl diff --git a/package.json b/package.json index d6da61975..9478af8fa 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "danger:clean:modules": "scripty", "reset-password": "ts-node ./scripts/reset-password.ts", "play": "scripty", + "dev": "scripty", "dev:server": "scripty", "dev:client": "scripty", "start": "node dist/server", diff --git a/scripts/dev/index.sh b/scripts/dev/index.sh new file mode 100755 index 000000000..938bf6056 --- /dev/null +++ b/scripts/dev/index.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env sh + +NODE_ENV=test concurrently -k \ + "npm run watch:client" \ + "npm run watch:server" diff --git a/scripts/update-host.ts b/scripts/update-host.ts index 23e8d5ef3..5e69e4172 100755 --- a/scripts/update-host.ts +++ b/scripts/update-host.ts @@ -1,4 +1,5 @@ import { readFileSync, writeFileSync } from 'fs' +import { join } from 'path' import * as parseTorrent from 'parse-torrent' import { CONFIG, STATIC_PATHS } from '../server/initializers/constants' @@ -19,17 +20,10 @@ db.init(true) return db.Video.list() }) .then(videos => { - videos.forEach(function (video) { - const torrentName = video.id + '.torrent' - const torrentPath = CONFIG.STORAGE.TORRENTS_DIR + torrentName - const filename = video.id + video.extname - - const parsed = parseTorrent(readFileSync(torrentPath)) - parsed.announce = [ CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOST + '/tracker/socket' ] - parsed.urlList = [ CONFIG.WEBSERVER.URL + STATIC_PATHS.WEBSEED + filename ] - - const buf = parseTorrent.toTorrentFile(parsed) - writeFileSync(torrentPath, buf) + videos.forEach(video => { + video.VideoFiles.forEach(file => { + video.createTorrentAndSetInfoHash(file) + }) }) process.exit(0) diff --git a/server.ts b/server.ts index 1ba208c28..2effa9340 100644 --- a/server.ts +++ b/server.ts @@ -26,7 +26,7 @@ const app = express() // ----------- Database ----------- // Do not use barrels because we don't want to load all modules here (we need to initialize database first) import { logger } from './server/helpers/logger' -import { API_VERSION, CONFIG } from './server/initializers/constants' +import { API_VERSION, CONFIG, STATIC_PATHS } from './server/initializers/constants' // Initialize database and models import { database as db } from './server/initializers/database' db.init(false).then(() => onDatabaseInitDone()) @@ -57,10 +57,20 @@ import { apiRouter, clientsRouter, staticRouter } from './server/controllers' // Enable CORS for develop if (isTestInstance()) { - app.use(cors({ - origin: 'http://localhost:3000', - credentials: true - })) + app.use((req, res, next) => { + // These routes have already cors + if ( + req.path.indexOf(STATIC_PATHS.TORRENTS) === -1 && + req.path.indexOf(STATIC_PATHS.WEBSEED) === -1 + ) { + return (cors({ + origin: 'http://localhost:3000', + credentials: true + }))(req, res, next) + } + + return next() + }) } // For the logger diff --git a/server/controllers/api/remote/videos.ts b/server/controllers/api/remote/videos.ts index 30771d8c4..e7edff606 100644 --- a/server/controllers/api/remote/videos.ts +++ b/server/controllers/api/remote/videos.ts @@ -258,8 +258,6 @@ function addRemoteVideo (videoToCreateData: RemoteVideoCreateData, fromPod: PodI const videoData = { name: videoToCreateData.name, uuid: videoToCreateData.uuid, - extname: videoToCreateData.extname, - infoHash: videoToCreateData.infoHash, category: videoToCreateData.category, licence: videoToCreateData.licence, language: videoToCreateData.language, @@ -289,6 +287,26 @@ function addRemoteVideo (videoToCreateData: RemoteVideoCreateData, fromPod: PodI return video.save(options).then(videoCreated => ({ tagInstances, videoCreated })) }) + .then(({ tagInstances, videoCreated }) => { + const tasks = [] + const options = { + transaction: t + } + + videoToCreateData.files.forEach(fileData => { + const videoFileInstance = db.VideoFile.build({ + extname: fileData.extname, + infoHash: fileData.infoHash, + resolution: fileData.resolution, + size: fileData.size, + videoId: videoCreated.id + }) + + tasks.push(videoFileInstance.save(options)) + }) + + return Promise.all(tasks).then(() => ({ tagInstances, videoCreated })) + }) .then(({ tagInstances, videoCreated }) => { const options = { transaction: t @@ -344,6 +362,26 @@ function updateRemoteVideo (videoAttributesToUpdate: RemoteVideoUpdateData, from return videoInstance.save(options).then(() => ({ videoInstance, tagInstances })) }) + .then(({ tagInstances, videoInstance }) => { + const tasks = [] + const options = { + transaction: t + } + + videoAttributesToUpdate.files.forEach(fileData => { + const videoFileInstance = db.VideoFile.build({ + extname: fileData.extname, + infoHash: fileData.infoHash, + resolution: fileData.resolution, + size: fileData.size, + videoId: videoInstance.id + }) + + tasks.push(videoFileInstance.save(options)) + }) + + return Promise.all(tasks).then(() => ({ tagInstances, videoInstance })) + }) .then(({ videoInstance, tagInstances }) => { const options = { transaction: t } diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts index 815881df3..d71a132ed 100644 --- a/server/controllers/api/videos/index.ts +++ b/server/controllers/api/videos/index.ts @@ -1,7 +1,7 @@ import * as express from 'express' import * as Promise from 'bluebird' import * as multer from 'multer' -import * as path from 'path' +import { extname, join } from 'path' import { database as db } from '../../../initializers/database' import { @@ -16,7 +16,8 @@ import { addEventToRemoteVideo, quickAndDirtyUpdateVideoToFriends, addVideoToFriends, - updateVideoToFriends + updateVideoToFriends, + JobScheduler } from '../../../lib' import { authenticate, @@ -155,7 +156,7 @@ function addVideoRetryWrapper (req: express.Request, res: express.Response, next .catch(err => next(err)) } -function addVideo (req: express.Request, res: express.Response, videoFile: Express.Multer.File) { +function addVideo (req: express.Request, res: express.Response, videoPhysicalFile: Express.Multer.File) { const videoInfos: VideoCreate = req.body return db.sequelize.transaction(t => { @@ -177,13 +178,13 @@ function addVideo (req: express.Request, res: express.Response, videoFile: Expre const videoData = { name: videoInfos.name, remote: false, - extname: path.extname(videoFile.filename), + extname: extname(videoPhysicalFile.filename), category: videoInfos.category, licence: videoInfos.licence, language: videoInfos.language, nsfw: videoInfos.nsfw, description: videoInfos.description, - duration: videoFile['duration'], // duration was added by a previous middleware + duration: videoPhysicalFile['duration'], // duration was added by a previous middleware authorId: author.id } @@ -191,18 +192,50 @@ function addVideo (req: express.Request, res: express.Response, videoFile: Expre return { author, tagInstances, video } }) .then(({ author, tagInstances, video }) => { + const videoFileData = { + extname: extname(videoPhysicalFile.filename), + resolution: 0, // TODO: improve readability, + size: videoPhysicalFile.size + } + + const videoFile = db.VideoFile.build(videoFileData) + return { author, tagInstances, video, videoFile } + }) + .then(({ author, tagInstances, video, videoFile }) => { const videoDir = CONFIG.STORAGE.VIDEOS_DIR - const source = path.join(videoDir, videoFile.filename) - const destination = path.join(videoDir, video.getVideoFilename()) + const source = join(videoDir, videoPhysicalFile.filename) + const destination = join(videoDir, video.getVideoFilename(videoFile)) return renamePromise(source, destination) .then(() => { // This is important in case if there is another attempt in the retry process - videoFile.filename = video.getVideoFilename() - return { author, tagInstances, video } + videoPhysicalFile.filename = video.getVideoFilename(videoFile) + return { author, tagInstances, video, videoFile } }) }) - .then(({ author, tagInstances, video }) => { + .then(({ author, tagInstances, video, videoFile }) => { + const tasks = [] + + tasks.push( + video.createTorrentAndSetInfoHash(videoFile), + video.createThumbnail(videoFile), + video.createPreview(videoFile) + ) + + if (CONFIG.TRANSCODING.ENABLED === true) { + // Put uuid because we don't have id auto incremented for now + const dataInput = { + videoUUID: video.uuid + } + + tasks.push( + JobScheduler.Instance.createJob(t, 'videoTranscoder', dataInput) + ) + } + + return Promise.all(tasks).then(() => ({ author, tagInstances, video, videoFile })) + }) + .then(({ author, tagInstances, video, videoFile }) => { const options = { transaction: t } return video.save(options) @@ -210,9 +243,17 @@ function addVideo (req: express.Request, res: express.Response, videoFile: Expre // Do not forget to add Author informations to the created video videoCreated.Author = author - return { tagInstances, video: videoCreated } + return { tagInstances, video: videoCreated, videoFile } }) }) + .then(({ tagInstances, video, videoFile }) => { + const options = { transaction: t } + videoFile.videoId = video.id + + return videoFile.save(options) + .then(() => video.VideoFiles = [ videoFile ]) + .then(() => ({ tagInstances, video })) + }) .then(({ tagInstances, video }) => { if (!tagInstances) return video @@ -236,7 +277,7 @@ function addVideo (req: express.Request, res: express.Response, videoFile: Expre }) .then(() => logger.info('Video with name %s created.', videoInfos.name)) .catch((err: Error) => { - logger.debug('Cannot insert the video.', { error: err.stack }) + logger.debug('Cannot insert the video.', err) throw err }) } diff --git a/server/helpers/custom-validators/remote/videos.ts b/server/helpers/custom-validators/remote/videos.ts index b33d8c9be..091cd2186 100644 --- a/server/helpers/custom-validators/remote/videos.ts +++ b/server/helpers/custom-validators/remote/videos.ts @@ -23,10 +23,11 @@ import { isVideoNSFWValid, isVideoDescriptionValid, isVideoDurationValid, - isVideoInfoHashValid, + isVideoFileInfoHashValid, isVideoNameValid, isVideoTagsValid, - isVideoExtnameValid + isVideoFileExtnameValid, + isVideoFileResolutionValid } from '../videos' const ENDPOINT_ACTIONS = REQUEST_ENDPOINT_ACTIONS[REQUEST_ENDPOINTS.VIDEOS] @@ -121,14 +122,22 @@ function isCommonVideoAttributesValid (video: any) { isVideoNSFWValid(video.nsfw) && isVideoDescriptionValid(video.description) && isVideoDurationValid(video.duration) && - isVideoInfoHashValid(video.infoHash) && isVideoNameValid(video.name) && isVideoTagsValid(video.tags) && isVideoUUIDValid(video.uuid) && - isVideoExtnameValid(video.extname) && isVideoViewsValid(video.views) && isVideoLikesValid(video.likes) && - isVideoDislikesValid(video.dislikes) + isVideoDislikesValid(video.dislikes) && + isArray(video.files) && + video.files.every(videoFile => { + if (!videoFile) return false + + return ( + isVideoFileInfoHashValid(videoFile.infoHash) && + isVideoFileExtnameValid(videoFile.extname) && + isVideoFileResolutionValid(videoFile.resolution) + ) + }) } function isRequestTypeAddValid (value: string) { diff --git a/server/helpers/custom-validators/videos.ts b/server/helpers/custom-validators/videos.ts index 62132acb1..139fa760f 100644 --- a/server/helpers/custom-validators/videos.ts +++ b/server/helpers/custom-validators/videos.ts @@ -7,7 +7,8 @@ import { VIDEO_CATEGORIES, VIDEO_LICENCES, VIDEO_LANGUAGES, - VIDEO_RATE_TYPES + VIDEO_RATE_TYPES, + VIDEO_FILE_RESOLUTIONS } from '../../initializers' import { isUserUsernameValid } from './users' import { isArray, exists } from './misc' @@ -53,14 +54,6 @@ function isVideoDurationValid (value: string) { return exists(value) && validator.isInt(value + '', VIDEOS_CONSTRAINTS_FIELDS.DURATION) } -function isVideoExtnameValid (value: string) { - return VIDEOS_CONSTRAINTS_FIELDS.EXTNAME.indexOf(value) !== -1 -} - -function isVideoInfoHashValid (value: string) { - return exists(value) && validator.isLength(value, VIDEOS_CONSTRAINTS_FIELDS.INFO_HASH) -} - function isVideoNameValid (value: string) { return exists(value) && validator.isLength(value, VIDEOS_CONSTRAINTS_FIELDS.NAME) } @@ -128,6 +121,22 @@ function isVideoFile (value: string, files: { [ fieldname: string ]: Express.Mul return new RegExp('^video/(webm|mp4|ogg)$', 'i').test(file.mimetype) } +function isVideoFileSizeValid (value: string) { + return exists(value) && validator.isInt(value + '', VIDEOS_CONSTRAINTS_FIELDS.FILE_SIZE) +} + +function isVideoFileResolutionValid (value: string) { + return VIDEO_FILE_RESOLUTIONS[value] !== undefined +} + +function isVideoFileExtnameValid (value: string) { + return VIDEOS_CONSTRAINTS_FIELDS.EXTNAME.indexOf(value) !== -1 +} + +function isVideoFileInfoHashValid (value: string) { + return exists(value) && validator.isLength(value, VIDEOS_CONSTRAINTS_FIELDS.INFO_HASH) +} + // --------------------------------------------------------------------------- export { @@ -140,12 +149,12 @@ export { isVideoNSFWValid, isVideoDescriptionValid, isVideoDurationValid, - isVideoInfoHashValid, + isVideoFileInfoHashValid, isVideoNameValid, isVideoTagsValid, isVideoThumbnailValid, isVideoThumbnailDataValid, - isVideoExtnameValid, + isVideoFileExtnameValid, isVideoUUIDValid, isVideoAbuseReasonValid, isVideoAbuseReporterUsernameValid, @@ -154,7 +163,9 @@ export { isVideoLikesValid, isVideoRatingTypeValid, isVideoDislikesValid, - isVideoEventCountValid + isVideoEventCountValid, + isVideoFileSizeValid, + isVideoFileResolutionValid } declare global { @@ -183,7 +194,9 @@ declare global { isVideoLikesValid, isVideoRatingTypeValid, isVideoDislikesValid, - isVideoEventCountValid + isVideoEventCountValid, + isVideoFileSizeValid, + isVideoFileResolutionValid } } } diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index 314a05ab7..50a939083 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts @@ -15,7 +15,7 @@ import { // --------------------------------------------------------------------------- -const LAST_MIGRATION_VERSION = 55 +const LAST_MIGRATION_VERSION = 65 // --------------------------------------------------------------------------- @@ -114,7 +114,8 @@ const CONSTRAINTS_FIELDS = { THUMBNAIL_DATA: { min: 0, max: 20000 }, // Bytes VIEWS: { min: 0 }, LIKES: { min: 0 }, - DISLIKES: { min: 0 } + DISLIKES: { min: 0 }, + FILE_SIZE: { min: 10, max: 1024 * 1024 * 1024 * 3 /* 3Go */ } }, VIDEO_EVENTS: { COUNT: { min: 0 } @@ -176,6 +177,14 @@ const VIDEO_LANGUAGES = { 14: 'Italien' } +const VIDEO_FILE_RESOLUTIONS = { + 0: 'original', + 1: '360p', + 2: '480p', + 3: '720p', + 4: '1080p' +} + // --------------------------------------------------------------------------- // Score a pod has when we create it as a friend @@ -362,6 +371,7 @@ export { THUMBNAILS_SIZE, USER_ROLES, VIDEO_CATEGORIES, + VIDEO_FILE_RESOLUTIONS, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_RATE_TYPES diff --git a/server/initializers/database.ts b/server/initializers/database.ts index 9e691bf1d..c0df2b63a 100644 --- a/server/initializers/database.ts +++ b/server/initializers/database.ts @@ -23,6 +23,7 @@ import { UserVideoRateModel, VideoAbuseModel, BlacklistedVideoModel, + VideoFileModel, VideoTagModel, VideoModel } from '../models' @@ -49,6 +50,7 @@ const database: { UserVideoRate?: UserVideoRateModel, User?: UserModel, VideoAbuse?: VideoAbuseModel, + VideoFile?: VideoFileModel, BlacklistedVideo?: BlacklistedVideoModel, VideoTag?: VideoTagModel, Video?: VideoModel diff --git a/server/initializers/migrations/0060-video-file.ts b/server/initializers/migrations/0060-video-file.ts new file mode 100644 index 000000000..c362cf71a --- /dev/null +++ b/server/initializers/migrations/0060-video-file.ts @@ -0,0 +1,34 @@ +import * as Sequelize from 'sequelize' +import * as Promise from 'bluebird' + +function up (utils: { + transaction: Sequelize.Transaction, + queryInterface: Sequelize.QueryInterface, + sequelize: Sequelize.Sequelize, + db: any +}): Promise { + const q = utils.queryInterface + + const query = 'INSERT INTO "VideoFiles" ("videoId", "resolution", "size", "extname", "infoHash", "createdAt", "updatedAt") ' + + 'SELECT "id" AS "videoId", 0 AS "resolution", 0 AS "size", ' + + '"extname"::"text"::"enum_VideoFiles_extname" as "extname", "infoHash", "createdAt", "updatedAt" ' + + 'FROM "Videos"' + + return utils.db.VideoFile.sync() + .then(() => utils.sequelize.query(query)) + .then(() => { + return q.removeColumn('Videos', 'extname') + }) + .then(() => { + return q.removeColumn('Videos', 'infoHash') + }) +} + +function down (options) { + throw new Error('Not implemented.') +} + +export { + up, + down +} diff --git a/server/initializers/migrations/0065-video-file-size.ts b/server/initializers/migrations/0065-video-file-size.ts new file mode 100644 index 000000000..58f8f3bcc --- /dev/null +++ b/server/initializers/migrations/0065-video-file-size.ts @@ -0,0 +1,46 @@ +import * as Sequelize from 'sequelize' +import * as Promise from 'bluebird' +import { stat } from 'fs' + +import { VideoInstance } from '../../models' + +function up (utils: { + transaction: Sequelize.Transaction, + queryInterface: Sequelize.QueryInterface, + sequelize: Sequelize.Sequelize, + db: any +}): Promise { + return utils.db.Video.listOwnedAndPopulateAuthorAndTags() + .then((videos: VideoInstance[]) => { + const tasks: Promise[] = [] + + videos.forEach(video => { + video.VideoFiles.forEach(videoFile => { + const p = new Promise((res, rej) => { + stat(video.getVideoFilePath(videoFile), (err, stats) => { + if (err) return rej(err) + + videoFile.size = stats.size + videoFile.save().then(res).catch(rej) + }) + }) + + tasks.push(p) + }) + }) + + return tasks + }) + .then((tasks: Promise[]) => { + return Promise.all(tasks) + }) +} + +function down (options) { + throw new Error('Not implemented.') +} + +export { + up, + down +} diff --git a/server/initializers/migrator.ts b/server/initializers/migrator.ts index 71a656c59..7b535aea9 100644 --- a/server/initializers/migrator.ts +++ b/server/initializers/migrator.ts @@ -64,14 +64,16 @@ function getMigrationScripts () { script: string }[] = [] - files.forEach(file => { - // Filename is something like 'version-blabla.js' - const version = file.split('-')[0] - filesToMigrate.push({ - version, - script: file + files + .filter(file => file.endsWith('.js.map') === false) + .forEach(file => { + // Filename is something like 'version-blabla.js' + const version = file.split('-')[0] + filesToMigrate.push({ + version, + script: file + }) }) - }) return filesToMigrate }) @@ -93,7 +95,8 @@ function executeMigration (actualVersion: number, entity: { version: string, scr const options = { transaction: t, queryInterface: db.sequelize.getQueryInterface(), - sequelize: db.sequelize + sequelize: db.sequelize, + db } return migrationScript.up(options) diff --git a/server/lib/jobs/handlers/video-transcoder.ts b/server/lib/jobs/handlers/video-transcoder.ts index 0d32dfd2f..87d8ffa6a 100644 --- a/server/lib/jobs/handlers/video-transcoder.ts +++ b/server/lib/jobs/handlers/video-transcoder.ts @@ -5,7 +5,9 @@ import { VideoInstance } from '../../../models' function process (data: { videoUUID: string }) { return db.Video.loadByUUIDAndPopulateAuthorAndPodAndTags(data.videoUUID).then(video => { - return video.transcodeVideofile().then(() => video) + // TODO: handle multiple resolutions + const videoFile = video.VideoFiles[0] + return video.transcodeVideofile(videoFile).then(() => video) }) } diff --git a/server/models/video/index.ts b/server/models/video/index.ts index 84b801c72..08b360376 100644 --- a/server/models/video/index.ts +++ b/server/models/video/index.ts @@ -3,4 +3,5 @@ export * from './tag-interface' export * from './video-abuse-interface' export * from './video-blacklist-interface' export * from './video-tag-interface' +export * from './video-file-interface' export * from './video-interface' diff --git a/server/models/video/video-file-interface.ts b/server/models/video/video-file-interface.ts new file mode 100644 index 000000000..c9fb8b8ae --- /dev/null +++ b/server/models/video/video-file-interface.ts @@ -0,0 +1,24 @@ +import * as Sequelize from 'sequelize' + +export namespace VideoFileMethods { +} + +export interface VideoFileClass { +} + +export interface VideoFileAttributes { + resolution: number + size: number + infoHash?: string + extname: string + + videoId?: number +} + +export interface VideoFileInstance extends VideoFileClass, VideoFileAttributes, Sequelize.Instance { + id: number + createdAt: Date + updatedAt: Date +} + +export interface VideoFileModel extends VideoFileClass, Sequelize.Model {} diff --git a/server/models/video/video-file.ts b/server/models/video/video-file.ts new file mode 100644 index 000000000..09a30d7e0 --- /dev/null +++ b/server/models/video/video-file.ts @@ -0,0 +1,89 @@ +import * as Sequelize from 'sequelize' +import { values } from 'lodash' + +import { CONSTRAINTS_FIELDS } from '../../initializers' +import { + isVideoFileResolutionValid, + isVideoFileSizeValid, + isVideoFileInfoHashValid +} from '../../helpers' + +import { addMethodsToModel } from '../utils' +import { + VideoFileInstance, + VideoFileAttributes +} from './video-file-interface' + +let VideoFile: Sequelize.Model + +export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { + VideoFile = sequelize.define('VideoFile', + { + resolution: { + type: DataTypes.INTEGER, + allowNull: false, + validate: { + resolutionValid: value => { + const res = isVideoFileResolutionValid(value) + if (res === false) throw new Error('Video file resolution is not valid.') + } + } + }, + size: { + type: DataTypes.INTEGER, + allowNull: false, + validate: { + sizeValid: value => { + const res = isVideoFileSizeValid(value) + if (res === false) throw new Error('Video file size is not valid.') + } + } + }, + extname: { + type: DataTypes.ENUM(values(CONSTRAINTS_FIELDS.VIDEOS.EXTNAME)), + allowNull: false + }, + infoHash: { + type: DataTypes.STRING, + allowNull: false, + validate: { + infoHashValid: value => { + const res = isVideoFileInfoHashValid(value) + if (res === false) throw new Error('Video file info hash is not valid.') + } + } + } + }, + { + indexes: [ + { + fields: [ 'videoId' ] + }, + { + fields: [ 'infoHash' ] + } + ] + } + ) + + const classMethods = [ + associate + ] + addMethodsToModel(VideoFile, classMethods) + + return VideoFile +} + +// ------------------------------ STATICS ------------------------------ + +function associate (models) { + VideoFile.belongsTo(models.Video, { + foreignKey: { + name: 'videoId', + allowNull: false + }, + onDelete: 'CASCADE' + }) +} + +// ------------------------------ METHODS ------------------------------ diff --git a/server/models/video/video-interface.ts b/server/models/video/video-interface.ts index 2fabcd906..976c70b5e 100644 --- a/server/models/video/video-interface.ts +++ b/server/models/video/video-interface.ts @@ -3,11 +3,19 @@ import * as Promise from 'bluebird' import { AuthorInstance } from './author-interface' import { TagAttributes, TagInstance } from './tag-interface' +import { VideoFileAttributes, VideoFileInstance } from './video-file-interface' // Don't use barrel, import just what we need import { Video as FormatedVideo } from '../../../shared/models/videos/video.model' import { ResultList } from '../../../shared/models/result-list.model' +export type FormatedRemoteVideoFile = { + infoHash: string + resolution: number + extname: string + size: number +} + export type FormatedAddRemoteVideo = { uuid: string name: string @@ -16,17 +24,16 @@ export type FormatedAddRemoteVideo = { language: number nsfw: boolean description: string - infoHash: string author: string duration: number thumbnailData: string tags: string[] createdAt: Date updatedAt: Date - extname: string views: number likes: number dislikes: number + files: FormatedRemoteVideoFile[] } export type FormatedUpdateRemoteVideo = { @@ -37,31 +44,35 @@ export type FormatedUpdateRemoteVideo = { language: number nsfw: boolean description: string - infoHash: string author: string duration: number tags: string[] createdAt: Date updatedAt: Date - extname: string views: number likes: number dislikes: number + files: FormatedRemoteVideoFile[] } export namespace VideoMethods { - export type GenerateMagnetUri = (this: VideoInstance) => string - export type GetVideoFilename = (this: VideoInstance) => string export type GetThumbnailName = (this: VideoInstance) => string export type GetPreviewName = (this: VideoInstance) => string - export type GetTorrentName = (this: VideoInstance) => string export type IsOwned = (this: VideoInstance) => boolean export type ToFormatedJSON = (this: VideoInstance) => FormatedVideo + export type GenerateMagnetUri = (this: VideoInstance, videoFile: VideoFileInstance) => string + export type GetTorrentFileName = (this: VideoInstance, videoFile: VideoFileInstance) => string + export type GetVideoFilename = (this: VideoInstance, videoFile: VideoFileInstance) => string + export type CreatePreview = (this: VideoInstance, videoFile: VideoFileInstance) => Promise + export type CreateThumbnail = (this: VideoInstance, videoFile: VideoFileInstance) => Promise + export type GetVideoFilePath = (this: VideoInstance, videoFile: VideoFileInstance) => string + export type CreateTorrentAndSetInfoHash = (this: VideoInstance, videoFile: VideoFileInstance) => Promise + export type ToAddRemoteJSON = (this: VideoInstance) => Promise export type ToUpdateRemoteJSON = (this: VideoInstance) => FormatedUpdateRemoteVideo - export type TranscodeVideofile = (this: VideoInstance) => Promise + export type TranscodeVideofile = (this: VideoInstance, inputVideoFile: VideoFileInstance) => Promise // Return thumbnail name export type GenerateThumbnailFromData = (video: VideoInstance, thumbnailData: string) => Promise @@ -86,31 +97,25 @@ export namespace VideoMethods { export type LoadAndPopulateAuthor = (id: number) => Promise export type LoadAndPopulateAuthorAndPodAndTags = (id: number) => Promise export type LoadByUUIDAndPopulateAuthorAndPodAndTags = (uuid: string) => Promise + + export type RemoveThumbnail = (this: VideoInstance) => Promise + export type RemovePreview = (this: VideoInstance) => Promise + export type RemoveFile = (this: VideoInstance, videoFile: VideoFileInstance) => Promise + export type RemoveTorrent = (this: VideoInstance, videoFile: VideoFileInstance) => Promise } export interface VideoClass { - generateMagnetUri: VideoMethods.GenerateMagnetUri - getVideoFilename: VideoMethods.GetVideoFilename - getThumbnailName: VideoMethods.GetThumbnailName - getPreviewName: VideoMethods.GetPreviewName - getTorrentName: VideoMethods.GetTorrentName - isOwned: VideoMethods.IsOwned - toFormatedJSON: VideoMethods.ToFormatedJSON - toAddRemoteJSON: VideoMethods.ToAddRemoteJSON - toUpdateRemoteJSON: VideoMethods.ToUpdateRemoteJSON - transcodeVideofile: VideoMethods.TranscodeVideofile - generateThumbnailFromData: VideoMethods.GenerateThumbnailFromData getDurationFromFile: VideoMethods.GetDurationFromFile list: VideoMethods.List listForApi: VideoMethods.ListForApi - loadByHostAndUUID: VideoMethods.LoadByHostAndUUID listOwnedAndPopulateAuthorAndTags: VideoMethods.ListOwnedAndPopulateAuthorAndTags listOwnedByAuthor: VideoMethods.ListOwnedByAuthor load: VideoMethods.Load - loadByUUID: VideoMethods.LoadByUUID loadAndPopulateAuthor: VideoMethods.LoadAndPopulateAuthor loadAndPopulateAuthorAndPodAndTags: VideoMethods.LoadAndPopulateAuthorAndPodAndTags + loadByHostAndUUID: VideoMethods.LoadByHostAndUUID + loadByUUID: VideoMethods.LoadByUUID loadByUUIDAndPopulateAuthorAndPodAndTags: VideoMethods.LoadByUUIDAndPopulateAuthorAndPodAndTags searchAndPopulateAuthorAndPodAndTags: VideoMethods.SearchAndPopulateAuthorAndPodAndTags } @@ -118,13 +123,11 @@ export interface VideoClass { export interface VideoAttributes { uuid?: string name: string - extname: string category: number licence: number language: number nsfw: boolean description: string - infoHash?: string duration: number views?: number likes?: number @@ -133,6 +136,7 @@ export interface VideoAttributes { Author?: AuthorInstance Tags?: TagInstance[] + VideoFiles?: VideoFileInstance[] } export interface VideoInstance extends VideoClass, VideoAttributes, Sequelize.Instance { @@ -140,18 +144,27 @@ export interface VideoInstance extends VideoClass, VideoAttributes, Sequelize.In createdAt: Date updatedAt: Date + createPreview: VideoMethods.CreatePreview + createThumbnail: VideoMethods.CreateThumbnail + createTorrentAndSetInfoHash: VideoMethods.CreateTorrentAndSetInfoHash generateMagnetUri: VideoMethods.GenerateMagnetUri - getVideoFilename: VideoMethods.GetVideoFilename - getThumbnailName: VideoMethods.GetThumbnailName getPreviewName: VideoMethods.GetPreviewName - getTorrentName: VideoMethods.GetTorrentName + getThumbnailName: VideoMethods.GetThumbnailName + getTorrentFileName: VideoMethods.GetTorrentFileName + getVideoFilename: VideoMethods.GetVideoFilename + getVideoFilePath: VideoMethods.GetVideoFilePath isOwned: VideoMethods.IsOwned - toFormatedJSON: VideoMethods.ToFormatedJSON + removeFile: VideoMethods.RemoveFile + removePreview: VideoMethods.RemovePreview + removeThumbnail: VideoMethods.RemoveThumbnail + removeTorrent: VideoMethods.RemoveTorrent toAddRemoteJSON: VideoMethods.ToAddRemoteJSON + toFormatedJSON: VideoMethods.ToFormatedJSON toUpdateRemoteJSON: VideoMethods.ToUpdateRemoteJSON transcodeVideofile: VideoMethods.TranscodeVideofile setTags: Sequelize.HasManySetAssociationsMixin + setVideoFiles: Sequelize.HasManySetAssociationsMixin } export interface VideoModel extends VideoClass, Sequelize.Model {} diff --git a/server/models/video/video.ts b/server/models/video/video.ts index b7eb24c4a..1e4bdf51c 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts @@ -2,13 +2,12 @@ import * as safeBuffer from 'safe-buffer' const Buffer = safeBuffer.Buffer import * as ffmpeg from 'fluent-ffmpeg' import * as magnetUtil from 'magnet-uri' -import { map, values } from 'lodash' +import { map } from 'lodash' import * as parseTorrent from 'parse-torrent' import { join } from 'path' import * as Sequelize from 'sequelize' import * as Promise from 'bluebird' -import { database as db } from '../../initializers/database' import { TagInstance } from './tag-interface' import { logger, @@ -18,7 +17,6 @@ import { isVideoLanguageValid, isVideoNSFWValid, isVideoDescriptionValid, - isVideoInfoHashValid, isVideoDurationValid, readFileBufferPromise, unlinkPromise, @@ -27,16 +25,17 @@ import { createTorrentPromise } from '../../helpers' import { - CONSTRAINTS_FIELDS, CONFIG, REMOTE_SCHEME, STATIC_PATHS, VIDEO_CATEGORIES, VIDEO_LICENCES, VIDEO_LANGUAGES, - THUMBNAILS_SIZE + THUMBNAILS_SIZE, + VIDEO_FILE_RESOLUTIONS } from '../../initializers' -import { JobScheduler, removeVideoToFriends } from '../../lib' +import { removeVideoToFriends } from '../../lib' +import { VideoFileInstance } from './video-file-interface' import { addMethodsToModel, getSort } from '../utils' import { @@ -51,12 +50,16 @@ let generateMagnetUri: VideoMethods.GenerateMagnetUri let getVideoFilename: VideoMethods.GetVideoFilename let getThumbnailName: VideoMethods.GetThumbnailName let getPreviewName: VideoMethods.GetPreviewName -let getTorrentName: VideoMethods.GetTorrentName +let getTorrentFileName: VideoMethods.GetTorrentFileName let isOwned: VideoMethods.IsOwned let toFormatedJSON: VideoMethods.ToFormatedJSON let toAddRemoteJSON: VideoMethods.ToAddRemoteJSON let toUpdateRemoteJSON: VideoMethods.ToUpdateRemoteJSON let transcodeVideofile: VideoMethods.TranscodeVideofile +let createPreview: VideoMethods.CreatePreview +let createThumbnail: VideoMethods.CreateThumbnail +let getVideoFilePath: VideoMethods.GetVideoFilePath +let createTorrentAndSetInfoHash: VideoMethods.CreateTorrentAndSetInfoHash let generateThumbnailFromData: VideoMethods.GenerateThumbnailFromData let getDurationFromFile: VideoMethods.GetDurationFromFile @@ -71,6 +74,10 @@ let loadAndPopulateAuthor: VideoMethods.LoadAndPopulateAuthor let loadAndPopulateAuthorAndPodAndTags: VideoMethods.LoadAndPopulateAuthorAndPodAndTags let loadByUUIDAndPopulateAuthorAndPodAndTags: VideoMethods.LoadByUUIDAndPopulateAuthorAndPodAndTags let searchAndPopulateAuthorAndPodAndTags: VideoMethods.SearchAndPopulateAuthorAndPodAndTags +let removeThumbnail: VideoMethods.RemoveThumbnail +let removePreview: VideoMethods.RemovePreview +let removeFile: VideoMethods.RemoveFile +let removeTorrent: VideoMethods.RemoveTorrent export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { Video = sequelize.define('Video', @@ -93,10 +100,6 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da } } }, - extname: { - type: DataTypes.ENUM(values(CONSTRAINTS_FIELDS.VIDEOS.EXTNAME)), - allowNull: false - }, category: { type: DataTypes.INTEGER, allowNull: false, @@ -148,16 +151,6 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da } } }, - infoHash: { - type: DataTypes.STRING, - allowNull: false, - validate: { - infoHashValid: value => { - const res = isVideoInfoHashValid(value) - if (res === false) throw new Error('Video info hash is not valid.') - } - } - }, duration: { type: DataTypes.INTEGER, allowNull: false, @@ -215,9 +208,6 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da { fields: [ 'duration' ] }, - { - fields: [ 'infoHash' ] - }, { fields: [ 'views' ] }, @@ -229,8 +219,6 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da } ], hooks: { - beforeValidate, - beforeCreate, afterDestroy } } @@ -246,23 +234,30 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da listOwnedAndPopulateAuthorAndTags, listOwnedByAuthor, load, - loadByUUID, - loadByHostAndUUID, loadAndPopulateAuthor, loadAndPopulateAuthorAndPodAndTags, + loadByHostAndUUID, + loadByUUID, loadByUUIDAndPopulateAuthorAndPodAndTags, - searchAndPopulateAuthorAndPodAndTags, - removeFromBlacklist + searchAndPopulateAuthorAndPodAndTags ] const instanceMethods = [ + createPreview, + createThumbnail, + createTorrentAndSetInfoHash, generateMagnetUri, - getVideoFilename, - getThumbnailName, getPreviewName, - getTorrentName, + getThumbnailName, + getTorrentFileName, + getVideoFilename, + getVideoFilePath, isOwned, - toFormatedJSON, + removeFile, + removePreview, + removeThumbnail, + removeTorrent, toAddRemoteJSON, + toFormatedJSON, toUpdateRemoteJSON, transcodeVideofile ] @@ -271,65 +266,6 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da return Video } -function beforeValidate (video: VideoInstance) { - // Put a fake infoHash if it does not exists yet - if (video.isOwned() && !video.infoHash) { - // 40 hexa length - video.infoHash = '0123456789abcdef0123456789abcdef01234567' - } -} - -function beforeCreate (video: VideoInstance, options: { transaction: Sequelize.Transaction }) { - if (video.isOwned()) { - const videoPath = join(CONFIG.STORAGE.VIDEOS_DIR, video.getVideoFilename()) - const tasks = [] - - tasks.push( - createTorrentFromVideo(video, videoPath), - createThumbnail(video, videoPath), - createPreview(video, videoPath) - ) - - if (CONFIG.TRANSCODING.ENABLED === true) { - // Put uuid because we don't have id auto incremented for now - const dataInput = { - videoUUID: video.uuid - } - - tasks.push( - JobScheduler.Instance.createJob(options.transaction, 'videoTranscoder', dataInput) - ) - } - - return Promise.all(tasks) - } - - return Promise.resolve() -} - -function afterDestroy (video: VideoInstance) { - const tasks = [] - - tasks.push( - removeThumbnail(video) - ) - - if (video.isOwned()) { - const removeVideoToFriendsParams = { - uuid: video.uuid - } - - tasks.push( - removeFile(video), - removeTorrent(video), - removePreview(video), - removeVideoToFriends(removeVideoToFriendsParams) - ) - } - - return Promise.all(tasks) -} - // ------------------------------ METHODS ------------------------------ function associate (models) { @@ -354,37 +290,46 @@ function associate (models) { }, onDelete: 'cascade' }) + + Video.hasMany(models.VideoFile, { + foreignKey: { + name: 'videoId', + allowNull: false + }, + onDelete: 'cascade' + }) } -generateMagnetUri = function (this: VideoInstance) { - let baseUrlHttp - let baseUrlWs +function afterDestroy (video: VideoInstance) { + const tasks = [] - if (this.isOwned()) { - baseUrlHttp = CONFIG.WEBSERVER.URL - baseUrlWs = CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT - } else { - baseUrlHttp = REMOTE_SCHEME.HTTP + '://' + this.Author.Pod.host - baseUrlWs = REMOTE_SCHEME.WS + '://' + this.Author.Pod.host - } + tasks.push( + video.removeThumbnail() + ) - const xs = baseUrlHttp + STATIC_PATHS.TORRENTS + this.getTorrentName() - const announce = [ baseUrlWs + '/tracker/socket' ] - const urlList = [ baseUrlHttp + STATIC_PATHS.WEBSEED + this.getVideoFilename() ] + if (video.isOwned()) { + const removeVideoToFriendsParams = { + uuid: video.uuid + } - const magnetHash = { - xs, - announce, - urlList, - infoHash: this.infoHash, - name: this.name + tasks.push( + video.removePreview(), + removeVideoToFriends(removeVideoToFriendsParams) + ) + + // TODO: check files is populated + video.VideoFiles.forEach(file => { + video.removeFile(file), + video.removeTorrent(file) + }) } - return magnetUtil.encode(magnetHash) + return Promise.all(tasks) } -getVideoFilename = function (this: VideoInstance) { - return this.uuid + this.extname +getVideoFilename = function (this: VideoInstance, videoFile: VideoFileInstance) { + // return this.uuid + '-' + VIDEO_FILE_RESOLUTIONS[videoFile.resolution] + videoFile.extname + return this.uuid + videoFile.extname } getThumbnailName = function (this: VideoInstance) { @@ -398,8 +343,9 @@ getPreviewName = function (this: VideoInstance) { return this.uuid + extension } -getTorrentName = function (this: VideoInstance) { +getTorrentFileName = function (this: VideoInstance, videoFile: VideoFileInstance) { const extension = '.torrent' + // return this.uuid + '-' + VIDEO_FILE_RESOLUTIONS[videoFile.resolution] + extension return this.uuid + extension } @@ -407,6 +353,67 @@ isOwned = function (this: VideoInstance) { return this.remote === false } +createPreview = function (this: VideoInstance, videoFile: VideoFileInstance) { + return generateImage(this, this.getVideoFilePath(videoFile), CONFIG.STORAGE.PREVIEWS_DIR, this.getPreviewName(), null) +} + +createThumbnail = function (this: VideoInstance, videoFile: VideoFileInstance) { + return generateImage(this, this.getVideoFilePath(videoFile), CONFIG.STORAGE.THUMBNAILS_DIR, this.getThumbnailName(), THUMBNAILS_SIZE) +} + +getVideoFilePath = function (this: VideoInstance, videoFile: VideoFileInstance) { + return join(CONFIG.STORAGE.VIDEOS_DIR, this.getVideoFilename(videoFile)) +} + +createTorrentAndSetInfoHash = function (this: VideoInstance, videoFile: VideoFileInstance) { + const options = { + announceList: [ + [ CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT + '/tracker/socket' ] + ], + urlList: [ + CONFIG.WEBSERVER.URL + STATIC_PATHS.WEBSEED + this.getVideoFilename(videoFile) + ] + } + + return createTorrentPromise(this.getVideoFilePath(videoFile), options) + .then(torrent => { + const filePath = join(CONFIG.STORAGE.TORRENTS_DIR, this.getTorrentFileName(videoFile)) + return writeFilePromise(filePath, torrent).then(() => torrent) + }) + .then(torrent => { + const parsedTorrent = parseTorrent(torrent) + + videoFile.infoHash = parsedTorrent.infoHash + }) +} + +generateMagnetUri = function (this: VideoInstance, videoFile: VideoFileInstance) { + let baseUrlHttp + let baseUrlWs + + if (this.isOwned()) { + baseUrlHttp = CONFIG.WEBSERVER.URL + baseUrlWs = CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT + } else { + baseUrlHttp = REMOTE_SCHEME.HTTP + '://' + this.Author.Pod.host + baseUrlWs = REMOTE_SCHEME.WS + '://' + this.Author.Pod.host + } + + const xs = baseUrlHttp + STATIC_PATHS.TORRENTS + this.getTorrentFileName(videoFile) + const announce = [ baseUrlWs + '/tracker/socket' ] + const urlList = [ baseUrlHttp + STATIC_PATHS.WEBSEED + this.getVideoFilename(videoFile) ] + + const magnetHash = { + xs, + announce, + urlList, + infoHash: videoFile.infoHash, + name: this.name + } + + return magnetUtil.encode(magnetHash) +} + toFormatedJSON = function (this: VideoInstance) { let podHost @@ -443,7 +450,6 @@ toFormatedJSON = function (this: VideoInstance) { description: this.description, podHost, isLocal: this.isOwned(), - magnetUri: this.generateMagnetUri(), author: this.Author.name, duration: this.duration, views: this.views, @@ -453,9 +459,24 @@ toFormatedJSON = function (this: VideoInstance) { thumbnailPath: join(STATIC_PATHS.THUMBNAILS, this.getThumbnailName()), previewPath: join(STATIC_PATHS.PREVIEWS, this.getPreviewName()), createdAt: this.createdAt, - updatedAt: this.updatedAt + updatedAt: this.updatedAt, + files: [] } + this.VideoFiles.forEach(videoFile => { + let resolutionLabel = VIDEO_FILE_RESOLUTIONS[videoFile.resolution] + if (!resolutionLabel) resolutionLabel = 'Unknown' + + const videoFileJson = { + resolution: videoFile.resolution, + resolutionLabel, + magnetUri: this.generateMagnetUri(videoFile), + size: videoFile.size + } + + json.files.push(videoFileJson) + }) + return json } @@ -472,19 +493,27 @@ toAddRemoteJSON = function (this: VideoInstance) { language: this.language, nsfw: this.nsfw, description: this.description, - infoHash: this.infoHash, author: this.Author.name, duration: this.duration, thumbnailData: thumbnailData.toString('binary'), tags: map(this.Tags, 'name'), createdAt: this.createdAt, updatedAt: this.updatedAt, - extname: this.extname, views: this.views, likes: this.likes, - dislikes: this.dislikes + dislikes: this.dislikes, + files: [] } + this.VideoFiles.forEach(videoFile => { + remoteVideo.files.push({ + infoHash: videoFile.infoHash, + resolution: videoFile.resolution, + extname: videoFile.extname, + size: videoFile.size + }) + }) + return remoteVideo }) } @@ -498,28 +527,34 @@ toUpdateRemoteJSON = function (this: VideoInstance) { language: this.language, nsfw: this.nsfw, description: this.description, - infoHash: this.infoHash, author: this.Author.name, duration: this.duration, tags: map(this.Tags, 'name'), createdAt: this.createdAt, updatedAt: this.updatedAt, - extname: this.extname, views: this.views, likes: this.likes, - dislikes: this.dislikes + dislikes: this.dislikes, + files: [] } + this.VideoFiles.forEach(videoFile => { + json.files.push({ + infoHash: videoFile.infoHash, + resolution: videoFile.resolution, + extname: videoFile.extname, + size: videoFile.size + }) + }) + return json } -transcodeVideofile = function (this: VideoInstance) { - const video = this - +transcodeVideofile = function (this: VideoInstance, inputVideoFile: VideoFileInstance) { const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR const newExtname = '.mp4' - const videoInputPath = join(videosDirectory, video.getVideoFilename()) - const videoOutputPath = join(videosDirectory, video.id + '-transcoded' + newExtname) + const videoInputPath = join(videosDirectory, this.getVideoFilename(inputVideoFile)) + const videoOutputPath = join(videosDirectory, this.id + '-transcoded' + newExtname) return new Promise((res, rej) => { ffmpeg(videoInputPath) @@ -533,24 +568,22 @@ transcodeVideofile = function (this: VideoInstance) { return unlinkPromise(videoInputPath) .then(() => { // Important to do this before getVideoFilename() to take in account the new file extension - video.set('extname', newExtname) + inputVideoFile.set('extname', newExtname) - const newVideoPath = join(videosDirectory, video.getVideoFilename()) - return renamePromise(videoOutputPath, newVideoPath) + return renamePromise(videoOutputPath, this.getVideoFilePath(inputVideoFile)) }) .then(() => { - const newVideoPath = join(videosDirectory, video.getVideoFilename()) - return createTorrentFromVideo(video, newVideoPath) + return this.createTorrentAndSetInfoHash(inputVideoFile) }) .then(() => { - return video.save() + return inputVideoFile.save() }) .then(() => { return res() }) .catch(err => { - // Autodesctruction... - video.destroy().catch(err => logger.error('Cannot destruct video after transcoding failure.', err)) + // Autodestruction... + this.destroy().catch(err => logger.error('Cannot destruct video after transcoding failure.', err)) return rej(err) }) @@ -559,6 +592,26 @@ transcodeVideofile = function (this: VideoInstance) { }) } +removeThumbnail = function (this: VideoInstance) { + const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, this.getThumbnailName()) + return unlinkPromise(thumbnailPath) +} + +removePreview = function (this: VideoInstance) { + // Same name than video thumbnail + return unlinkPromise(CONFIG.STORAGE.PREVIEWS_DIR + this.getPreviewName()) +} + +removeFile = function (this: VideoInstance, videoFile: VideoFileInstance) { + const filePath = join(CONFIG.STORAGE.VIDEOS_DIR, this.getVideoFilename(videoFile)) + return unlinkPromise(filePath) +} + +removeTorrent = function (this: VideoInstance, videoFile: VideoFileInstance) { + const torrenPath = join(CONFIG.STORAGE.TORRENTS_DIR, this.getTorrentFileName(videoFile)) + return unlinkPromise(torrenPath) +} + // ------------------------------ STATICS ------------------------------ generateThumbnailFromData = function (video: VideoInstance, thumbnailData: string) { @@ -582,7 +635,11 @@ getDurationFromFile = function (videoPath: string) { } list = function () { - return Video.findAll() + const query = { + include: [ Video['sequelize'].models.VideoFile ] + } + + return Video.findAll(query) } listForApi = function (start: number, count: number, sort: string) { @@ -597,8 +654,8 @@ listForApi = function (start: number, count: number, sort: string) { model: Video['sequelize'].models.Author, include: [ { model: Video['sequelize'].models.Pod, required: false } ] }, - - Video['sequelize'].models.Tag + Video['sequelize'].models.Tag, + Video['sequelize'].models.VideoFile ], where: createBaseVideosWhere() } @@ -617,6 +674,9 @@ loadByHostAndUUID = function (fromHost: string, uuid: string) { uuid }, include: [ + { + model: Video['sequelize'].models.VideoFile + }, { model: Video['sequelize'].models.Author, include: [ @@ -640,7 +700,11 @@ listOwnedAndPopulateAuthorAndTags = function () { where: { remote: false }, - include: [ Video['sequelize'].models.Author, Video['sequelize'].models.Tag ] + include: [ + Video['sequelize'].models.VideoFile, + Video['sequelize'].models.Author, + Video['sequelize'].models.Tag + ] } return Video.findAll(query) @@ -652,6 +716,9 @@ listOwnedByAuthor = function (author: string) { remote: false }, include: [ + { + model: Video['sequelize'].models.VideoFile + }, { model: Video['sequelize'].models.Author, where: { @@ -672,14 +739,15 @@ loadByUUID = function (uuid: string) { const query = { where: { uuid - } + }, + include: [ Video['sequelize'].models.VideoFile ] } return Video.findOne(query) } loadAndPopulateAuthor = function (id: number) { const options = { - include: [ Video['sequelize'].models.Author ] + include: [ Video['sequelize'].models.VideoFile, Video['sequelize'].models.Author ] } return Video.findById(id, options) @@ -692,7 +760,8 @@ loadAndPopulateAuthorAndPodAndTags = function (id: number) { model: Video['sequelize'].models.Author, include: [ { model: Video['sequelize'].models.Pod, required: false } ] }, - Video['sequelize'].models.Tag + Video['sequelize'].models.Tag, + Video['sequelize'].models.VideoFile ] } @@ -709,7 +778,8 @@ loadByUUIDAndPopulateAuthorAndPodAndTags = function (uuid: string) { model: Video['sequelize'].models.Author, include: [ { model: Video['sequelize'].models.Pod, required: false } ] }, - Video['sequelize'].models.Tag + Video['sequelize'].models.Tag, + Video['sequelize'].models.VideoFile ] } @@ -733,6 +803,10 @@ searchAndPopulateAuthorAndPodAndTags = function (value: string, field: string, s model: Video['sequelize'].models.Tag } + const videoFileInclude: Sequelize.IncludeOptions = { + model: Video['sequelize'].models.VideoFile + } + const query: Sequelize.FindOptions = { distinct: true, where: createBaseVideosWhere(), @@ -743,8 +817,9 @@ searchAndPopulateAuthorAndPodAndTags = function (value: string, field: string, s // Make an exact search with the magnet if (field === 'magnetUri') { - const infoHash = magnetUtil.decode(value).infoHash - query.where['infoHash'] = infoHash + videoFileInclude.where = { + infoHash: magnetUtil.decode(value).infoHash + } } else if (field === 'tags') { const escapedValue = Video['sequelize'].escape('%' + value + '%') query.where['id'].$in = Video['sequelize'].literal( @@ -777,7 +852,7 @@ searchAndPopulateAuthorAndPodAndTags = function (value: string, field: string, s } query.include = [ - authorInclude, tagInclude + authorInclude, tagInclude, videoFileInclude ] return Video.findAndCountAll(query).then(({ rows, count }) => { @@ -800,56 +875,6 @@ function createBaseVideosWhere () { } } -function removeThumbnail (video: VideoInstance) { - const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, video.getThumbnailName()) - return unlinkPromise(thumbnailPath) -} - -function removeFile (video: VideoInstance) { - const filePath = join(CONFIG.STORAGE.VIDEOS_DIR, video.getVideoFilename()) - return unlinkPromise(filePath) -} - -function removeTorrent (video: VideoInstance) { - const torrenPath = join(CONFIG.STORAGE.TORRENTS_DIR, video.getTorrentName()) - return unlinkPromise(torrenPath) -} - -function removePreview (video: VideoInstance) { - // Same name than video thumnail - return unlinkPromise(CONFIG.STORAGE.PREVIEWS_DIR + video.getPreviewName()) -} - -function createTorrentFromVideo (video: VideoInstance, videoPath: string) { - const options = { - announceList: [ - [ CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT + '/tracker/socket' ] - ], - urlList: [ - CONFIG.WEBSERVER.URL + STATIC_PATHS.WEBSEED + video.getVideoFilename() - ] - } - - return createTorrentPromise(videoPath, options) - .then(torrent => { - const filePath = join(CONFIG.STORAGE.TORRENTS_DIR, video.getTorrentName()) - return writeFilePromise(filePath, torrent).then(() => torrent) - }) - .then(torrent => { - const parsedTorrent = parseTorrent(torrent) - video.set('infoHash', parsedTorrent.infoHash) - return video.validate() - }) -} - -function createPreview (video: VideoInstance, videoPath: string) { - return generateImage(video, videoPath, CONFIG.STORAGE.PREVIEWS_DIR, video.getPreviewName(), null) -} - -function createThumbnail (video: VideoInstance, videoPath: string) { - return generateImage(video, videoPath, CONFIG.STORAGE.THUMBNAILS_DIR, video.getThumbnailName(), THUMBNAILS_SIZE) -} - function generateImage (video: VideoInstance, videoPath: string, folder: string, imageName: string, size: string) { const options = { filename: imageName, @@ -868,16 +893,3 @@ function generateImage (video: VideoInstance, videoPath: string, folder: string, .thumbnail(options) }) } - -function removeFromBlacklist (video: VideoInstance) { - // Find the blacklisted video - return db.BlacklistedVideo.loadByVideoId(video.id).then(video => { - // Not found the video, skip - if (!video) { - return null - } - - // If we found the video, remove it from the blacklist - return video.destroy() - }) -} diff --git a/server/tests/api/multiple-pods.js b/server/tests/api/multiple-pods.js index abbc2caf4..b281cc249 100644 --- a/server/tests/api/multiple-pods.js +++ b/server/tests/api/multiple-pods.js @@ -121,13 +121,21 @@ describe('Test multiple pods', function () { expect(video.nsfw).to.be.ok expect(video.description).to.equal('my super description for pod 1') expect(video.podHost).to.equal('localhost:9001') - expect(video.magnetUri).to.exist expect(video.duration).to.equal(10) expect(video.tags).to.deep.equal([ 'tag1p1', 'tag2p1' ]) expect(miscsUtils.dateIsValid(video.createdAt)).to.be.true expect(miscsUtils.dateIsValid(video.updatedAt)).to.be.true expect(video.author).to.equal('root') + expect(video.files).to.have.lengthOf(1) + + const file = video.files[0] + const magnetUri = file.magnetUri + expect(file.magnetUri).to.exist + expect(file.resolution).to.equal(0) + expect(file.resolutionLabel).to.equal('original') + expect(file.size).to.equal(572456) + if (server.url !== 'http://localhost:9001') { expect(video.isLocal).to.be.false } else { @@ -136,9 +144,9 @@ describe('Test multiple pods', function () { // All pods should have the same magnet Uri if (baseMagnet === null) { - baseMagnet = video.magnetUri + baseMagnet = magnetUri } else { - expect(video.magnetUri).to.equal.magnetUri + expect(baseMagnet).to.equal(magnetUri) } videosUtils.testVideoImage(server.url, 'video_short1.webm', video.thumbnailPath, function (err, test) { @@ -198,13 +206,21 @@ describe('Test multiple pods', function () { expect(video.nsfw).to.be.true expect(video.description).to.equal('my super description for pod 2') expect(video.podHost).to.equal('localhost:9002') - expect(video.magnetUri).to.exist expect(video.duration).to.equal(5) expect(video.tags).to.deep.equal([ 'tag1p2', 'tag2p2', 'tag3p2' ]) expect(miscsUtils.dateIsValid(video.createdAt)).to.be.true expect(miscsUtils.dateIsValid(video.updatedAt)).to.be.true expect(video.author).to.equal('root') + expect(video.files).to.have.lengthOf(1) + + const file = video.files[0] + const magnetUri = file.magnetUri + expect(file.magnetUri).to.exist + expect(file.resolution).to.equal(0) + expect(file.resolutionLabel).to.equal('original') + expect(file.size).to.equal(942961) + if (server.url !== 'http://localhost:9002') { expect(video.isLocal).to.be.false } else { @@ -213,9 +229,9 @@ describe('Test multiple pods', function () { // All pods should have the same magnet Uri if (baseMagnet === null) { - baseMagnet = video.magnetUri + baseMagnet = magnetUri } else { - expect(video.magnetUri).to.equal.magnetUri + expect(baseMagnet).to.equal(magnetUri) } videosUtils.testVideoImage(server.url, 'video_short2.webm', video.thumbnailPath, function (err, test) { @@ -297,13 +313,21 @@ describe('Test multiple pods', function () { expect(video1.nsfw).to.be.ok expect(video1.description).to.equal('my super description for pod 3') expect(video1.podHost).to.equal('localhost:9003') - expect(video1.magnetUri).to.exist expect(video1.duration).to.equal(5) expect(video1.tags).to.deep.equal([ 'tag1p3' ]) expect(video1.author).to.equal('root') expect(miscsUtils.dateIsValid(video1.createdAt)).to.be.true expect(miscsUtils.dateIsValid(video1.updatedAt)).to.be.true + expect(video1.files).to.have.lengthOf(1) + + const file1 = video1.files[0] + const magnetUri1 = file1.magnetUri + expect(file1.magnetUri).to.exist + expect(file1.resolution).to.equal(0) + expect(file1.resolutionLabel).to.equal('original') + expect(file1.size).to.equal(292677) + expect(video2.name).to.equal('my super name for pod 3-2') expect(video2.category).to.equal(7) expect(video2.categoryLabel).to.equal('Gaming') @@ -314,13 +338,21 @@ describe('Test multiple pods', function () { expect(video2.nsfw).to.be.false expect(video2.description).to.equal('my super description for pod 3-2') expect(video2.podHost).to.equal('localhost:9003') - expect(video2.magnetUri).to.exist expect(video2.duration).to.equal(5) expect(video2.tags).to.deep.equal([ 'tag2p3', 'tag3p3', 'tag4p3' ]) expect(video2.author).to.equal('root') expect(miscsUtils.dateIsValid(video2.createdAt)).to.be.true expect(miscsUtils.dateIsValid(video2.updatedAt)).to.be.true + expect(video2.files).to.have.lengthOf(1) + + const file2 = video2.files[0] + const magnetUri2 = file2.magnetUri + expect(file2.magnetUri).to.exist + expect(file2.resolution).to.equal(0) + expect(file2.resolutionLabel).to.equal('original') + expect(file2.size).to.equal(218910) + if (server.url !== 'http://localhost:9003') { expect(video1.isLocal).to.be.false expect(video2.isLocal).to.be.false @@ -331,9 +363,9 @@ describe('Test multiple pods', function () { // All pods should have the same magnet Uri if (baseMagnet === null) { - baseMagnet = video2.magnetUri + baseMagnet = magnetUri2 } else { - expect(video2.magnetUri).to.equal.magnetUri + expect(baseMagnet).to.equal(magnetUri2) } videosUtils.testVideoImage(server.url, 'video_short3.webm', video1.thumbnailPath, function (err, test) { @@ -366,7 +398,7 @@ describe('Test multiple pods', function () { toRemove.push(res.body.data[2]) toRemove.push(res.body.data[3]) - webtorrent.add(video.magnetUri, function (torrent) { + webtorrent.add(video.files[0].magnetUri, function (torrent) { expect(torrent.files).to.exist expect(torrent.files.length).to.equal(1) expect(torrent.files[0].path).to.exist.and.to.not.equal('') @@ -385,7 +417,7 @@ describe('Test multiple pods', function () { const video = res.body.data[1] - webtorrent.add(video.magnetUri, function (torrent) { + webtorrent.add(video.files[0].magnetUri, function (torrent) { expect(torrent.files).to.exist expect(torrent.files.length).to.equal(1) expect(torrent.files[0].path).to.exist.and.to.not.equal('') @@ -404,7 +436,7 @@ describe('Test multiple pods', function () { const video = res.body.data[2] - webtorrent.add(video.magnetUri, function (torrent) { + webtorrent.add(video.files[0].magnetUri, function (torrent) { expect(torrent.files).to.exist expect(torrent.files.length).to.equal(1) expect(torrent.files[0].path).to.exist.and.to.not.equal('') @@ -423,7 +455,7 @@ describe('Test multiple pods', function () { const video = res.body.data[3] - webtorrent.add(video.magnetUri, function (torrent) { + webtorrent.add(video.files[0].magnetUri, function (torrent) { expect(torrent.files).to.exist expect(torrent.files.length).to.equal(1) expect(torrent.files[0].path).to.exist.and.to.not.equal('') @@ -700,11 +732,18 @@ describe('Test multiple pods', function () { expect(videoUpdated.tags).to.deep.equal([ 'tagup1', 'tagup2' ]) expect(miscsUtils.dateIsValid(videoUpdated.updatedAt, 20000)).to.be.true + const file = videoUpdated.files[0] + const magnetUri = file.magnetUri + expect(file.magnetUri).to.exist + expect(file.resolution).to.equal(0) + expect(file.resolutionLabel).to.equal('original') + expect(file.size).to.equal(292677) + videosUtils.testVideoImage(server.url, 'video_short3.webm', videoUpdated.thumbnailPath, function (err, test) { if (err) throw err expect(test).to.equal(true) - webtorrent.add(videoUpdated.magnetUri, function (torrent) { + webtorrent.add(videoUpdated.files[0].magnetUri, function (torrent) { expect(torrent.files).to.exist expect(torrent.files.length).to.equal(1) expect(torrent.files[0].path).to.exist.and.to.not.equal('') diff --git a/server/tests/api/single-pod.js b/server/tests/api/single-pod.js index 1258e7e55..6933d18dd 100644 --- a/server/tests/api/single-pod.js +++ b/server/tests/api/single-pod.js @@ -129,13 +129,21 @@ describe('Test a single pod', function () { expect(video.nsfw).to.be.ok expect(video.description).to.equal('my super description') expect(video.podHost).to.equal('localhost:9001') - expect(video.magnetUri).to.exist expect(video.author).to.equal('root') expect(video.isLocal).to.be.true expect(video.tags).to.deep.equal([ 'tag1', 'tag2', 'tag3' ]) expect(miscsUtils.dateIsValid(video.createdAt)).to.be.true expect(miscsUtils.dateIsValid(video.updatedAt)).to.be.true + expect(video.files).to.have.lengthOf(1) + + const file = video.files[0] + const magnetUri = file.magnetUri + expect(file.magnetUri).to.exist + expect(file.resolution).to.equal(0) + expect(file.resolutionLabel).to.equal('original') + expect(file.size).to.equal(218910) + videosUtils.testVideoImage(server.url, 'video_short.webm', video.thumbnailPath, function (err, test) { if (err) throw err expect(test).to.equal(true) @@ -143,7 +151,7 @@ describe('Test a single pod', function () { videoId = video.id videoUUID = video.uuid - webtorrent.add(video.magnetUri, function (torrent) { + webtorrent.add(magnetUri, function (torrent) { expect(torrent.files).to.exist expect(torrent.files.length).to.equal(1) expect(torrent.files[0].path).to.exist.and.to.not.equal('') @@ -172,13 +180,21 @@ describe('Test a single pod', function () { expect(video.nsfw).to.be.ok expect(video.description).to.equal('my super description') expect(video.podHost).to.equal('localhost:9001') - expect(video.magnetUri).to.exist expect(video.author).to.equal('root') expect(video.isLocal).to.be.true expect(video.tags).to.deep.equal([ 'tag1', 'tag2', 'tag3' ]) expect(miscsUtils.dateIsValid(video.createdAt)).to.be.true expect(miscsUtils.dateIsValid(video.updatedAt)).to.be.true + expect(video.files).to.have.lengthOf(1) + + const file = video.files[0] + const magnetUri = file.magnetUri + expect(file.magnetUri).to.exist + expect(file.resolution).to.equal(0) + expect(file.resolutionLabel).to.equal('original') + expect(file.size).to.equal(218910) + videosUtils.testVideoImage(server.url, 'video_short.webm', video.thumbnailPath, function (err, test) { if (err) throw err expect(test).to.equal(true) @@ -240,6 +256,15 @@ describe('Test a single pod', function () { expect(miscsUtils.dateIsValid(video.createdAt)).to.be.true expect(miscsUtils.dateIsValid(video.updatedAt)).to.be.true + expect(video.files).to.have.lengthOf(1) + + const file = video.files[0] + const magnetUri = file.magnetUri + expect(file.magnetUri).to.exist + expect(file.resolution).to.equal(0) + expect(file.resolutionLabel).to.equal('original') + expect(file.size).to.equal(218910) + videosUtils.testVideoImage(server.url, 'video_short.webm', video.thumbnailPath, function (err, test) { if (err) throw err expect(test).to.equal(true) @@ -302,6 +327,15 @@ describe('Test a single pod', function () { expect(miscsUtils.dateIsValid(video.createdAt)).to.be.true expect(miscsUtils.dateIsValid(video.updatedAt)).to.be.true + expect(video.files).to.have.lengthOf(1) + + const file = video.files[0] + const magnetUri = file.magnetUri + expect(file.magnetUri).to.exist + expect(file.resolution).to.equal(0) + expect(file.resolutionLabel).to.equal('original') + expect(file.size).to.equal(218910) + videosUtils.testVideoImage(server.url, 'video_short.webm', video.thumbnailPath, function (err, test) { if (err) throw err expect(test).to.equal(true) @@ -564,7 +598,7 @@ describe('Test a single pod', function () { it('Should search the right magnetUri video', function (done) { const video = videosListBase[0] - videosUtils.searchVideoWithPagination(server.url, encodeURIComponent(video.magnetUri), 'magnetUri', 0, 15, function (err, res) { + videosUtils.searchVideoWithPagination(server.url, encodeURIComponent(video.files[0].magnetUri), 'magnetUri', 0, 15, function (err, res) { if (err) throw err const videos = res.body.data @@ -650,11 +684,20 @@ describe('Test a single pod', function () { expect(miscsUtils.dateIsValid(video.createdAt)).to.be.true expect(miscsUtils.dateIsValid(video.updatedAt)).to.be.true + expect(video.files).to.have.lengthOf(1) + + const file = video.files[0] + const magnetUri = file.magnetUri + expect(file.magnetUri).to.exist + expect(file.resolution).to.equal(0) + expect(file.resolutionLabel).to.equal('original') + expect(file.size).to.equal(292677) + videosUtils.testVideoImage(server.url, 'video_short3.webm', video.thumbnailPath, function (err, test) { if (err) throw err expect(test).to.equal(true) - webtorrent.add(video.magnetUri, function (torrent) { + webtorrent.add(magnetUri, function (torrent) { expect(torrent.files).to.exist expect(torrent.files.length).to.equal(1) expect(torrent.files[0].path).to.exist.and.to.not.equal('') @@ -694,6 +737,15 @@ describe('Test a single pod', function () { expect(miscsUtils.dateIsValid(video.createdAt)).to.be.true expect(miscsUtils.dateIsValid(video.updatedAt)).to.be.true + expect(video.files).to.have.lengthOf(1) + + const file = video.files[0] + const magnetUri = file.magnetUri + expect(file.magnetUri).to.exist + expect(file.resolution).to.equal(0) + expect(file.resolutionLabel).to.equal('original') + expect(file.size).to.equal(292677) + done() }) }) @@ -728,6 +780,15 @@ describe('Test a single pod', function () { expect(miscsUtils.dateIsValid(video.createdAt)).to.be.true expect(miscsUtils.dateIsValid(video.updatedAt)).to.be.true + expect(video.files).to.have.lengthOf(1) + + const file = video.files[0] + const magnetUri = file.magnetUri + expect(file.magnetUri).to.exist + expect(file.resolution).to.equal(0) + expect(file.resolutionLabel).to.equal('original') + expect(file.size).to.equal(292677) + done() }) }) diff --git a/server/tests/api/video-transcoder.js b/server/tests/api/video-transcoder.js index c0b597668..c7af3cf11 100644 --- a/server/tests/api/video-transcoder.js +++ b/server/tests/api/video-transcoder.js @@ -56,9 +56,10 @@ describe('Test video transcoding', function () { if (err) throw err const video = res.body.data[0] - expect(video.magnetUri).to.match(/\.webm/) + const magnetUri = video.files[0].magnetUri + expect(magnetUri).to.match(/\.webm/) - webtorrent.add(video.magnetUri, function (torrent) { + webtorrent.add(magnetUri, function (torrent) { expect(torrent.files).to.exist expect(torrent.files.length).to.equal(1) expect(torrent.files[0].path).match(/\.webm$/) @@ -86,9 +87,10 @@ describe('Test video transcoding', function () { if (err) throw err const video = res.body.data[0] - expect(video.magnetUri).to.match(/\.mp4/) + const magnetUri = video.files[0].magnetUri + expect(magnetUri).to.match(/\.mp4/) - webtorrent.add(video.magnetUri, function (torrent) { + webtorrent.add(magnetUri, function (torrent) { expect(torrent.files).to.exist expect(torrent.files.length).to.equal(1) expect(torrent.files[0].path).match(/\.mp4$/) diff --git a/shared/models/pods/remote-video/remote-video-create-request.model.ts b/shared/models/pods/remote-video/remote-video-create-request.model.ts index b6a570e42..98425e4d9 100644 --- a/shared/models/pods/remote-video/remote-video-create-request.model.ts +++ b/shared/models/pods/remote-video/remote-video-create-request.model.ts @@ -5,8 +5,6 @@ export interface RemoteVideoCreateData { author: string tags: string[] name: string - extname: string - infoHash: string category: number licence: number language: number @@ -19,6 +17,12 @@ export interface RemoteVideoCreateData { likes: number dislikes: number thumbnailData: string + files: { + infoHash: string + extname: string + resolution: number + size: number + }[] } export interface RemoteVideoCreateRequest extends RemoteVideoRequest { diff --git a/shared/models/pods/remote-video/remote-video-update-request.model.ts b/shared/models/pods/remote-video/remote-video-update-request.model.ts index 805548563..dd3e2ae1a 100644 --- a/shared/models/pods/remote-video/remote-video-update-request.model.ts +++ b/shared/models/pods/remote-video/remote-video-update-request.model.ts @@ -15,6 +15,12 @@ export interface RemoteVideoUpdateData { views: number likes: number dislikes: number + files: { + infoHash: string + extname: string + resolution: number + size: number + }[] } export interface RemoteVideoUpdateRequest { diff --git a/shared/models/videos/video.model.ts b/shared/models/videos/video.model.ts index 8aa8ee448..82c8763d0 100644 --- a/shared/models/videos/video.model.ts +++ b/shared/models/videos/video.model.ts @@ -1,3 +1,10 @@ +export interface VideoFile { + magnetUri: string + resolution: number + resolutionLabel: string + size: number // Bytes +} + export interface Video { id: number uuid: string @@ -12,7 +19,6 @@ export interface Video { description: string duration: number isLocal: boolean - magnetUri: string name: string podHost: string tags: string[] @@ -22,4 +28,5 @@ export interface Video { likes: number dislikes: number nsfw: boolean + files: VideoFile[] } -- 2.25.1