Move video file metadata in their own table
authorChocobozzz <florian.bigard@gmail.com>
Fri, 25 Aug 2017 09:36:23 +0000 (11:36 +0200)
committerChocobozzz <florian.bigard@gmail.com>
Fri, 25 Aug 2017 09:36:23 +0000 (11:36 +0200)
Will be used for user video quotas and multiple video resolutions

30 files changed:
.gitignore
client/src/app/videos/shared/video.model.ts
client/src/app/videos/video-watch/video-magnet.component.html
client/src/app/videos/video-watch/video-watch.component.ts
client/src/standalone/videos/embed.ts
package.json
scripts/dev/index.sh [new file with mode: 0755]
scripts/update-host.ts
server.ts
server/controllers/api/remote/videos.ts
server/controllers/api/videos/index.ts
server/helpers/custom-validators/remote/videos.ts
server/helpers/custom-validators/videos.ts
server/initializers/constants.ts
server/initializers/database.ts
server/initializers/migrations/0060-video-file.ts [new file with mode: 0644]
server/initializers/migrations/0065-video-file-size.ts [new file with mode: 0644]
server/initializers/migrator.ts
server/lib/jobs/handlers/video-transcoder.ts
server/models/video/index.ts
server/models/video/video-file-interface.ts [new file with mode: 0644]
server/models/video/video-file.ts [new file with mode: 0644]
server/models/video/video-interface.ts
server/models/video/video.ts
server/tests/api/multiple-pods.js
server/tests/api/single-pod.js
server/tests/api/video-transcoder.js
shared/models/pods/remote-video/remote-video-create-request.model.ts
shared/models/pods/remote-video/remote-video-update-request.model.ts
shared/models/videos/video.model.ts

index 42d4f59269de7e3da430bb1fdf3de4e0b4deb9d3..b4c1de87dae1e996fc89f0a04d9cf5e2c4f60b36 100644 (file)
@@ -19,3 +19,4 @@
 /*.sublime-workspace
 /dist
 /.idea
+/PeerTube.iml
index f0556343f5045c2ac70b5be795804c3de5e2800a..438791368d3f77e427493d3522f3da4761310f26 100644 (file)
@@ -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
     }
   }
 }
index 3fa82f1be3da775512e7fc6cf340d7760c24fedf..5b0324e37926d1aa653b3c7a6a0f93f241c4df2a 100644 (file)
@@ -10,7 +10,7 @@
       </div>
 
       <div class="modal-body">
-        <input #magnetUriInput (click)="magnetUriInput.select()" type="text" class="form-control input-sm readonly" readonly [value]="video.magnetUri" />
+        <input #magnetUriInput (click)="magnetUriInput.select()" type="text" class="form-control input-sm readonly" readonly [value]="video.getDefaultMagnetUri()" />
       </div>
     </div>
   </div>
index cd11aa33c508f0fb4e198509fc5ab1c8aff09173..25575769214e9ce4b2c0b0dd52dda2097b8a6946 100644 (file)
@@ -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.')
index 64a0f0798d4a06b5b1c4dce77a5c7ebc30337dc3..0698344b014023afdf341032efcb5c1e3e2b6403 100644 (file)
@@ -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
index d6da61975521f1d08f84812d19af1b81f35f604c..9478af8fab8a85ab99b2529457a5fce00bc628ba 100644 (file)
@@ -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 (executable)
index 0000000..938bf60
--- /dev/null
@@ -0,0 +1,5 @@
+#!/usr/bin/env sh
+
+NODE_ENV=test concurrently -k \
+  "npm run watch:client" \
+  "npm run watch:server"
index 23e8d5ef35ca7234ba723b044885be4cfa551e13..5e69e4172bd9629b537605abce809b8f602ee026 100755 (executable)
@@ -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)
index 1ba208c280b38ffc3940044387074b3d19530724..2effa93402af345e20e8e410fa3cebb2b3b3931e 100644 (file)
--- 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
index 30771d8c4f01b6147d2dae2160b065fc6da8eed4..e7edff6061948315a0bc1413f9b8b7f6a410e5f5 100644 (file)
@@ -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 }
 
index 815881df38b9cc3184e475f31e1d0ecc20830552..d71a132ed05d40742d244ebdd3a1b249fd119914 100644 (file)
@@ -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
   })
 }
index b33d8c9bee3913f14481ca10ae291b04d7f4a7d8..091cd2186b6e263d9e51a5484cad15e8d4330f24 100644 (file)
@@ -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) {
index 62132acb12b5bb389ac9df1c67027dad9bb630fb..139fa760f34f7bed6d5e2e0e462d4d0a60130d62 100644 (file)
@@ -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
     }
   }
 }
index 314a05ab7a0241a021db3c75ed51414259ba6a2d..50a939083c7db721ddad6ee2032b849fed4f0cb6 100644 (file)
@@ -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
index 9e691bf1d5af3f2ec9b9c387447b15da50f58c0a..c0df2b63a15d50c52c3f1e68aa8299243c2ec155 100644 (file)
@@ -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 (file)
index 0000000..c362cf7
--- /dev/null
@@ -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<void> {
+  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 (file)
index 0000000..58f8f3b
--- /dev/null
@@ -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<void> {
+  return utils.db.Video.listOwnedAndPopulateAuthorAndTags()
+    .then((videos: VideoInstance[]) => {
+      const tasks: Promise<any>[] = []
+
+      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<any>[]) => {
+      return Promise.all(tasks)
+    })
+}
+
+function down (options) {
+  throw new Error('Not implemented.')
+}
+
+export {
+  up,
+  down
+}
index 71a656c59b74d65d6127d018907c9c52365e8584..7b535aea956e42febb0c29d707ca9d0398e8eee9 100644 (file)
@@ -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)
index 0d32dfd2fcc043de2109601888ff987b5b76d0cc..87d8ffa6a6467264998a4178e7767feb6769a0f6 100644 (file)
@@ -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)
   })
 }
 
index 84b801c726e9a730d4bbe93d648a94bd229d896b..08b360376c6d832d4aff684c282ae86f56e2224c 100644 (file)
@@ -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 (file)
index 0000000..c9fb8b8
--- /dev/null
@@ -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<VideoFileAttributes> {
+  id: number
+  createdAt: Date
+  updatedAt: Date
+}
+
+export interface VideoFileModel extends VideoFileClass, Sequelize.Model<VideoFileInstance, VideoFileAttributes> {}
diff --git a/server/models/video/video-file.ts b/server/models/video/video-file.ts
new file mode 100644 (file)
index 0000000..09a30d7
--- /dev/null
@@ -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<VideoFileInstance, VideoFileAttributes>
+
+export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) {
+  VideoFile = sequelize.define<VideoFileInstance, VideoFileAttributes>('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 ------------------------------
index 2fabcd9065111b94ba71bcffa52020158ae4c39b..976c70b5e3660e7a6b3cf7e81082e630c2d6422a 100644 (file)
@@ -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<string>
+  export type CreateThumbnail = (this: VideoInstance, videoFile: VideoFileInstance) => Promise<string>
+  export type GetVideoFilePath = (this: VideoInstance, videoFile: VideoFileInstance) => string
+  export type CreateTorrentAndSetInfoHash = (this: VideoInstance, videoFile: VideoFileInstance) => Promise<void>
+
   export type ToAddRemoteJSON = (this: VideoInstance) => Promise<FormatedAddRemoteVideo>
   export type ToUpdateRemoteJSON = (this: VideoInstance) => FormatedUpdateRemoteVideo
 
-  export type TranscodeVideofile = (this: VideoInstance) => Promise<void>
+  export type TranscodeVideofile = (this: VideoInstance, inputVideoFile: VideoFileInstance) => Promise<void>
 
   // Return thumbnail name
   export type GenerateThumbnailFromData = (video: VideoInstance, thumbnailData: string) => Promise<string>
@@ -86,31 +97,25 @@ export namespace VideoMethods {
   export type LoadAndPopulateAuthor = (id: number) => Promise<VideoInstance>
   export type LoadAndPopulateAuthorAndPodAndTags = (id: number) => Promise<VideoInstance>
   export type LoadByUUIDAndPopulateAuthorAndPodAndTags = (uuid: string) => Promise<VideoInstance>
+
+  export type RemoveThumbnail = (this: VideoInstance) => Promise<void>
+  export type RemovePreview = (this: VideoInstance) => Promise<void>
+  export type RemoveFile = (this: VideoInstance, videoFile: VideoFileInstance) => Promise<void>
+  export type RemoveTorrent = (this: VideoInstance, videoFile: VideoFileInstance) => Promise<void>
 }
 
 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<VideoAttributes> {
@@ -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<TagAttributes, string>
+  setVideoFiles: Sequelize.HasManySetAssociationsMixin<VideoFileAttributes, string>
 }
 
 export interface VideoModel extends VideoClass, Sequelize.Model<VideoInstance, VideoAttributes> {}
index b7eb24c4a0f38559e25c49c280f569e1236506e8..1e4bdf51c9e8fdd698cf8666b8c49a086dd7ca58 100644 (file)
@@ -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<VideoInstance, VideoAttributes>('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<TagInstance, string>(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<TagInstance, string>(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<void>((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()
-  })
-}
index abbc2caf4765b5006b34f8a3ce703680f1ca1ecf..b281cc2492f559bafb1adb7c4dbf5eb5bd649aa6 100644 (file)
@@ -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('')
index 1258e7e5563f3a673f597295c948b051de72872a..6933d18ddd43d73d89f4427ecee55db09ea57220 100644 (file)
@@ -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()
       })
     })
index c0b59766897323081f1472039f52d00ad27ffcf3..c7af3cf11960b116f23be55c1dcd0781531443ba 100644 (file)
@@ -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$/)
index b6a570e4298499c2b4bd2421a0e2973333d56686..98425e4d9d91c6b6acd504b67d3d76c32a31515c 100644 (file)
@@ -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 {
index 80554856342e9cdda5ad465edd40f391039873b5..dd3e2ae1a2eb837bacfa8c2dbd5075e7ffe4ac07 100644 (file)
@@ -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 {
index 8aa8ee448b953faa0b58fa32369fa4454deed651..82c8763d0ec8767aa052a3ac3f87e897c0ef3779 100644 (file)
@@ -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[]
 }