aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorChocobozzz <florian.bigard@gmail.com>2017-08-25 11:36:23 +0200
committerChocobozzz <florian.bigard@gmail.com>2017-08-25 11:36:23 +0200
commit93e1258c7cbc0d1235ca6d2a1f7c1875985328b8 (patch)
treeb0a1f77af7ab54dc5f58f569fcd1e9d84b04c533
parent69f224587e99d56008e1fa129d0641840a486620 (diff)
downloadPeerTube-93e1258c7cbc0d1235ca6d2a1f7c1875985328b8.tar.gz
PeerTube-93e1258c7cbc0d1235ca6d2a1f7c1875985328b8.tar.zst
PeerTube-93e1258c7cbc0d1235ca6d2a1f7c1875985328b8.zip
Move video file metadata in their own table
Will be used for user video quotas and multiple video resolutions
-rw-r--r--.gitignore1
-rw-r--r--client/src/app/videos/shared/video.model.ts21
-rw-r--r--client/src/app/videos/video-watch/video-magnet.component.html2
-rw-r--r--client/src/app/videos/video-watch/video-watch.component.ts10
-rw-r--r--client/src/standalone/videos/embed.ts6
-rw-r--r--package.json1
-rwxr-xr-xscripts/dev/index.sh5
-rwxr-xr-xscripts/update-host.ts16
-rw-r--r--server.ts20
-rw-r--r--server/controllers/api/remote/videos.ts42
-rw-r--r--server/controllers/api/videos/index.ts65
-rw-r--r--server/helpers/custom-validators/remote/videos.ts19
-rw-r--r--server/helpers/custom-validators/videos.ts39
-rw-r--r--server/initializers/constants.ts14
-rw-r--r--server/initializers/database.ts2
-rw-r--r--server/initializers/migrations/0060-video-file.ts34
-rw-r--r--server/initializers/migrations/0065-video-file-size.ts46
-rw-r--r--server/initializers/migrator.ts19
-rw-r--r--server/lib/jobs/handlers/video-transcoder.ts4
-rw-r--r--server/models/video/index.ts1
-rw-r--r--server/models/video/video-file-interface.ts24
-rw-r--r--server/models/video/video-file.ts89
-rw-r--r--server/models/video/video-interface.ts67
-rw-r--r--server/models/video/video.ts434
-rw-r--r--server/tests/api/multiple-pods.js69
-rw-r--r--server/tests/api/single-pod.js71
-rw-r--r--server/tests/api/video-transcoder.js10
-rw-r--r--shared/models/pods/remote-video/remote-video-create-request.model.ts8
-rw-r--r--shared/models/pods/remote-video/remote-video-update-request.model.ts6
-rw-r--r--shared/models/videos/video.model.ts9
30 files changed, 816 insertions, 338 deletions
diff --git a/.gitignore b/.gitignore
index 42d4f5926..b4c1de87d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -19,3 +19,4 @@
19/*.sublime-workspace 19/*.sublime-workspace
20/dist 20/dist
21/.idea 21/.idea
22/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 @@
1import { Video as VideoServerModel } from '../../../../../shared' 1import { Video as VideoServerModel, VideoFile } from '../../../../../shared'
2import { User } from '../../shared' 2import { User } from '../../shared'
3 3
4export class Video implements VideoServerModel { 4export class Video implements VideoServerModel {
@@ -17,7 +17,6 @@ export class Video implements VideoServerModel {
17 id: number 17 id: number
18 uuid: string 18 uuid: string
19 isLocal: boolean 19 isLocal: boolean
20 magnetUri: string
21 name: string 20 name: string
22 podHost: string 21 podHost: string
23 tags: string[] 22 tags: string[]
@@ -29,6 +28,7 @@ export class Video implements VideoServerModel {
29 likes: number 28 likes: number
30 dislikes: number 29 dislikes: number
31 nsfw: boolean 30 nsfw: boolean
31 files: VideoFile[]
32 32
33 private static createByString (author: string, podHost: string) { 33 private static createByString (author: string, podHost: string) {
34 return author + '@' + podHost 34 return author + '@' + podHost
@@ -57,7 +57,6 @@ export class Video implements VideoServerModel {
57 id: number, 57 id: number,
58 uuid: string, 58 uuid: string,
59 isLocal: boolean, 59 isLocal: boolean,
60 magnetUri: string,
61 name: string, 60 name: string,
62 podHost: string, 61 podHost: string,
63 tags: string[], 62 tags: string[],
@@ -66,7 +65,8 @@ export class Video implements VideoServerModel {
66 views: number, 65 views: number,
67 likes: number, 66 likes: number,
68 dislikes: number, 67 dislikes: number,
69 nsfw: boolean 68 nsfw: boolean,
69 files: VideoFile[]
70 }) { 70 }) {
71 this.author = hash.author 71 this.author = hash.author
72 this.createdAt = new Date(hash.createdAt) 72 this.createdAt = new Date(hash.createdAt)
@@ -82,7 +82,6 @@ export class Video implements VideoServerModel {
82 this.id = hash.id 82 this.id = hash.id
83 this.uuid = hash.uuid 83 this.uuid = hash.uuid
84 this.isLocal = hash.isLocal 84 this.isLocal = hash.isLocal
85 this.magnetUri = hash.magnetUri
86 this.name = hash.name 85 this.name = hash.name
87 this.podHost = hash.podHost 86 this.podHost = hash.podHost
88 this.tags = hash.tags 87 this.tags = hash.tags
@@ -94,6 +93,7 @@ export class Video implements VideoServerModel {
94 this.likes = hash.likes 93 this.likes = hash.likes
95 this.dislikes = hash.dislikes 94 this.dislikes = hash.dislikes
96 this.nsfw = hash.nsfw 95 this.nsfw = hash.nsfw
96 this.files = hash.files
97 97
98 this.by = Video.createByString(hash.author, hash.podHost) 98 this.by = Video.createByString(hash.author, hash.podHost)
99 } 99 }
@@ -115,6 +115,13 @@ export class Video implements VideoServerModel {
115 return (this.nsfw && (!user || user.displayNSFW === false)) 115 return (this.nsfw && (!user || user.displayNSFW === false))
116 } 116 }
117 117
118 getDefaultMagnetUri () {
119 if (this.files === undefined || this.files.length === 0) return ''
120
121 // TODO: choose the original file
122 return this.files[0].magnetUri
123 }
124
118 patch (values: Object) { 125 patch (values: Object) {
119 Object.keys(values).forEach((key) => { 126 Object.keys(values).forEach((key) => {
120 this[key] = values[key] 127 this[key] = values[key]
@@ -132,7 +139,6 @@ export class Video implements VideoServerModel {
132 duration: this.duration, 139 duration: this.duration,
133 id: this.id, 140 id: this.id,
134 isLocal: this.isLocal, 141 isLocal: this.isLocal,
135 magnetUri: this.magnetUri,
136 name: this.name, 142 name: this.name,
137 podHost: this.podHost, 143 podHost: this.podHost,
138 tags: this.tags, 144 tags: this.tags,
@@ -140,7 +146,8 @@ export class Video implements VideoServerModel {
140 views: this.views, 146 views: this.views,
141 likes: this.likes, 147 likes: this.likes,
142 dislikes: this.dislikes, 148 dislikes: this.dislikes,
143 nsfw: this.nsfw 149 nsfw: this.nsfw,
150 files: this.files
144 } 151 }
145 } 152 }
146} 153}
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 @@
10 </div> 10 </div>
11 11
12 <div class="modal-body"> 12 <div class="modal-body">
13 <input #magnetUriInput (click)="magnetUriInput.select()" type="text" class="form-control input-sm readonly" readonly [value]="video.magnetUri" /> 13 <input #magnetUriInput (click)="magnetUriInput.select()" type="text" class="form-control input-sm readonly" readonly [value]="video.getDefaultMagnetUri()" />
14 </div> 14 </div>
15 </div> 15 </div>
16 </div> 16 </div>
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 {
90 window.clearInterval(this.torrentInfosInterval) 90 window.clearInterval(this.torrentInfosInterval)
91 window.clearTimeout(this.errorTimer) 91 window.clearTimeout(this.errorTimer)
92 92
93 if (this.video !== null && this.webTorrentService.has(this.video.magnetUri)) { 93 if (this.video !== null && this.webTorrentService.has(this.video.getDefaultMagnetUri())) {
94 this.webTorrentService.remove(this.video.magnetUri) 94 this.webTorrentService.remove(this.video.getDefaultMagnetUri())
95 } 95 }
96 96
97 // Remove player 97 // Remove player
@@ -108,13 +108,13 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
108 // We are loading the video 108 // We are loading the video
109 this.loading = true 109 this.loading = true
110 110
111 console.log('Adding ' + this.video.magnetUri + '.') 111 console.log('Adding ' + this.video.getDefaultMagnetUri() + '.')
112 112
113 // The callback might never return if there are network issues 113 // The callback might never return if there are network issues
114 // So we create a timer to inform the user the load is abnormally long 114 // So we create a timer to inform the user the load is abnormally long
115 this.errorTimer = window.setTimeout(() => this.loadTooLong(), VideoWatchComponent.LOADTIME_TOO_LONG) 115 this.errorTimer = window.setTimeout(() => this.loadTooLong(), VideoWatchComponent.LOADTIME_TOO_LONG)
116 116
117 const torrent = this.webTorrentService.add(this.video.magnetUri, torrent => { 117 const torrent = this.webTorrentService.add(this.video.getDefaultMagnetUri(), torrent => {
118 // Clear the error timer 118 // Clear the error timer
119 window.clearTimeout(this.errorTimer) 119 window.clearTimeout(this.errorTimer)
120 // Maybe the error was fired by the timer, so reset it 120 // Maybe the error was fired by the timer, so reset it
@@ -123,7 +123,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
123 // We are not loading the video anymore 123 // We are not loading the video anymore
124 this.loading = false 124 this.loading = false
125 125
126 console.log('Added ' + this.video.magnetUri + '.') 126 console.log('Added ' + this.video.getDefaultMagnetUri() + '.')
127 torrent.files[0].renderTo(this.playerElement, (err) => { 127 torrent.files[0].renderTo(this.playerElement, (err) => {
128 if (err) { 128 if (err) {
129 this.notificationsService.error('Error', 'Cannot append the file in the video element.') 129 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) => {
57 return 57 return
58 } 58 }
59 59
60 const magnetUri = videoInfos.magnetUri 60 let magnetUri = ''
61 if (videoInfos.files !== undefined && videoInfos.files.length !== 0) {
62 magnetUri = videoInfos.files[0].magnetUri
63 }
64
61 const videoContainer = document.getElementById('video-container') as HTMLVideoElement 65 const videoContainer = document.getElementById('video-container') as HTMLVideoElement
62 const previewUrl = window.location.origin + videoInfos.previewPath 66 const previewUrl = window.location.origin + videoInfos.previewPath
63 videoContainer.poster = previewUrl 67 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 @@
30 "danger:clean:modules": "scripty", 30 "danger:clean:modules": "scripty",
31 "reset-password": "ts-node ./scripts/reset-password.ts", 31 "reset-password": "ts-node ./scripts/reset-password.ts",
32 "play": "scripty", 32 "play": "scripty",
33 "dev": "scripty",
33 "dev:server": "scripty", 34 "dev:server": "scripty",
34 "dev:client": "scripty", 35 "dev:client": "scripty",
35 "start": "node dist/server", 36 "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 @@
1#!/usr/bin/env sh
2
3NODE_ENV=test concurrently -k \
4 "npm run watch:client" \
5 "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 @@
1import { readFileSync, writeFileSync } from 'fs' 1import { readFileSync, writeFileSync } from 'fs'
2import { join } from 'path'
2import * as parseTorrent from 'parse-torrent' 3import * as parseTorrent from 'parse-torrent'
3 4
4import { CONFIG, STATIC_PATHS } from '../server/initializers/constants' 5import { CONFIG, STATIC_PATHS } from '../server/initializers/constants'
@@ -19,17 +20,10 @@ db.init(true)
19 return db.Video.list() 20 return db.Video.list()
20 }) 21 })
21 .then(videos => { 22 .then(videos => {
22 videos.forEach(function (video) { 23 videos.forEach(video => {
23 const torrentName = video.id + '.torrent' 24 video.VideoFiles.forEach(file => {
24 const torrentPath = CONFIG.STORAGE.TORRENTS_DIR + torrentName 25 video.createTorrentAndSetInfoHash(file)
25 const filename = video.id + video.extname 26 })
26
27 const parsed = parseTorrent(readFileSync(torrentPath))
28 parsed.announce = [ CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOST + '/tracker/socket' ]
29 parsed.urlList = [ CONFIG.WEBSERVER.URL + STATIC_PATHS.WEBSEED + filename ]
30
31 const buf = parseTorrent.toTorrentFile(parsed)
32 writeFileSync(torrentPath, buf)
33 }) 27 })
34 28
35 process.exit(0) 29 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()
26// ----------- Database ----------- 26// ----------- Database -----------
27// Do not use barrels because we don't want to load all modules here (we need to initialize database first) 27// Do not use barrels because we don't want to load all modules here (we need to initialize database first)
28import { logger } from './server/helpers/logger' 28import { logger } from './server/helpers/logger'
29import { API_VERSION, CONFIG } from './server/initializers/constants' 29import { API_VERSION, CONFIG, STATIC_PATHS } from './server/initializers/constants'
30// Initialize database and models 30// Initialize database and models
31import { database as db } from './server/initializers/database' 31import { database as db } from './server/initializers/database'
32db.init(false).then(() => onDatabaseInitDone()) 32db.init(false).then(() => onDatabaseInitDone())
@@ -57,10 +57,20 @@ import { apiRouter, clientsRouter, staticRouter } from './server/controllers'
57 57
58// Enable CORS for develop 58// Enable CORS for develop
59if (isTestInstance()) { 59if (isTestInstance()) {
60 app.use(cors({ 60 app.use((req, res, next) => {
61 origin: 'http://localhost:3000', 61 // These routes have already cors
62 credentials: true 62 if (
63 })) 63 req.path.indexOf(STATIC_PATHS.TORRENTS) === -1 &&
64 req.path.indexOf(STATIC_PATHS.WEBSEED) === -1
65 ) {
66 return (cors({
67 origin: 'http://localhost:3000',
68 credentials: true
69 }))(req, res, next)
70 }
71
72 return next()
73 })
64} 74}
65 75
66// For the logger 76// 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
258 const videoData = { 258 const videoData = {
259 name: videoToCreateData.name, 259 name: videoToCreateData.name,
260 uuid: videoToCreateData.uuid, 260 uuid: videoToCreateData.uuid,
261 extname: videoToCreateData.extname,
262 infoHash: videoToCreateData.infoHash,
263 category: videoToCreateData.category, 261 category: videoToCreateData.category,
264 licence: videoToCreateData.licence, 262 licence: videoToCreateData.licence,
265 language: videoToCreateData.language, 263 language: videoToCreateData.language,
@@ -290,6 +288,26 @@ function addRemoteVideo (videoToCreateData: RemoteVideoCreateData, fromPod: PodI
290 return video.save(options).then(videoCreated => ({ tagInstances, videoCreated })) 288 return video.save(options).then(videoCreated => ({ tagInstances, videoCreated }))
291 }) 289 })
292 .then(({ tagInstances, videoCreated }) => { 290 .then(({ tagInstances, videoCreated }) => {
291 const tasks = []
292 const options = {
293 transaction: t
294 }
295
296 videoToCreateData.files.forEach(fileData => {
297 const videoFileInstance = db.VideoFile.build({
298 extname: fileData.extname,
299 infoHash: fileData.infoHash,
300 resolution: fileData.resolution,
301 size: fileData.size,
302 videoId: videoCreated.id
303 })
304
305 tasks.push(videoFileInstance.save(options))
306 })
307
308 return Promise.all(tasks).then(() => ({ tagInstances, videoCreated }))
309 })
310 .then(({ tagInstances, videoCreated }) => {
293 const options = { 311 const options = {
294 transaction: t 312 transaction: t
295 } 313 }
@@ -344,6 +362,26 @@ function updateRemoteVideo (videoAttributesToUpdate: RemoteVideoUpdateData, from
344 362
345 return videoInstance.save(options).then(() => ({ videoInstance, tagInstances })) 363 return videoInstance.save(options).then(() => ({ videoInstance, tagInstances }))
346 }) 364 })
365 .then(({ tagInstances, videoInstance }) => {
366 const tasks = []
367 const options = {
368 transaction: t
369 }
370
371 videoAttributesToUpdate.files.forEach(fileData => {
372 const videoFileInstance = db.VideoFile.build({
373 extname: fileData.extname,
374 infoHash: fileData.infoHash,
375 resolution: fileData.resolution,
376 size: fileData.size,
377 videoId: videoInstance.id
378 })
379
380 tasks.push(videoFileInstance.save(options))
381 })
382
383 return Promise.all(tasks).then(() => ({ tagInstances, videoInstance }))
384 })
347 .then(({ videoInstance, tagInstances }) => { 385 .then(({ videoInstance, tagInstances }) => {
348 const options = { transaction: t } 386 const options = { transaction: t }
349 387
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 @@
1import * as express from 'express' 1import * as express from 'express'
2import * as Promise from 'bluebird' 2import * as Promise from 'bluebird'
3import * as multer from 'multer' 3import * as multer from 'multer'
4import * as path from 'path' 4import { extname, join } from 'path'
5 5
6import { database as db } from '../../../initializers/database' 6import { database as db } from '../../../initializers/database'
7import { 7import {
@@ -16,7 +16,8 @@ import {
16 addEventToRemoteVideo, 16 addEventToRemoteVideo,
17 quickAndDirtyUpdateVideoToFriends, 17 quickAndDirtyUpdateVideoToFriends,
18 addVideoToFriends, 18 addVideoToFriends,
19 updateVideoToFriends 19 updateVideoToFriends,
20 JobScheduler
20} from '../../../lib' 21} from '../../../lib'
21import { 22import {
22 authenticate, 23 authenticate,
@@ -155,7 +156,7 @@ function addVideoRetryWrapper (req: express.Request, res: express.Response, next
155 .catch(err => next(err)) 156 .catch(err => next(err))
156} 157}
157 158
158function addVideo (req: express.Request, res: express.Response, videoFile: Express.Multer.File) { 159function addVideo (req: express.Request, res: express.Response, videoPhysicalFile: Express.Multer.File) {
159 const videoInfos: VideoCreate = req.body 160 const videoInfos: VideoCreate = req.body
160 161
161 return db.sequelize.transaction(t => { 162 return db.sequelize.transaction(t => {
@@ -177,13 +178,13 @@ function addVideo (req: express.Request, res: express.Response, videoFile: Expre
177 const videoData = { 178 const videoData = {
178 name: videoInfos.name, 179 name: videoInfos.name,
179 remote: false, 180 remote: false,
180 extname: path.extname(videoFile.filename), 181 extname: extname(videoPhysicalFile.filename),
181 category: videoInfos.category, 182 category: videoInfos.category,
182 licence: videoInfos.licence, 183 licence: videoInfos.licence,
183 language: videoInfos.language, 184 language: videoInfos.language,
184 nsfw: videoInfos.nsfw, 185 nsfw: videoInfos.nsfw,
185 description: videoInfos.description, 186 description: videoInfos.description,
186 duration: videoFile['duration'], // duration was added by a previous middleware 187 duration: videoPhysicalFile['duration'], // duration was added by a previous middleware
187 authorId: author.id 188 authorId: author.id
188 } 189 }
189 190
@@ -191,18 +192,50 @@ function addVideo (req: express.Request, res: express.Response, videoFile: Expre
191 return { author, tagInstances, video } 192 return { author, tagInstances, video }
192 }) 193 })
193 .then(({ author, tagInstances, video }) => { 194 .then(({ author, tagInstances, video }) => {
195 const videoFileData = {
196 extname: extname(videoPhysicalFile.filename),
197 resolution: 0, // TODO: improve readability,
198 size: videoPhysicalFile.size
199 }
200
201 const videoFile = db.VideoFile.build(videoFileData)
202 return { author, tagInstances, video, videoFile }
203 })
204 .then(({ author, tagInstances, video, videoFile }) => {
194 const videoDir = CONFIG.STORAGE.VIDEOS_DIR 205 const videoDir = CONFIG.STORAGE.VIDEOS_DIR
195 const source = path.join(videoDir, videoFile.filename) 206 const source = join(videoDir, videoPhysicalFile.filename)
196 const destination = path.join(videoDir, video.getVideoFilename()) 207 const destination = join(videoDir, video.getVideoFilename(videoFile))
197 208
198 return renamePromise(source, destination) 209 return renamePromise(source, destination)
199 .then(() => { 210 .then(() => {
200 // This is important in case if there is another attempt in the retry process 211 // This is important in case if there is another attempt in the retry process
201 videoFile.filename = video.getVideoFilename() 212 videoPhysicalFile.filename = video.getVideoFilename(videoFile)
202 return { author, tagInstances, video } 213 return { author, tagInstances, video, videoFile }
203 }) 214 })
204 }) 215 })
205 .then(({ author, tagInstances, video }) => { 216 .then(({ author, tagInstances, video, videoFile }) => {
217 const tasks = []
218
219 tasks.push(
220 video.createTorrentAndSetInfoHash(videoFile),
221 video.createThumbnail(videoFile),
222 video.createPreview(videoFile)
223 )
224
225 if (CONFIG.TRANSCODING.ENABLED === true) {
226 // Put uuid because we don't have id auto incremented for now
227 const dataInput = {
228 videoUUID: video.uuid
229 }
230
231 tasks.push(
232 JobScheduler.Instance.createJob(t, 'videoTranscoder', dataInput)
233 )
234 }
235
236 return Promise.all(tasks).then(() => ({ author, tagInstances, video, videoFile }))
237 })
238 .then(({ author, tagInstances, video, videoFile }) => {
206 const options = { transaction: t } 239 const options = { transaction: t }
207 240
208 return video.save(options) 241 return video.save(options)
@@ -210,9 +243,17 @@ function addVideo (req: express.Request, res: express.Response, videoFile: Expre
210 // Do not forget to add Author informations to the created video 243 // Do not forget to add Author informations to the created video
211 videoCreated.Author = author 244 videoCreated.Author = author
212 245
213 return { tagInstances, video: videoCreated } 246 return { tagInstances, video: videoCreated, videoFile }
214 }) 247 })
215 }) 248 })
249 .then(({ tagInstances, video, videoFile }) => {
250 const options = { transaction: t }
251 videoFile.videoId = video.id
252
253 return videoFile.save(options)
254 .then(() => video.VideoFiles = [ videoFile ])
255 .then(() => ({ tagInstances, video }))
256 })
216 .then(({ tagInstances, video }) => { 257 .then(({ tagInstances, video }) => {
217 if (!tagInstances) return video 258 if (!tagInstances) return video
218 259
@@ -236,7 +277,7 @@ function addVideo (req: express.Request, res: express.Response, videoFile: Expre
236 }) 277 })
237 .then(() => logger.info('Video with name %s created.', videoInfos.name)) 278 .then(() => logger.info('Video with name %s created.', videoInfos.name))
238 .catch((err: Error) => { 279 .catch((err: Error) => {
239 logger.debug('Cannot insert the video.', { error: err.stack }) 280 logger.debug('Cannot insert the video.', err)
240 throw err 281 throw err
241 }) 282 })
242} 283}
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 {
23 isVideoNSFWValid, 23 isVideoNSFWValid,
24 isVideoDescriptionValid, 24 isVideoDescriptionValid,
25 isVideoDurationValid, 25 isVideoDurationValid,
26 isVideoInfoHashValid, 26 isVideoFileInfoHashValid,
27 isVideoNameValid, 27 isVideoNameValid,
28 isVideoTagsValid, 28 isVideoTagsValid,
29 isVideoExtnameValid 29 isVideoFileExtnameValid,
30 isVideoFileResolutionValid
30} from '../videos' 31} from '../videos'
31 32
32const ENDPOINT_ACTIONS = REQUEST_ENDPOINT_ACTIONS[REQUEST_ENDPOINTS.VIDEOS] 33const ENDPOINT_ACTIONS = REQUEST_ENDPOINT_ACTIONS[REQUEST_ENDPOINTS.VIDEOS]
@@ -121,14 +122,22 @@ function isCommonVideoAttributesValid (video: any) {
121 isVideoNSFWValid(video.nsfw) && 122 isVideoNSFWValid(video.nsfw) &&
122 isVideoDescriptionValid(video.description) && 123 isVideoDescriptionValid(video.description) &&
123 isVideoDurationValid(video.duration) && 124 isVideoDurationValid(video.duration) &&
124 isVideoInfoHashValid(video.infoHash) &&
125 isVideoNameValid(video.name) && 125 isVideoNameValid(video.name) &&
126 isVideoTagsValid(video.tags) && 126 isVideoTagsValid(video.tags) &&
127 isVideoUUIDValid(video.uuid) && 127 isVideoUUIDValid(video.uuid) &&
128 isVideoExtnameValid(video.extname) &&
129 isVideoViewsValid(video.views) && 128 isVideoViewsValid(video.views) &&
130 isVideoLikesValid(video.likes) && 129 isVideoLikesValid(video.likes) &&
131 isVideoDislikesValid(video.dislikes) 130 isVideoDislikesValid(video.dislikes) &&
131 isArray(video.files) &&
132 video.files.every(videoFile => {
133 if (!videoFile) return false
134
135 return (
136 isVideoFileInfoHashValid(videoFile.infoHash) &&
137 isVideoFileExtnameValid(videoFile.extname) &&
138 isVideoFileResolutionValid(videoFile.resolution)
139 )
140 })
132} 141}
133 142
134function isRequestTypeAddValid (value: string) { 143function 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 {
7 VIDEO_CATEGORIES, 7 VIDEO_CATEGORIES,
8 VIDEO_LICENCES, 8 VIDEO_LICENCES,
9 VIDEO_LANGUAGES, 9 VIDEO_LANGUAGES,
10 VIDEO_RATE_TYPES 10 VIDEO_RATE_TYPES,
11 VIDEO_FILE_RESOLUTIONS
11} from '../../initializers' 12} from '../../initializers'
12import { isUserUsernameValid } from './users' 13import { isUserUsernameValid } from './users'
13import { isArray, exists } from './misc' 14import { isArray, exists } from './misc'
@@ -53,14 +54,6 @@ function isVideoDurationValid (value: string) {
53 return exists(value) && validator.isInt(value + '', VIDEOS_CONSTRAINTS_FIELDS.DURATION) 54 return exists(value) && validator.isInt(value + '', VIDEOS_CONSTRAINTS_FIELDS.DURATION)
54} 55}
55 56
56function isVideoExtnameValid (value: string) {
57 return VIDEOS_CONSTRAINTS_FIELDS.EXTNAME.indexOf(value) !== -1
58}
59
60function isVideoInfoHashValid (value: string) {
61 return exists(value) && validator.isLength(value, VIDEOS_CONSTRAINTS_FIELDS.INFO_HASH)
62}
63
64function isVideoNameValid (value: string) { 57function isVideoNameValid (value: string) {
65 return exists(value) && validator.isLength(value, VIDEOS_CONSTRAINTS_FIELDS.NAME) 58 return exists(value) && validator.isLength(value, VIDEOS_CONSTRAINTS_FIELDS.NAME)
66} 59}
@@ -128,6 +121,22 @@ function isVideoFile (value: string, files: { [ fieldname: string ]: Express.Mul
128 return new RegExp('^video/(webm|mp4|ogg)$', 'i').test(file.mimetype) 121 return new RegExp('^video/(webm|mp4|ogg)$', 'i').test(file.mimetype)
129} 122}
130 123
124function isVideoFileSizeValid (value: string) {
125 return exists(value) && validator.isInt(value + '', VIDEOS_CONSTRAINTS_FIELDS.FILE_SIZE)
126}
127
128function isVideoFileResolutionValid (value: string) {
129 return VIDEO_FILE_RESOLUTIONS[value] !== undefined
130}
131
132function isVideoFileExtnameValid (value: string) {
133 return VIDEOS_CONSTRAINTS_FIELDS.EXTNAME.indexOf(value) !== -1
134}
135
136function isVideoFileInfoHashValid (value: string) {
137 return exists(value) && validator.isLength(value, VIDEOS_CONSTRAINTS_FIELDS.INFO_HASH)
138}
139
131// --------------------------------------------------------------------------- 140// ---------------------------------------------------------------------------
132 141
133export { 142export {
@@ -140,12 +149,12 @@ export {
140 isVideoNSFWValid, 149 isVideoNSFWValid,
141 isVideoDescriptionValid, 150 isVideoDescriptionValid,
142 isVideoDurationValid, 151 isVideoDurationValid,
143 isVideoInfoHashValid, 152 isVideoFileInfoHashValid,
144 isVideoNameValid, 153 isVideoNameValid,
145 isVideoTagsValid, 154 isVideoTagsValid,
146 isVideoThumbnailValid, 155 isVideoThumbnailValid,
147 isVideoThumbnailDataValid, 156 isVideoThumbnailDataValid,
148 isVideoExtnameValid, 157 isVideoFileExtnameValid,
149 isVideoUUIDValid, 158 isVideoUUIDValid,
150 isVideoAbuseReasonValid, 159 isVideoAbuseReasonValid,
151 isVideoAbuseReporterUsernameValid, 160 isVideoAbuseReporterUsernameValid,
@@ -154,7 +163,9 @@ export {
154 isVideoLikesValid, 163 isVideoLikesValid,
155 isVideoRatingTypeValid, 164 isVideoRatingTypeValid,
156 isVideoDislikesValid, 165 isVideoDislikesValid,
157 isVideoEventCountValid 166 isVideoEventCountValid,
167 isVideoFileSizeValid,
168 isVideoFileResolutionValid
158} 169}
159 170
160declare global { 171declare global {
@@ -183,7 +194,9 @@ declare global {
183 isVideoLikesValid, 194 isVideoLikesValid,
184 isVideoRatingTypeValid, 195 isVideoRatingTypeValid,
185 isVideoDislikesValid, 196 isVideoDislikesValid,
186 isVideoEventCountValid 197 isVideoEventCountValid,
198 isVideoFileSizeValid,
199 isVideoFileResolutionValid
187 } 200 }
188 } 201 }
189} 202}
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 {
15 15
16// --------------------------------------------------------------------------- 16// ---------------------------------------------------------------------------
17 17
18const LAST_MIGRATION_VERSION = 55 18const LAST_MIGRATION_VERSION = 65
19 19
20// --------------------------------------------------------------------------- 20// ---------------------------------------------------------------------------
21 21
@@ -114,7 +114,8 @@ const CONSTRAINTS_FIELDS = {
114 THUMBNAIL_DATA: { min: 0, max: 20000 }, // Bytes 114 THUMBNAIL_DATA: { min: 0, max: 20000 }, // Bytes
115 VIEWS: { min: 0 }, 115 VIEWS: { min: 0 },
116 LIKES: { min: 0 }, 116 LIKES: { min: 0 },
117 DISLIKES: { min: 0 } 117 DISLIKES: { min: 0 },
118 FILE_SIZE: { min: 10, max: 1024 * 1024 * 1024 * 3 /* 3Go */ }
118 }, 119 },
119 VIDEO_EVENTS: { 120 VIDEO_EVENTS: {
120 COUNT: { min: 0 } 121 COUNT: { min: 0 }
@@ -176,6 +177,14 @@ const VIDEO_LANGUAGES = {
176 14: 'Italien' 177 14: 'Italien'
177} 178}
178 179
180const VIDEO_FILE_RESOLUTIONS = {
181 0: 'original',
182 1: '360p',
183 2: '480p',
184 3: '720p',
185 4: '1080p'
186}
187
179// --------------------------------------------------------------------------- 188// ---------------------------------------------------------------------------
180 189
181// Score a pod has when we create it as a friend 190// Score a pod has when we create it as a friend
@@ -362,6 +371,7 @@ export {
362 THUMBNAILS_SIZE, 371 THUMBNAILS_SIZE,
363 USER_ROLES, 372 USER_ROLES,
364 VIDEO_CATEGORIES, 373 VIDEO_CATEGORIES,
374 VIDEO_FILE_RESOLUTIONS,
365 VIDEO_LANGUAGES, 375 VIDEO_LANGUAGES,
366 VIDEO_LICENCES, 376 VIDEO_LICENCES,
367 VIDEO_RATE_TYPES 377 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 {
23 UserVideoRateModel, 23 UserVideoRateModel,
24 VideoAbuseModel, 24 VideoAbuseModel,
25 BlacklistedVideoModel, 25 BlacklistedVideoModel,
26 VideoFileModel,
26 VideoTagModel, 27 VideoTagModel,
27 VideoModel 28 VideoModel
28} from '../models' 29} from '../models'
@@ -49,6 +50,7 @@ const database: {
49 UserVideoRate?: UserVideoRateModel, 50 UserVideoRate?: UserVideoRateModel,
50 User?: UserModel, 51 User?: UserModel,
51 VideoAbuse?: VideoAbuseModel, 52 VideoAbuse?: VideoAbuseModel,
53 VideoFile?: VideoFileModel,
52 BlacklistedVideo?: BlacklistedVideoModel, 54 BlacklistedVideo?: BlacklistedVideoModel,
53 VideoTag?: VideoTagModel, 55 VideoTag?: VideoTagModel,
54 Video?: VideoModel 56 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 @@
1import * as Sequelize from 'sequelize'
2import * as Promise from 'bluebird'
3
4function up (utils: {
5 transaction: Sequelize.Transaction,
6 queryInterface: Sequelize.QueryInterface,
7 sequelize: Sequelize.Sequelize,
8 db: any
9}): Promise<void> {
10 const q = utils.queryInterface
11
12 const query = 'INSERT INTO "VideoFiles" ("videoId", "resolution", "size", "extname", "infoHash", "createdAt", "updatedAt") ' +
13 'SELECT "id" AS "videoId", 0 AS "resolution", 0 AS "size", ' +
14 '"extname"::"text"::"enum_VideoFiles_extname" as "extname", "infoHash", "createdAt", "updatedAt" ' +
15 'FROM "Videos"'
16
17 return utils.db.VideoFile.sync()
18 .then(() => utils.sequelize.query(query))
19 .then(() => {
20 return q.removeColumn('Videos', 'extname')
21 })
22 .then(() => {
23 return q.removeColumn('Videos', 'infoHash')
24 })
25}
26
27function down (options) {
28 throw new Error('Not implemented.')
29}
30
31export {
32 up,
33 down
34}
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 @@
1import * as Sequelize from 'sequelize'
2import * as Promise from 'bluebird'
3import { stat } from 'fs'
4
5import { VideoInstance } from '../../models'
6
7function up (utils: {
8 transaction: Sequelize.Transaction,
9 queryInterface: Sequelize.QueryInterface,
10 sequelize: Sequelize.Sequelize,
11 db: any
12}): Promise<void> {
13 return utils.db.Video.listOwnedAndPopulateAuthorAndTags()
14 .then((videos: VideoInstance[]) => {
15 const tasks: Promise<any>[] = []
16
17 videos.forEach(video => {
18 video.VideoFiles.forEach(videoFile => {
19 const p = new Promise((res, rej) => {
20 stat(video.getVideoFilePath(videoFile), (err, stats) => {
21 if (err) return rej(err)
22
23 videoFile.size = stats.size
24 videoFile.save().then(res).catch(rej)
25 })
26 })
27
28 tasks.push(p)
29 })
30 })
31
32 return tasks
33 })
34 .then((tasks: Promise<any>[]) => {
35 return Promise.all(tasks)
36 })
37}
38
39function down (options) {
40 throw new Error('Not implemented.')
41}
42
43export {
44 up,
45 down
46}
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 () {
64 script: string 64 script: string
65 }[] = [] 65 }[] = []
66 66
67 files.forEach(file => { 67 files
68 // Filename is something like 'version-blabla.js' 68 .filter(file => file.endsWith('.js.map') === false)
69 const version = file.split('-')[0] 69 .forEach(file => {
70 filesToMigrate.push({ 70 // Filename is something like 'version-blabla.js'
71 version, 71 const version = file.split('-')[0]
72 script: file 72 filesToMigrate.push({
73 version,
74 script: file
75 })
73 }) 76 })
74 })
75 77
76 return filesToMigrate 78 return filesToMigrate
77 }) 79 })
@@ -93,7 +95,8 @@ function executeMigration (actualVersion: number, entity: { version: string, scr
93 const options = { 95 const options = {
94 transaction: t, 96 transaction: t,
95 queryInterface: db.sequelize.getQueryInterface(), 97 queryInterface: db.sequelize.getQueryInterface(),
96 sequelize: db.sequelize 98 sequelize: db.sequelize,
99 db
97 } 100 }
98 101
99 return migrationScript.up(options) 102 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'
5 5
6function process (data: { videoUUID: string }) { 6function process (data: { videoUUID: string }) {
7 return db.Video.loadByUUIDAndPopulateAuthorAndPodAndTags(data.videoUUID).then(video => { 7 return db.Video.loadByUUIDAndPopulateAuthorAndPodAndTags(data.videoUUID).then(video => {
8 return video.transcodeVideofile().then(() => video) 8 // TODO: handle multiple resolutions
9 const videoFile = video.VideoFiles[0]
10 return video.transcodeVideofile(videoFile).then(() => video)
9 }) 11 })
10} 12}
11 13
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'
3export * from './video-abuse-interface' 3export * from './video-abuse-interface'
4export * from './video-blacklist-interface' 4export * from './video-blacklist-interface'
5export * from './video-tag-interface' 5export * from './video-tag-interface'
6export * from './video-file-interface'
6export * from './video-interface' 7export * 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 @@
1import * as Sequelize from 'sequelize'
2
3export namespace VideoFileMethods {
4}
5
6export interface VideoFileClass {
7}
8
9export interface VideoFileAttributes {
10 resolution: number
11 size: number
12 infoHash?: string
13 extname: string
14
15 videoId?: number
16}
17
18export interface VideoFileInstance extends VideoFileClass, VideoFileAttributes, Sequelize.Instance<VideoFileAttributes> {
19 id: number
20 createdAt: Date
21 updatedAt: Date
22}
23
24export 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
index 000000000..09a30d7e0
--- /dev/null
+++ b/server/models/video/video-file.ts
@@ -0,0 +1,89 @@
1import * as Sequelize from 'sequelize'
2import { values } from 'lodash'
3
4import { CONSTRAINTS_FIELDS } from '../../initializers'
5import {
6 isVideoFileResolutionValid,
7 isVideoFileSizeValid,
8 isVideoFileInfoHashValid
9} from '../../helpers'
10
11import { addMethodsToModel } from '../utils'
12import {
13 VideoFileInstance,
14 VideoFileAttributes
15} from './video-file-interface'
16
17let VideoFile: Sequelize.Model<VideoFileInstance, VideoFileAttributes>
18
19export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) {
20 VideoFile = sequelize.define<VideoFileInstance, VideoFileAttributes>('VideoFile',
21 {
22 resolution: {
23 type: DataTypes.INTEGER,
24 allowNull: false,
25 validate: {
26 resolutionValid: value => {
27 const res = isVideoFileResolutionValid(value)
28 if (res === false) throw new Error('Video file resolution is not valid.')
29 }
30 }
31 },
32 size: {
33 type: DataTypes.INTEGER,
34 allowNull: false,
35 validate: {
36 sizeValid: value => {
37 const res = isVideoFileSizeValid(value)
38 if (res === false) throw new Error('Video file size is not valid.')
39 }
40 }
41 },
42 extname: {
43 type: DataTypes.ENUM(values(CONSTRAINTS_FIELDS.VIDEOS.EXTNAME)),
44 allowNull: false
45 },
46 infoHash: {
47 type: DataTypes.STRING,
48 allowNull: false,
49 validate: {
50 infoHashValid: value => {
51 const res = isVideoFileInfoHashValid(value)
52 if (res === false) throw new Error('Video file info hash is not valid.')
53 }
54 }
55 }
56 },
57 {
58 indexes: [
59 {
60 fields: [ 'videoId' ]
61 },
62 {
63 fields: [ 'infoHash' ]
64 }
65 ]
66 }
67 )
68
69 const classMethods = [
70 associate
71 ]
72 addMethodsToModel(VideoFile, classMethods)
73
74 return VideoFile
75}
76
77// ------------------------------ STATICS ------------------------------
78
79function associate (models) {
80 VideoFile.belongsTo(models.Video, {
81 foreignKey: {
82 name: 'videoId',
83 allowNull: false
84 },
85 onDelete: 'CASCADE'
86 })
87}
88
89// ------------------------------ 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'
3 3
4import { AuthorInstance } from './author-interface' 4import { AuthorInstance } from './author-interface'
5import { TagAttributes, TagInstance } from './tag-interface' 5import { TagAttributes, TagInstance } from './tag-interface'
6import { VideoFileAttributes, VideoFileInstance } from './video-file-interface'
6 7
7// Don't use barrel, import just what we need 8// Don't use barrel, import just what we need
8import { Video as FormatedVideo } from '../../../shared/models/videos/video.model' 9import { Video as FormatedVideo } from '../../../shared/models/videos/video.model'
9import { ResultList } from '../../../shared/models/result-list.model' 10import { ResultList } from '../../../shared/models/result-list.model'
10 11
12export type FormatedRemoteVideoFile = {
13 infoHash: string
14 resolution: number
15 extname: string
16 size: number
17}
18
11export type FormatedAddRemoteVideo = { 19export type FormatedAddRemoteVideo = {
12 uuid: string 20 uuid: string
13 name: string 21 name: string
@@ -16,17 +24,16 @@ export type FormatedAddRemoteVideo = {
16 language: number 24 language: number
17 nsfw: boolean 25 nsfw: boolean
18 description: string 26 description: string
19 infoHash: string
20 author: string 27 author: string
21 duration: number 28 duration: number
22 thumbnailData: string 29 thumbnailData: string
23 tags: string[] 30 tags: string[]
24 createdAt: Date 31 createdAt: Date
25 updatedAt: Date 32 updatedAt: Date
26 extname: string
27 views: number 33 views: number
28 likes: number 34 likes: number
29 dislikes: number 35 dislikes: number
36 files: FormatedRemoteVideoFile[]
30} 37}
31 38
32export type FormatedUpdateRemoteVideo = { 39export type FormatedUpdateRemoteVideo = {
@@ -37,31 +44,35 @@ export type FormatedUpdateRemoteVideo = {
37 language: number 44 language: number
38 nsfw: boolean 45 nsfw: boolean
39 description: string 46 description: string
40 infoHash: string
41 author: string 47 author: string
42 duration: number 48 duration: number
43 tags: string[] 49 tags: string[]
44 createdAt: Date 50 createdAt: Date
45 updatedAt: Date 51 updatedAt: Date
46 extname: string
47 views: number 52 views: number
48 likes: number 53 likes: number
49 dislikes: number 54 dislikes: number
55 files: FormatedRemoteVideoFile[]
50} 56}
51 57
52export namespace VideoMethods { 58export namespace VideoMethods {
53 export type GenerateMagnetUri = (this: VideoInstance) => string
54 export type GetVideoFilename = (this: VideoInstance) => string
55 export type GetThumbnailName = (this: VideoInstance) => string 59 export type GetThumbnailName = (this: VideoInstance) => string
56 export type GetPreviewName = (this: VideoInstance) => string 60 export type GetPreviewName = (this: VideoInstance) => string
57 export type GetTorrentName = (this: VideoInstance) => string
58 export type IsOwned = (this: VideoInstance) => boolean 61 export type IsOwned = (this: VideoInstance) => boolean
59 export type ToFormatedJSON = (this: VideoInstance) => FormatedVideo 62 export type ToFormatedJSON = (this: VideoInstance) => FormatedVideo
60 63
64 export type GenerateMagnetUri = (this: VideoInstance, videoFile: VideoFileInstance) => string
65 export type GetTorrentFileName = (this: VideoInstance, videoFile: VideoFileInstance) => string
66 export type GetVideoFilename = (this: VideoInstance, videoFile: VideoFileInstance) => string
67 export type CreatePreview = (this: VideoInstance, videoFile: VideoFileInstance) => Promise<string>
68 export type CreateThumbnail = (this: VideoInstance, videoFile: VideoFileInstance) => Promise<string>
69 export type GetVideoFilePath = (this: VideoInstance, videoFile: VideoFileInstance) => string
70 export type CreateTorrentAndSetInfoHash = (this: VideoInstance, videoFile: VideoFileInstance) => Promise<void>
71
61 export type ToAddRemoteJSON = (this: VideoInstance) => Promise<FormatedAddRemoteVideo> 72 export type ToAddRemoteJSON = (this: VideoInstance) => Promise<FormatedAddRemoteVideo>
62 export type ToUpdateRemoteJSON = (this: VideoInstance) => FormatedUpdateRemoteVideo 73 export type ToUpdateRemoteJSON = (this: VideoInstance) => FormatedUpdateRemoteVideo
63 74
64 export type TranscodeVideofile = (this: VideoInstance) => Promise<void> 75 export type TranscodeVideofile = (this: VideoInstance, inputVideoFile: VideoFileInstance) => Promise<void>
65 76
66 // Return thumbnail name 77 // Return thumbnail name
67 export type GenerateThumbnailFromData = (video: VideoInstance, thumbnailData: string) => Promise<string> 78 export type GenerateThumbnailFromData = (video: VideoInstance, thumbnailData: string) => Promise<string>
@@ -86,31 +97,25 @@ export namespace VideoMethods {
86 export type LoadAndPopulateAuthor = (id: number) => Promise<VideoInstance> 97 export type LoadAndPopulateAuthor = (id: number) => Promise<VideoInstance>
87 export type LoadAndPopulateAuthorAndPodAndTags = (id: number) => Promise<VideoInstance> 98 export type LoadAndPopulateAuthorAndPodAndTags = (id: number) => Promise<VideoInstance>
88 export type LoadByUUIDAndPopulateAuthorAndPodAndTags = (uuid: string) => Promise<VideoInstance> 99 export type LoadByUUIDAndPopulateAuthorAndPodAndTags = (uuid: string) => Promise<VideoInstance>
100
101 export type RemoveThumbnail = (this: VideoInstance) => Promise<void>
102 export type RemovePreview = (this: VideoInstance) => Promise<void>
103 export type RemoveFile = (this: VideoInstance, videoFile: VideoFileInstance) => Promise<void>
104 export type RemoveTorrent = (this: VideoInstance, videoFile: VideoFileInstance) => Promise<void>
89} 105}
90 106
91export interface VideoClass { 107export interface VideoClass {
92 generateMagnetUri: VideoMethods.GenerateMagnetUri
93 getVideoFilename: VideoMethods.GetVideoFilename
94 getThumbnailName: VideoMethods.GetThumbnailName
95 getPreviewName: VideoMethods.GetPreviewName
96 getTorrentName: VideoMethods.GetTorrentName
97 isOwned: VideoMethods.IsOwned
98 toFormatedJSON: VideoMethods.ToFormatedJSON
99 toAddRemoteJSON: VideoMethods.ToAddRemoteJSON
100 toUpdateRemoteJSON: VideoMethods.ToUpdateRemoteJSON
101 transcodeVideofile: VideoMethods.TranscodeVideofile
102
103 generateThumbnailFromData: VideoMethods.GenerateThumbnailFromData 108 generateThumbnailFromData: VideoMethods.GenerateThumbnailFromData
104 getDurationFromFile: VideoMethods.GetDurationFromFile 109 getDurationFromFile: VideoMethods.GetDurationFromFile
105 list: VideoMethods.List 110 list: VideoMethods.List
106 listForApi: VideoMethods.ListForApi 111 listForApi: VideoMethods.ListForApi
107 loadByHostAndUUID: VideoMethods.LoadByHostAndUUID
108 listOwnedAndPopulateAuthorAndTags: VideoMethods.ListOwnedAndPopulateAuthorAndTags 112 listOwnedAndPopulateAuthorAndTags: VideoMethods.ListOwnedAndPopulateAuthorAndTags
109 listOwnedByAuthor: VideoMethods.ListOwnedByAuthor 113 listOwnedByAuthor: VideoMethods.ListOwnedByAuthor
110 load: VideoMethods.Load 114 load: VideoMethods.Load
111 loadByUUID: VideoMethods.LoadByUUID
112 loadAndPopulateAuthor: VideoMethods.LoadAndPopulateAuthor 115 loadAndPopulateAuthor: VideoMethods.LoadAndPopulateAuthor
113 loadAndPopulateAuthorAndPodAndTags: VideoMethods.LoadAndPopulateAuthorAndPodAndTags 116 loadAndPopulateAuthorAndPodAndTags: VideoMethods.LoadAndPopulateAuthorAndPodAndTags
117 loadByHostAndUUID: VideoMethods.LoadByHostAndUUID
118 loadByUUID: VideoMethods.LoadByUUID
114 loadByUUIDAndPopulateAuthorAndPodAndTags: VideoMethods.LoadByUUIDAndPopulateAuthorAndPodAndTags 119 loadByUUIDAndPopulateAuthorAndPodAndTags: VideoMethods.LoadByUUIDAndPopulateAuthorAndPodAndTags
115 searchAndPopulateAuthorAndPodAndTags: VideoMethods.SearchAndPopulateAuthorAndPodAndTags 120 searchAndPopulateAuthorAndPodAndTags: VideoMethods.SearchAndPopulateAuthorAndPodAndTags
116} 121}
@@ -118,13 +123,11 @@ export interface VideoClass {
118export interface VideoAttributes { 123export interface VideoAttributes {
119 uuid?: string 124 uuid?: string
120 name: string 125 name: string
121 extname: string
122 category: number 126 category: number
123 licence: number 127 licence: number
124 language: number 128 language: number
125 nsfw: boolean 129 nsfw: boolean
126 description: string 130 description: string
127 infoHash?: string
128 duration: number 131 duration: number
129 views?: number 132 views?: number
130 likes?: number 133 likes?: number
@@ -133,6 +136,7 @@ export interface VideoAttributes {
133 136
134 Author?: AuthorInstance 137 Author?: AuthorInstance
135 Tags?: TagInstance[] 138 Tags?: TagInstance[]
139 VideoFiles?: VideoFileInstance[]
136} 140}
137 141
138export interface VideoInstance extends VideoClass, VideoAttributes, Sequelize.Instance<VideoAttributes> { 142export interface VideoInstance extends VideoClass, VideoAttributes, Sequelize.Instance<VideoAttributes> {
@@ -140,18 +144,27 @@ export interface VideoInstance extends VideoClass, VideoAttributes, Sequelize.In
140 createdAt: Date 144 createdAt: Date
141 updatedAt: Date 145 updatedAt: Date
142 146
147 createPreview: VideoMethods.CreatePreview
148 createThumbnail: VideoMethods.CreateThumbnail
149 createTorrentAndSetInfoHash: VideoMethods.CreateTorrentAndSetInfoHash
143 generateMagnetUri: VideoMethods.GenerateMagnetUri 150 generateMagnetUri: VideoMethods.GenerateMagnetUri
144 getVideoFilename: VideoMethods.GetVideoFilename
145 getThumbnailName: VideoMethods.GetThumbnailName
146 getPreviewName: VideoMethods.GetPreviewName 151 getPreviewName: VideoMethods.GetPreviewName
147 getTorrentName: VideoMethods.GetTorrentName 152 getThumbnailName: VideoMethods.GetThumbnailName
153 getTorrentFileName: VideoMethods.GetTorrentFileName
154 getVideoFilename: VideoMethods.GetVideoFilename
155 getVideoFilePath: VideoMethods.GetVideoFilePath
148 isOwned: VideoMethods.IsOwned 156 isOwned: VideoMethods.IsOwned
149 toFormatedJSON: VideoMethods.ToFormatedJSON 157 removeFile: VideoMethods.RemoveFile
158 removePreview: VideoMethods.RemovePreview
159 removeThumbnail: VideoMethods.RemoveThumbnail
160 removeTorrent: VideoMethods.RemoveTorrent
150 toAddRemoteJSON: VideoMethods.ToAddRemoteJSON 161 toAddRemoteJSON: VideoMethods.ToAddRemoteJSON
162 toFormatedJSON: VideoMethods.ToFormatedJSON
151 toUpdateRemoteJSON: VideoMethods.ToUpdateRemoteJSON 163 toUpdateRemoteJSON: VideoMethods.ToUpdateRemoteJSON
152 transcodeVideofile: VideoMethods.TranscodeVideofile 164 transcodeVideofile: VideoMethods.TranscodeVideofile
153 165
154 setTags: Sequelize.HasManySetAssociationsMixin<TagAttributes, string> 166 setTags: Sequelize.HasManySetAssociationsMixin<TagAttributes, string>
167 setVideoFiles: Sequelize.HasManySetAssociationsMixin<VideoFileAttributes, string>
155} 168}
156 169
157export interface VideoModel extends VideoClass, Sequelize.Model<VideoInstance, VideoAttributes> {} 170export interface VideoModel extends VideoClass, Sequelize.Model<VideoInstance, VideoAttributes> {}
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'
2const Buffer = safeBuffer.Buffer 2const Buffer = safeBuffer.Buffer
3import * as ffmpeg from 'fluent-ffmpeg' 3import * as ffmpeg from 'fluent-ffmpeg'
4import * as magnetUtil from 'magnet-uri' 4import * as magnetUtil from 'magnet-uri'
5import { map, values } from 'lodash' 5import { map } from 'lodash'
6import * as parseTorrent from 'parse-torrent' 6import * as parseTorrent from 'parse-torrent'
7import { join } from 'path' 7import { join } from 'path'
8import * as Sequelize from 'sequelize' 8import * as Sequelize from 'sequelize'
9import * as Promise from 'bluebird' 9import * as Promise from 'bluebird'
10 10
11import { database as db } from '../../initializers/database'
12import { TagInstance } from './tag-interface' 11import { TagInstance } from './tag-interface'
13import { 12import {
14 logger, 13 logger,
@@ -18,7 +17,6 @@ import {
18 isVideoLanguageValid, 17 isVideoLanguageValid,
19 isVideoNSFWValid, 18 isVideoNSFWValid,
20 isVideoDescriptionValid, 19 isVideoDescriptionValid,
21 isVideoInfoHashValid,
22 isVideoDurationValid, 20 isVideoDurationValid,
23 readFileBufferPromise, 21 readFileBufferPromise,
24 unlinkPromise, 22 unlinkPromise,
@@ -27,16 +25,17 @@ import {
27 createTorrentPromise 25 createTorrentPromise
28} from '../../helpers' 26} from '../../helpers'
29import { 27import {
30 CONSTRAINTS_FIELDS,
31 CONFIG, 28 CONFIG,
32 REMOTE_SCHEME, 29 REMOTE_SCHEME,
33 STATIC_PATHS, 30 STATIC_PATHS,
34 VIDEO_CATEGORIES, 31 VIDEO_CATEGORIES,
35 VIDEO_LICENCES, 32 VIDEO_LICENCES,
36 VIDEO_LANGUAGES, 33 VIDEO_LANGUAGES,
37 THUMBNAILS_SIZE 34 THUMBNAILS_SIZE,
35 VIDEO_FILE_RESOLUTIONS
38} from '../../initializers' 36} from '../../initializers'
39import { JobScheduler, removeVideoToFriends } from '../../lib' 37import { removeVideoToFriends } from '../../lib'
38import { VideoFileInstance } from './video-file-interface'
40 39
41import { addMethodsToModel, getSort } from '../utils' 40import { addMethodsToModel, getSort } from '../utils'
42import { 41import {
@@ -51,12 +50,16 @@ let generateMagnetUri: VideoMethods.GenerateMagnetUri
51let getVideoFilename: VideoMethods.GetVideoFilename 50let getVideoFilename: VideoMethods.GetVideoFilename
52let getThumbnailName: VideoMethods.GetThumbnailName 51let getThumbnailName: VideoMethods.GetThumbnailName
53let getPreviewName: VideoMethods.GetPreviewName 52let getPreviewName: VideoMethods.GetPreviewName
54let getTorrentName: VideoMethods.GetTorrentName 53let getTorrentFileName: VideoMethods.GetTorrentFileName
55let isOwned: VideoMethods.IsOwned 54let isOwned: VideoMethods.IsOwned
56let toFormatedJSON: VideoMethods.ToFormatedJSON 55let toFormatedJSON: VideoMethods.ToFormatedJSON
57let toAddRemoteJSON: VideoMethods.ToAddRemoteJSON 56let toAddRemoteJSON: VideoMethods.ToAddRemoteJSON
58let toUpdateRemoteJSON: VideoMethods.ToUpdateRemoteJSON 57let toUpdateRemoteJSON: VideoMethods.ToUpdateRemoteJSON
59let transcodeVideofile: VideoMethods.TranscodeVideofile 58let transcodeVideofile: VideoMethods.TranscodeVideofile
59let createPreview: VideoMethods.CreatePreview
60let createThumbnail: VideoMethods.CreateThumbnail
61let getVideoFilePath: VideoMethods.GetVideoFilePath
62let createTorrentAndSetInfoHash: VideoMethods.CreateTorrentAndSetInfoHash
60 63
61let generateThumbnailFromData: VideoMethods.GenerateThumbnailFromData 64let generateThumbnailFromData: VideoMethods.GenerateThumbnailFromData
62let getDurationFromFile: VideoMethods.GetDurationFromFile 65let getDurationFromFile: VideoMethods.GetDurationFromFile
@@ -71,6 +74,10 @@ let loadAndPopulateAuthor: VideoMethods.LoadAndPopulateAuthor
71let loadAndPopulateAuthorAndPodAndTags: VideoMethods.LoadAndPopulateAuthorAndPodAndTags 74let loadAndPopulateAuthorAndPodAndTags: VideoMethods.LoadAndPopulateAuthorAndPodAndTags
72let loadByUUIDAndPopulateAuthorAndPodAndTags: VideoMethods.LoadByUUIDAndPopulateAuthorAndPodAndTags 75let loadByUUIDAndPopulateAuthorAndPodAndTags: VideoMethods.LoadByUUIDAndPopulateAuthorAndPodAndTags
73let searchAndPopulateAuthorAndPodAndTags: VideoMethods.SearchAndPopulateAuthorAndPodAndTags 76let searchAndPopulateAuthorAndPodAndTags: VideoMethods.SearchAndPopulateAuthorAndPodAndTags
77let removeThumbnail: VideoMethods.RemoveThumbnail
78let removePreview: VideoMethods.RemovePreview
79let removeFile: VideoMethods.RemoveFile
80let removeTorrent: VideoMethods.RemoveTorrent
74 81
75export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { 82export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) {
76 Video = sequelize.define<VideoInstance, VideoAttributes>('Video', 83 Video = sequelize.define<VideoInstance, VideoAttributes>('Video',
@@ -93,10 +100,6 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
93 } 100 }
94 } 101 }
95 }, 102 },
96 extname: {
97 type: DataTypes.ENUM(values(CONSTRAINTS_FIELDS.VIDEOS.EXTNAME)),
98 allowNull: false
99 },
100 category: { 103 category: {
101 type: DataTypes.INTEGER, 104 type: DataTypes.INTEGER,
102 allowNull: false, 105 allowNull: false,
@@ -148,16 +151,6 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
148 } 151 }
149 } 152 }
150 }, 153 },
151 infoHash: {
152 type: DataTypes.STRING,
153 allowNull: false,
154 validate: {
155 infoHashValid: value => {
156 const res = isVideoInfoHashValid(value)
157 if (res === false) throw new Error('Video info hash is not valid.')
158 }
159 }
160 },
161 duration: { 154 duration: {
162 type: DataTypes.INTEGER, 155 type: DataTypes.INTEGER,
163 allowNull: false, 156 allowNull: false,
@@ -216,9 +209,6 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
216 fields: [ 'duration' ] 209 fields: [ 'duration' ]
217 }, 210 },
218 { 211 {
219 fields: [ 'infoHash' ]
220 },
221 {
222 fields: [ 'views' ] 212 fields: [ 'views' ]
223 }, 213 },
224 { 214 {
@@ -229,8 +219,6 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
229 } 219 }
230 ], 220 ],
231 hooks: { 221 hooks: {
232 beforeValidate,
233 beforeCreate,
234 afterDestroy 222 afterDestroy
235 } 223 }
236 } 224 }
@@ -246,23 +234,30 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
246 listOwnedAndPopulateAuthorAndTags, 234 listOwnedAndPopulateAuthorAndTags,
247 listOwnedByAuthor, 235 listOwnedByAuthor,
248 load, 236 load,
249 loadByUUID,
250 loadByHostAndUUID,
251 loadAndPopulateAuthor, 237 loadAndPopulateAuthor,
252 loadAndPopulateAuthorAndPodAndTags, 238 loadAndPopulateAuthorAndPodAndTags,
239 loadByHostAndUUID,
240 loadByUUID,
253 loadByUUIDAndPopulateAuthorAndPodAndTags, 241 loadByUUIDAndPopulateAuthorAndPodAndTags,
254 searchAndPopulateAuthorAndPodAndTags, 242 searchAndPopulateAuthorAndPodAndTags
255 removeFromBlacklist
256 ] 243 ]
257 const instanceMethods = [ 244 const instanceMethods = [
245 createPreview,
246 createThumbnail,
247 createTorrentAndSetInfoHash,
258 generateMagnetUri, 248 generateMagnetUri,
259 getVideoFilename,
260 getThumbnailName,
261 getPreviewName, 249 getPreviewName,
262 getTorrentName, 250 getThumbnailName,
251 getTorrentFileName,
252 getVideoFilename,
253 getVideoFilePath,
263 isOwned, 254 isOwned,
264 toFormatedJSON, 255 removeFile,
256 removePreview,
257 removeThumbnail,
258 removeTorrent,
265 toAddRemoteJSON, 259 toAddRemoteJSON,
260 toFormatedJSON,
266 toUpdateRemoteJSON, 261 toUpdateRemoteJSON,
267 transcodeVideofile 262 transcodeVideofile
268 ] 263 ]
@@ -271,65 +266,6 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
271 return Video 266 return Video
272} 267}
273 268
274function beforeValidate (video: VideoInstance) {
275 // Put a fake infoHash if it does not exists yet
276 if (video.isOwned() && !video.infoHash) {
277 // 40 hexa length
278 video.infoHash = '0123456789abcdef0123456789abcdef01234567'
279 }
280}
281
282function beforeCreate (video: VideoInstance, options: { transaction: Sequelize.Transaction }) {
283 if (video.isOwned()) {
284 const videoPath = join(CONFIG.STORAGE.VIDEOS_DIR, video.getVideoFilename())
285 const tasks = []
286
287 tasks.push(
288 createTorrentFromVideo(video, videoPath),
289 createThumbnail(video, videoPath),
290 createPreview(video, videoPath)
291 )
292
293 if (CONFIG.TRANSCODING.ENABLED === true) {
294 // Put uuid because we don't have id auto incremented for now
295 const dataInput = {
296 videoUUID: video.uuid
297 }
298
299 tasks.push(
300 JobScheduler.Instance.createJob(options.transaction, 'videoTranscoder', dataInput)
301 )
302 }
303
304 return Promise.all(tasks)
305 }
306
307 return Promise.resolve()
308}
309
310function afterDestroy (video: VideoInstance) {
311 const tasks = []
312
313 tasks.push(
314 removeThumbnail(video)
315 )
316
317 if (video.isOwned()) {
318 const removeVideoToFriendsParams = {
319 uuid: video.uuid
320 }
321
322 tasks.push(
323 removeFile(video),
324 removeTorrent(video),
325 removePreview(video),
326 removeVideoToFriends(removeVideoToFriendsParams)
327 )
328 }
329
330 return Promise.all(tasks)
331}
332
333// ------------------------------ METHODS ------------------------------ 269// ------------------------------ METHODS ------------------------------
334 270
335function associate (models) { 271function associate (models) {
@@ -354,37 +290,46 @@ function associate (models) {
354 }, 290 },
355 onDelete: 'cascade' 291 onDelete: 'cascade'
356 }) 292 })
293
294 Video.hasMany(models.VideoFile, {
295 foreignKey: {
296 name: 'videoId',
297 allowNull: false
298 },
299 onDelete: 'cascade'
300 })
357} 301}
358 302
359generateMagnetUri = function (this: VideoInstance) { 303function afterDestroy (video: VideoInstance) {
360 let baseUrlHttp 304 const tasks = []
361 let baseUrlWs
362 305
363 if (this.isOwned()) { 306 tasks.push(
364 baseUrlHttp = CONFIG.WEBSERVER.URL 307 video.removeThumbnail()
365 baseUrlWs = CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT 308 )
366 } else {
367 baseUrlHttp = REMOTE_SCHEME.HTTP + '://' + this.Author.Pod.host
368 baseUrlWs = REMOTE_SCHEME.WS + '://' + this.Author.Pod.host
369 }
370 309
371 const xs = baseUrlHttp + STATIC_PATHS.TORRENTS + this.getTorrentName() 310 if (video.isOwned()) {
372 const announce = [ baseUrlWs + '/tracker/socket' ] 311 const removeVideoToFriendsParams = {
373 const urlList = [ baseUrlHttp + STATIC_PATHS.WEBSEED + this.getVideoFilename() ] 312 uuid: video.uuid
313 }
374 314
375 const magnetHash = { 315 tasks.push(
376 xs, 316 video.removePreview(),
377 announce, 317 removeVideoToFriends(removeVideoToFriendsParams)
378 urlList, 318 )
379 infoHash: this.infoHash, 319
380 name: this.name 320 // TODO: check files is populated
321 video.VideoFiles.forEach(file => {
322 video.removeFile(file),
323 video.removeTorrent(file)
324 })
381 } 325 }
382 326
383 return magnetUtil.encode(magnetHash) 327 return Promise.all(tasks)
384} 328}
385 329
386getVideoFilename = function (this: VideoInstance) { 330getVideoFilename = function (this: VideoInstance, videoFile: VideoFileInstance) {
387 return this.uuid + this.extname 331 // return this.uuid + '-' + VIDEO_FILE_RESOLUTIONS[videoFile.resolution] + videoFile.extname
332 return this.uuid + videoFile.extname
388} 333}
389 334
390getThumbnailName = function (this: VideoInstance) { 335getThumbnailName = function (this: VideoInstance) {
@@ -398,8 +343,9 @@ getPreviewName = function (this: VideoInstance) {
398 return this.uuid + extension 343 return this.uuid + extension
399} 344}
400 345
401getTorrentName = function (this: VideoInstance) { 346getTorrentFileName = function (this: VideoInstance, videoFile: VideoFileInstance) {
402 const extension = '.torrent' 347 const extension = '.torrent'
348 // return this.uuid + '-' + VIDEO_FILE_RESOLUTIONS[videoFile.resolution] + extension
403 return this.uuid + extension 349 return this.uuid + extension
404} 350}
405 351
@@ -407,6 +353,67 @@ isOwned = function (this: VideoInstance) {
407 return this.remote === false 353 return this.remote === false
408} 354}
409 355
356createPreview = function (this: VideoInstance, videoFile: VideoFileInstance) {
357 return generateImage(this, this.getVideoFilePath(videoFile), CONFIG.STORAGE.PREVIEWS_DIR, this.getPreviewName(), null)
358}
359
360createThumbnail = function (this: VideoInstance, videoFile: VideoFileInstance) {
361 return generateImage(this, this.getVideoFilePath(videoFile), CONFIG.STORAGE.THUMBNAILS_DIR, this.getThumbnailName(), THUMBNAILS_SIZE)
362}
363
364getVideoFilePath = function (this: VideoInstance, videoFile: VideoFileInstance) {
365 return join(CONFIG.STORAGE.VIDEOS_DIR, this.getVideoFilename(videoFile))
366}
367
368createTorrentAndSetInfoHash = function (this: VideoInstance, videoFile: VideoFileInstance) {
369 const options = {
370 announceList: [
371 [ CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT + '/tracker/socket' ]
372 ],
373 urlList: [
374 CONFIG.WEBSERVER.URL + STATIC_PATHS.WEBSEED + this.getVideoFilename(videoFile)
375 ]
376 }
377
378 return createTorrentPromise(this.getVideoFilePath(videoFile), options)
379 .then(torrent => {
380 const filePath = join(CONFIG.STORAGE.TORRENTS_DIR, this.getTorrentFileName(videoFile))
381 return writeFilePromise(filePath, torrent).then(() => torrent)
382 })
383 .then(torrent => {
384 const parsedTorrent = parseTorrent(torrent)
385
386 videoFile.infoHash = parsedTorrent.infoHash
387 })
388}
389
390generateMagnetUri = function (this: VideoInstance, videoFile: VideoFileInstance) {
391 let baseUrlHttp
392 let baseUrlWs
393
394 if (this.isOwned()) {
395 baseUrlHttp = CONFIG.WEBSERVER.URL
396 baseUrlWs = CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT
397 } else {
398 baseUrlHttp = REMOTE_SCHEME.HTTP + '://' + this.Author.Pod.host
399 baseUrlWs = REMOTE_SCHEME.WS + '://' + this.Author.Pod.host
400 }
401
402 const xs = baseUrlHttp + STATIC_PATHS.TORRENTS + this.getTorrentFileName(videoFile)
403 const announce = [ baseUrlWs + '/tracker/socket' ]
404 const urlList = [ baseUrlHttp + STATIC_PATHS.WEBSEED + this.getVideoFilename(videoFile) ]
405
406 const magnetHash = {
407 xs,
408 announce,
409 urlList,
410 infoHash: videoFile.infoHash,
411 name: this.name
412 }
413
414 return magnetUtil.encode(magnetHash)
415}
416
410toFormatedJSON = function (this: VideoInstance) { 417toFormatedJSON = function (this: VideoInstance) {
411 let podHost 418 let podHost
412 419
@@ -443,7 +450,6 @@ toFormatedJSON = function (this: VideoInstance) {
443 description: this.description, 450 description: this.description,
444 podHost, 451 podHost,
445 isLocal: this.isOwned(), 452 isLocal: this.isOwned(),
446 magnetUri: this.generateMagnetUri(),
447 author: this.Author.name, 453 author: this.Author.name,
448 duration: this.duration, 454 duration: this.duration,
449 views: this.views, 455 views: this.views,
@@ -453,9 +459,24 @@ toFormatedJSON = function (this: VideoInstance) {
453 thumbnailPath: join(STATIC_PATHS.THUMBNAILS, this.getThumbnailName()), 459 thumbnailPath: join(STATIC_PATHS.THUMBNAILS, this.getThumbnailName()),
454 previewPath: join(STATIC_PATHS.PREVIEWS, this.getPreviewName()), 460 previewPath: join(STATIC_PATHS.PREVIEWS, this.getPreviewName()),
455 createdAt: this.createdAt, 461 createdAt: this.createdAt,
456 updatedAt: this.updatedAt 462 updatedAt: this.updatedAt,
463 files: []
457 } 464 }
458 465
466 this.VideoFiles.forEach(videoFile => {
467 let resolutionLabel = VIDEO_FILE_RESOLUTIONS[videoFile.resolution]
468 if (!resolutionLabel) resolutionLabel = 'Unknown'
469
470 const videoFileJson = {
471 resolution: videoFile.resolution,
472 resolutionLabel,
473 magnetUri: this.generateMagnetUri(videoFile),
474 size: videoFile.size
475 }
476
477 json.files.push(videoFileJson)
478 })
479
459 return json 480 return json
460} 481}
461 482
@@ -472,19 +493,27 @@ toAddRemoteJSON = function (this: VideoInstance) {
472 language: this.language, 493 language: this.language,
473 nsfw: this.nsfw, 494 nsfw: this.nsfw,
474 description: this.description, 495 description: this.description,
475 infoHash: this.infoHash,
476 author: this.Author.name, 496 author: this.Author.name,
477 duration: this.duration, 497 duration: this.duration,
478 thumbnailData: thumbnailData.toString('binary'), 498 thumbnailData: thumbnailData.toString('binary'),
479 tags: map<TagInstance, string>(this.Tags, 'name'), 499 tags: map<TagInstance, string>(this.Tags, 'name'),
480 createdAt: this.createdAt, 500 createdAt: this.createdAt,
481 updatedAt: this.updatedAt, 501 updatedAt: this.updatedAt,
482 extname: this.extname,
483 views: this.views, 502 views: this.views,
484 likes: this.likes, 503 likes: this.likes,
485 dislikes: this.dislikes 504 dislikes: this.dislikes,
505 files: []
486 } 506 }
487 507
508 this.VideoFiles.forEach(videoFile => {
509 remoteVideo.files.push({
510 infoHash: videoFile.infoHash,
511 resolution: videoFile.resolution,
512 extname: videoFile.extname,
513 size: videoFile.size
514 })
515 })
516
488 return remoteVideo 517 return remoteVideo
489 }) 518 })
490} 519}
@@ -498,28 +527,34 @@ toUpdateRemoteJSON = function (this: VideoInstance) {
498 language: this.language, 527 language: this.language,
499 nsfw: this.nsfw, 528 nsfw: this.nsfw,
500 description: this.description, 529 description: this.description,
501 infoHash: this.infoHash,
502 author: this.Author.name, 530 author: this.Author.name,
503 duration: this.duration, 531 duration: this.duration,
504 tags: map<TagInstance, string>(this.Tags, 'name'), 532 tags: map<TagInstance, string>(this.Tags, 'name'),
505 createdAt: this.createdAt, 533 createdAt: this.createdAt,
506 updatedAt: this.updatedAt, 534 updatedAt: this.updatedAt,
507 extname: this.extname,
508 views: this.views, 535 views: this.views,
509 likes: this.likes, 536 likes: this.likes,
510 dislikes: this.dislikes 537 dislikes: this.dislikes,
538 files: []
511 } 539 }
512 540
541 this.VideoFiles.forEach(videoFile => {
542 json.files.push({
543 infoHash: videoFile.infoHash,
544 resolution: videoFile.resolution,
545 extname: videoFile.extname,
546 size: videoFile.size
547 })
548 })
549
513 return json 550 return json
514} 551}
515 552
516transcodeVideofile = function (this: VideoInstance) { 553transcodeVideofile = function (this: VideoInstance, inputVideoFile: VideoFileInstance) {
517 const video = this
518
519 const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR 554 const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR
520 const newExtname = '.mp4' 555 const newExtname = '.mp4'
521 const videoInputPath = join(videosDirectory, video.getVideoFilename()) 556 const videoInputPath = join(videosDirectory, this.getVideoFilename(inputVideoFile))
522 const videoOutputPath = join(videosDirectory, video.id + '-transcoded' + newExtname) 557 const videoOutputPath = join(videosDirectory, this.id + '-transcoded' + newExtname)
523 558
524 return new Promise<void>((res, rej) => { 559 return new Promise<void>((res, rej) => {
525 ffmpeg(videoInputPath) 560 ffmpeg(videoInputPath)
@@ -533,24 +568,22 @@ transcodeVideofile = function (this: VideoInstance) {
533 return unlinkPromise(videoInputPath) 568 return unlinkPromise(videoInputPath)
534 .then(() => { 569 .then(() => {
535 // Important to do this before getVideoFilename() to take in account the new file extension 570 // Important to do this before getVideoFilename() to take in account the new file extension
536 video.set('extname', newExtname) 571 inputVideoFile.set('extname', newExtname)
537 572
538 const newVideoPath = join(videosDirectory, video.getVideoFilename()) 573 return renamePromise(videoOutputPath, this.getVideoFilePath(inputVideoFile))
539 return renamePromise(videoOutputPath, newVideoPath)
540 }) 574 })
541 .then(() => { 575 .then(() => {
542 const newVideoPath = join(videosDirectory, video.getVideoFilename()) 576 return this.createTorrentAndSetInfoHash(inputVideoFile)
543 return createTorrentFromVideo(video, newVideoPath)
544 }) 577 })
545 .then(() => { 578 .then(() => {
546 return video.save() 579 return inputVideoFile.save()
547 }) 580 })
548 .then(() => { 581 .then(() => {
549 return res() 582 return res()
550 }) 583 })
551 .catch(err => { 584 .catch(err => {
552 // Autodesctruction... 585 // Autodestruction...
553 video.destroy().catch(err => logger.error('Cannot destruct video after transcoding failure.', err)) 586 this.destroy().catch(err => logger.error('Cannot destruct video after transcoding failure.', err))
554 587
555 return rej(err) 588 return rej(err)
556 }) 589 })
@@ -559,6 +592,26 @@ transcodeVideofile = function (this: VideoInstance) {
559 }) 592 })
560} 593}
561 594
595removeThumbnail = function (this: VideoInstance) {
596 const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, this.getThumbnailName())
597 return unlinkPromise(thumbnailPath)
598}
599
600removePreview = function (this: VideoInstance) {
601 // Same name than video thumbnail
602 return unlinkPromise(CONFIG.STORAGE.PREVIEWS_DIR + this.getPreviewName())
603}
604
605removeFile = function (this: VideoInstance, videoFile: VideoFileInstance) {
606 const filePath = join(CONFIG.STORAGE.VIDEOS_DIR, this.getVideoFilename(videoFile))
607 return unlinkPromise(filePath)
608}
609
610removeTorrent = function (this: VideoInstance, videoFile: VideoFileInstance) {
611 const torrenPath = join(CONFIG.STORAGE.TORRENTS_DIR, this.getTorrentFileName(videoFile))
612 return unlinkPromise(torrenPath)
613}
614
562// ------------------------------ STATICS ------------------------------ 615// ------------------------------ STATICS ------------------------------
563 616
564generateThumbnailFromData = function (video: VideoInstance, thumbnailData: string) { 617generateThumbnailFromData = function (video: VideoInstance, thumbnailData: string) {
@@ -582,7 +635,11 @@ getDurationFromFile = function (videoPath: string) {
582} 635}
583 636
584list = function () { 637list = function () {
585 return Video.findAll() 638 const query = {
639 include: [ Video['sequelize'].models.VideoFile ]
640 }
641
642 return Video.findAll(query)
586} 643}
587 644
588listForApi = function (start: number, count: number, sort: string) { 645listForApi = function (start: number, count: number, sort: string) {
@@ -597,8 +654,8 @@ listForApi = function (start: number, count: number, sort: string) {
597 model: Video['sequelize'].models.Author, 654 model: Video['sequelize'].models.Author,
598 include: [ { model: Video['sequelize'].models.Pod, required: false } ] 655 include: [ { model: Video['sequelize'].models.Pod, required: false } ]
599 }, 656 },
600 657 Video['sequelize'].models.Tag,
601 Video['sequelize'].models.Tag 658 Video['sequelize'].models.VideoFile
602 ], 659 ],
603 where: createBaseVideosWhere() 660 where: createBaseVideosWhere()
604 } 661 }
@@ -618,6 +675,9 @@ loadByHostAndUUID = function (fromHost: string, uuid: string) {
618 }, 675 },
619 include: [ 676 include: [
620 { 677 {
678 model: Video['sequelize'].models.VideoFile
679 },
680 {
621 model: Video['sequelize'].models.Author, 681 model: Video['sequelize'].models.Author,
622 include: [ 682 include: [
623 { 683 {
@@ -640,7 +700,11 @@ listOwnedAndPopulateAuthorAndTags = function () {
640 where: { 700 where: {
641 remote: false 701 remote: false
642 }, 702 },
643 include: [ Video['sequelize'].models.Author, Video['sequelize'].models.Tag ] 703 include: [
704 Video['sequelize'].models.VideoFile,
705 Video['sequelize'].models.Author,
706 Video['sequelize'].models.Tag
707 ]
644 } 708 }
645 709
646 return Video.findAll(query) 710 return Video.findAll(query)
@@ -653,6 +717,9 @@ listOwnedByAuthor = function (author: string) {
653 }, 717 },
654 include: [ 718 include: [
655 { 719 {
720 model: Video['sequelize'].models.VideoFile
721 },
722 {
656 model: Video['sequelize'].models.Author, 723 model: Video['sequelize'].models.Author,
657 where: { 724 where: {
658 name: author 725 name: author
@@ -672,14 +739,15 @@ loadByUUID = function (uuid: string) {
672 const query = { 739 const query = {
673 where: { 740 where: {
674 uuid 741 uuid
675 } 742 },
743 include: [ Video['sequelize'].models.VideoFile ]
676 } 744 }
677 return Video.findOne(query) 745 return Video.findOne(query)
678} 746}
679 747
680loadAndPopulateAuthor = function (id: number) { 748loadAndPopulateAuthor = function (id: number) {
681 const options = { 749 const options = {
682 include: [ Video['sequelize'].models.Author ] 750 include: [ Video['sequelize'].models.VideoFile, Video['sequelize'].models.Author ]
683 } 751 }
684 752
685 return Video.findById(id, options) 753 return Video.findById(id, options)
@@ -692,7 +760,8 @@ loadAndPopulateAuthorAndPodAndTags = function (id: number) {
692 model: Video['sequelize'].models.Author, 760 model: Video['sequelize'].models.Author,
693 include: [ { model: Video['sequelize'].models.Pod, required: false } ] 761 include: [ { model: Video['sequelize'].models.Pod, required: false } ]
694 }, 762 },
695 Video['sequelize'].models.Tag 763 Video['sequelize'].models.Tag,
764 Video['sequelize'].models.VideoFile
696 ] 765 ]
697 } 766 }
698 767
@@ -709,7 +778,8 @@ loadByUUIDAndPopulateAuthorAndPodAndTags = function (uuid: string) {
709 model: Video['sequelize'].models.Author, 778 model: Video['sequelize'].models.Author,
710 include: [ { model: Video['sequelize'].models.Pod, required: false } ] 779 include: [ { model: Video['sequelize'].models.Pod, required: false } ]
711 }, 780 },
712 Video['sequelize'].models.Tag 781 Video['sequelize'].models.Tag,
782 Video['sequelize'].models.VideoFile
713 ] 783 ]
714 } 784 }
715 785
@@ -733,6 +803,10 @@ searchAndPopulateAuthorAndPodAndTags = function (value: string, field: string, s
733 model: Video['sequelize'].models.Tag 803 model: Video['sequelize'].models.Tag
734 } 804 }
735 805
806 const videoFileInclude: Sequelize.IncludeOptions = {
807 model: Video['sequelize'].models.VideoFile
808 }
809
736 const query: Sequelize.FindOptions = { 810 const query: Sequelize.FindOptions = {
737 distinct: true, 811 distinct: true,
738 where: createBaseVideosWhere(), 812 where: createBaseVideosWhere(),
@@ -743,8 +817,9 @@ searchAndPopulateAuthorAndPodAndTags = function (value: string, field: string, s
743 817
744 // Make an exact search with the magnet 818 // Make an exact search with the magnet
745 if (field === 'magnetUri') { 819 if (field === 'magnetUri') {
746 const infoHash = magnetUtil.decode(value).infoHash 820 videoFileInclude.where = {
747 query.where['infoHash'] = infoHash 821 infoHash: magnetUtil.decode(value).infoHash
822 }
748 } else if (field === 'tags') { 823 } else if (field === 'tags') {
749 const escapedValue = Video['sequelize'].escape('%' + value + '%') 824 const escapedValue = Video['sequelize'].escape('%' + value + '%')
750 query.where['id'].$in = Video['sequelize'].literal( 825 query.where['id'].$in = Video['sequelize'].literal(
@@ -777,7 +852,7 @@ searchAndPopulateAuthorAndPodAndTags = function (value: string, field: string, s
777 } 852 }
778 853
779 query.include = [ 854 query.include = [
780 authorInclude, tagInclude 855 authorInclude, tagInclude, videoFileInclude
781 ] 856 ]
782 857
783 return Video.findAndCountAll(query).then(({ rows, count }) => { 858 return Video.findAndCountAll(query).then(({ rows, count }) => {
@@ -800,56 +875,6 @@ function createBaseVideosWhere () {
800 } 875 }
801} 876}
802 877
803function removeThumbnail (video: VideoInstance) {
804 const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, video.getThumbnailName())
805 return unlinkPromise(thumbnailPath)
806}
807
808function removeFile (video: VideoInstance) {
809 const filePath = join(CONFIG.STORAGE.VIDEOS_DIR, video.getVideoFilename())
810 return unlinkPromise(filePath)
811}
812
813function removeTorrent (video: VideoInstance) {
814 const torrenPath = join(CONFIG.STORAGE.TORRENTS_DIR, video.getTorrentName())
815 return unlinkPromise(torrenPath)
816}
817
818function removePreview (video: VideoInstance) {
819 // Same name than video thumnail
820 return unlinkPromise(CONFIG.STORAGE.PREVIEWS_DIR + video.getPreviewName())
821}
822
823function createTorrentFromVideo (video: VideoInstance, videoPath: string) {
824 const options = {
825 announceList: [
826 [ CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT + '/tracker/socket' ]
827 ],
828 urlList: [
829 CONFIG.WEBSERVER.URL + STATIC_PATHS.WEBSEED + video.getVideoFilename()
830 ]
831 }
832
833 return createTorrentPromise(videoPath, options)
834 .then(torrent => {
835 const filePath = join(CONFIG.STORAGE.TORRENTS_DIR, video.getTorrentName())
836 return writeFilePromise(filePath, torrent).then(() => torrent)
837 })
838 .then(torrent => {
839 const parsedTorrent = parseTorrent(torrent)
840 video.set('infoHash', parsedTorrent.infoHash)
841 return video.validate()
842 })
843}
844
845function createPreview (video: VideoInstance, videoPath: string) {
846 return generateImage(video, videoPath, CONFIG.STORAGE.PREVIEWS_DIR, video.getPreviewName(), null)
847}
848
849function createThumbnail (video: VideoInstance, videoPath: string) {
850 return generateImage(video, videoPath, CONFIG.STORAGE.THUMBNAILS_DIR, video.getThumbnailName(), THUMBNAILS_SIZE)
851}
852
853function generateImage (video: VideoInstance, videoPath: string, folder: string, imageName: string, size: string) { 878function generateImage (video: VideoInstance, videoPath: string, folder: string, imageName: string, size: string) {
854 const options = { 879 const options = {
855 filename: imageName, 880 filename: imageName,
@@ -868,16 +893,3 @@ function generateImage (video: VideoInstance, videoPath: string, folder: string,
868 .thumbnail(options) 893 .thumbnail(options)
869 }) 894 })
870} 895}
871
872function removeFromBlacklist (video: VideoInstance) {
873 // Find the blacklisted video
874 return db.BlacklistedVideo.loadByVideoId(video.id).then(video => {
875 // Not found the video, skip
876 if (!video) {
877 return null
878 }
879
880 // If we found the video, remove it from the blacklist
881 return video.destroy()
882 })
883}
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 () {
121 expect(video.nsfw).to.be.ok 121 expect(video.nsfw).to.be.ok
122 expect(video.description).to.equal('my super description for pod 1') 122 expect(video.description).to.equal('my super description for pod 1')
123 expect(video.podHost).to.equal('localhost:9001') 123 expect(video.podHost).to.equal('localhost:9001')
124 expect(video.magnetUri).to.exist
125 expect(video.duration).to.equal(10) 124 expect(video.duration).to.equal(10)
126 expect(video.tags).to.deep.equal([ 'tag1p1', 'tag2p1' ]) 125 expect(video.tags).to.deep.equal([ 'tag1p1', 'tag2p1' ])
127 expect(miscsUtils.dateIsValid(video.createdAt)).to.be.true 126 expect(miscsUtils.dateIsValid(video.createdAt)).to.be.true
128 expect(miscsUtils.dateIsValid(video.updatedAt)).to.be.true 127 expect(miscsUtils.dateIsValid(video.updatedAt)).to.be.true
129 expect(video.author).to.equal('root') 128 expect(video.author).to.equal('root')
130 129
130 expect(video.files).to.have.lengthOf(1)
131
132 const file = video.files[0]
133 const magnetUri = file.magnetUri
134 expect(file.magnetUri).to.exist
135 expect(file.resolution).to.equal(0)
136 expect(file.resolutionLabel).to.equal('original')
137 expect(file.size).to.equal(572456)
138
131 if (server.url !== 'http://localhost:9001') { 139 if (server.url !== 'http://localhost:9001') {
132 expect(video.isLocal).to.be.false 140 expect(video.isLocal).to.be.false
133 } else { 141 } else {
@@ -136,9 +144,9 @@ describe('Test multiple pods', function () {
136 144
137 // All pods should have the same magnet Uri 145 // All pods should have the same magnet Uri
138 if (baseMagnet === null) { 146 if (baseMagnet === null) {
139 baseMagnet = video.magnetUri 147 baseMagnet = magnetUri
140 } else { 148 } else {
141 expect(video.magnetUri).to.equal.magnetUri 149 expect(baseMagnet).to.equal(magnetUri)
142 } 150 }
143 151
144 videosUtils.testVideoImage(server.url, 'video_short1.webm', video.thumbnailPath, function (err, test) { 152 videosUtils.testVideoImage(server.url, 'video_short1.webm', video.thumbnailPath, function (err, test) {
@@ -198,13 +206,21 @@ describe('Test multiple pods', function () {
198 expect(video.nsfw).to.be.true 206 expect(video.nsfw).to.be.true
199 expect(video.description).to.equal('my super description for pod 2') 207 expect(video.description).to.equal('my super description for pod 2')
200 expect(video.podHost).to.equal('localhost:9002') 208 expect(video.podHost).to.equal('localhost:9002')
201 expect(video.magnetUri).to.exist
202 expect(video.duration).to.equal(5) 209 expect(video.duration).to.equal(5)
203 expect(video.tags).to.deep.equal([ 'tag1p2', 'tag2p2', 'tag3p2' ]) 210 expect(video.tags).to.deep.equal([ 'tag1p2', 'tag2p2', 'tag3p2' ])
204 expect(miscsUtils.dateIsValid(video.createdAt)).to.be.true 211 expect(miscsUtils.dateIsValid(video.createdAt)).to.be.true
205 expect(miscsUtils.dateIsValid(video.updatedAt)).to.be.true 212 expect(miscsUtils.dateIsValid(video.updatedAt)).to.be.true
206 expect(video.author).to.equal('root') 213 expect(video.author).to.equal('root')
207 214
215 expect(video.files).to.have.lengthOf(1)
216
217 const file = video.files[0]
218 const magnetUri = file.magnetUri
219 expect(file.magnetUri).to.exist
220 expect(file.resolution).to.equal(0)
221 expect(file.resolutionLabel).to.equal('original')
222 expect(file.size).to.equal(942961)
223
208 if (server.url !== 'http://localhost:9002') { 224 if (server.url !== 'http://localhost:9002') {
209 expect(video.isLocal).to.be.false 225 expect(video.isLocal).to.be.false
210 } else { 226 } else {
@@ -213,9 +229,9 @@ describe('Test multiple pods', function () {
213 229
214 // All pods should have the same magnet Uri 230 // All pods should have the same magnet Uri
215 if (baseMagnet === null) { 231 if (baseMagnet === null) {
216 baseMagnet = video.magnetUri 232 baseMagnet = magnetUri
217 } else { 233 } else {
218 expect(video.magnetUri).to.equal.magnetUri 234 expect(baseMagnet).to.equal(magnetUri)
219 } 235 }
220 236
221 videosUtils.testVideoImage(server.url, 'video_short2.webm', video.thumbnailPath, function (err, test) { 237 videosUtils.testVideoImage(server.url, 'video_short2.webm', video.thumbnailPath, function (err, test) {
@@ -297,13 +313,21 @@ describe('Test multiple pods', function () {
297 expect(video1.nsfw).to.be.ok 313 expect(video1.nsfw).to.be.ok
298 expect(video1.description).to.equal('my super description for pod 3') 314 expect(video1.description).to.equal('my super description for pod 3')
299 expect(video1.podHost).to.equal('localhost:9003') 315 expect(video1.podHost).to.equal('localhost:9003')
300 expect(video1.magnetUri).to.exist
301 expect(video1.duration).to.equal(5) 316 expect(video1.duration).to.equal(5)
302 expect(video1.tags).to.deep.equal([ 'tag1p3' ]) 317 expect(video1.tags).to.deep.equal([ 'tag1p3' ])
303 expect(video1.author).to.equal('root') 318 expect(video1.author).to.equal('root')
304 expect(miscsUtils.dateIsValid(video1.createdAt)).to.be.true 319 expect(miscsUtils.dateIsValid(video1.createdAt)).to.be.true
305 expect(miscsUtils.dateIsValid(video1.updatedAt)).to.be.true 320 expect(miscsUtils.dateIsValid(video1.updatedAt)).to.be.true
306 321
322 expect(video1.files).to.have.lengthOf(1)
323
324 const file1 = video1.files[0]
325 const magnetUri1 = file1.magnetUri
326 expect(file1.magnetUri).to.exist
327 expect(file1.resolution).to.equal(0)
328 expect(file1.resolutionLabel).to.equal('original')
329 expect(file1.size).to.equal(292677)
330
307 expect(video2.name).to.equal('my super name for pod 3-2') 331 expect(video2.name).to.equal('my super name for pod 3-2')
308 expect(video2.category).to.equal(7) 332 expect(video2.category).to.equal(7)
309 expect(video2.categoryLabel).to.equal('Gaming') 333 expect(video2.categoryLabel).to.equal('Gaming')
@@ -314,13 +338,21 @@ describe('Test multiple pods', function () {
314 expect(video2.nsfw).to.be.false 338 expect(video2.nsfw).to.be.false
315 expect(video2.description).to.equal('my super description for pod 3-2') 339 expect(video2.description).to.equal('my super description for pod 3-2')
316 expect(video2.podHost).to.equal('localhost:9003') 340 expect(video2.podHost).to.equal('localhost:9003')
317 expect(video2.magnetUri).to.exist
318 expect(video2.duration).to.equal(5) 341 expect(video2.duration).to.equal(5)
319 expect(video2.tags).to.deep.equal([ 'tag2p3', 'tag3p3', 'tag4p3' ]) 342 expect(video2.tags).to.deep.equal([ 'tag2p3', 'tag3p3', 'tag4p3' ])
320 expect(video2.author).to.equal('root') 343 expect(video2.author).to.equal('root')
321 expect(miscsUtils.dateIsValid(video2.createdAt)).to.be.true 344 expect(miscsUtils.dateIsValid(video2.createdAt)).to.be.true
322 expect(miscsUtils.dateIsValid(video2.updatedAt)).to.be.true 345 expect(miscsUtils.dateIsValid(video2.updatedAt)).to.be.true
323 346
347 expect(video2.files).to.have.lengthOf(1)
348
349 const file2 = video2.files[0]
350 const magnetUri2 = file2.magnetUri
351 expect(file2.magnetUri).to.exist
352 expect(file2.resolution).to.equal(0)
353 expect(file2.resolutionLabel).to.equal('original')
354 expect(file2.size).to.equal(218910)
355
324 if (server.url !== 'http://localhost:9003') { 356 if (server.url !== 'http://localhost:9003') {
325 expect(video1.isLocal).to.be.false 357 expect(video1.isLocal).to.be.false
326 expect(video2.isLocal).to.be.false 358 expect(video2.isLocal).to.be.false
@@ -331,9 +363,9 @@ describe('Test multiple pods', function () {
331 363
332 // All pods should have the same magnet Uri 364 // All pods should have the same magnet Uri
333 if (baseMagnet === null) { 365 if (baseMagnet === null) {
334 baseMagnet = video2.magnetUri 366 baseMagnet = magnetUri2
335 } else { 367 } else {
336 expect(video2.magnetUri).to.equal.magnetUri 368 expect(baseMagnet).to.equal(magnetUri2)
337 } 369 }
338 370
339 videosUtils.testVideoImage(server.url, 'video_short3.webm', video1.thumbnailPath, function (err, test) { 371 videosUtils.testVideoImage(server.url, 'video_short3.webm', video1.thumbnailPath, function (err, test) {
@@ -366,7 +398,7 @@ describe('Test multiple pods', function () {
366 toRemove.push(res.body.data[2]) 398 toRemove.push(res.body.data[2])
367 toRemove.push(res.body.data[3]) 399 toRemove.push(res.body.data[3])
368 400
369 webtorrent.add(video.magnetUri, function (torrent) { 401 webtorrent.add(video.files[0].magnetUri, function (torrent) {
370 expect(torrent.files).to.exist 402 expect(torrent.files).to.exist
371 expect(torrent.files.length).to.equal(1) 403 expect(torrent.files.length).to.equal(1)
372 expect(torrent.files[0].path).to.exist.and.to.not.equal('') 404 expect(torrent.files[0].path).to.exist.and.to.not.equal('')
@@ -385,7 +417,7 @@ describe('Test multiple pods', function () {
385 417
386 const video = res.body.data[1] 418 const video = res.body.data[1]
387 419
388 webtorrent.add(video.magnetUri, function (torrent) { 420 webtorrent.add(video.files[0].magnetUri, function (torrent) {
389 expect(torrent.files).to.exist 421 expect(torrent.files).to.exist
390 expect(torrent.files.length).to.equal(1) 422 expect(torrent.files.length).to.equal(1)
391 expect(torrent.files[0].path).to.exist.and.to.not.equal('') 423 expect(torrent.files[0].path).to.exist.and.to.not.equal('')
@@ -404,7 +436,7 @@ describe('Test multiple pods', function () {
404 436
405 const video = res.body.data[2] 437 const video = res.body.data[2]
406 438
407 webtorrent.add(video.magnetUri, function (torrent) { 439 webtorrent.add(video.files[0].magnetUri, function (torrent) {
408 expect(torrent.files).to.exist 440 expect(torrent.files).to.exist
409 expect(torrent.files.length).to.equal(1) 441 expect(torrent.files.length).to.equal(1)
410 expect(torrent.files[0].path).to.exist.and.to.not.equal('') 442 expect(torrent.files[0].path).to.exist.and.to.not.equal('')
@@ -423,7 +455,7 @@ describe('Test multiple pods', function () {
423 455
424 const video = res.body.data[3] 456 const video = res.body.data[3]
425 457
426 webtorrent.add(video.magnetUri, function (torrent) { 458 webtorrent.add(video.files[0].magnetUri, function (torrent) {
427 expect(torrent.files).to.exist 459 expect(torrent.files).to.exist
428 expect(torrent.files.length).to.equal(1) 460 expect(torrent.files.length).to.equal(1)
429 expect(torrent.files[0].path).to.exist.and.to.not.equal('') 461 expect(torrent.files[0].path).to.exist.and.to.not.equal('')
@@ -700,11 +732,18 @@ describe('Test multiple pods', function () {
700 expect(videoUpdated.tags).to.deep.equal([ 'tagup1', 'tagup2' ]) 732 expect(videoUpdated.tags).to.deep.equal([ 'tagup1', 'tagup2' ])
701 expect(miscsUtils.dateIsValid(videoUpdated.updatedAt, 20000)).to.be.true 733 expect(miscsUtils.dateIsValid(videoUpdated.updatedAt, 20000)).to.be.true
702 734
735 const file = videoUpdated.files[0]
736 const magnetUri = file.magnetUri
737 expect(file.magnetUri).to.exist
738 expect(file.resolution).to.equal(0)
739 expect(file.resolutionLabel).to.equal('original')
740 expect(file.size).to.equal(292677)
741
703 videosUtils.testVideoImage(server.url, 'video_short3.webm', videoUpdated.thumbnailPath, function (err, test) { 742 videosUtils.testVideoImage(server.url, 'video_short3.webm', videoUpdated.thumbnailPath, function (err, test) {
704 if (err) throw err 743 if (err) throw err
705 expect(test).to.equal(true) 744 expect(test).to.equal(true)
706 745
707 webtorrent.add(videoUpdated.magnetUri, function (torrent) { 746 webtorrent.add(videoUpdated.files[0].magnetUri, function (torrent) {
708 expect(torrent.files).to.exist 747 expect(torrent.files).to.exist
709 expect(torrent.files.length).to.equal(1) 748 expect(torrent.files.length).to.equal(1)
710 expect(torrent.files[0].path).to.exist.and.to.not.equal('') 749 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 () {
129 expect(video.nsfw).to.be.ok 129 expect(video.nsfw).to.be.ok
130 expect(video.description).to.equal('my super description') 130 expect(video.description).to.equal('my super description')
131 expect(video.podHost).to.equal('localhost:9001') 131 expect(video.podHost).to.equal('localhost:9001')
132 expect(video.magnetUri).to.exist
133 expect(video.author).to.equal('root') 132 expect(video.author).to.equal('root')
134 expect(video.isLocal).to.be.true 133 expect(video.isLocal).to.be.true
135 expect(video.tags).to.deep.equal([ 'tag1', 'tag2', 'tag3' ]) 134 expect(video.tags).to.deep.equal([ 'tag1', 'tag2', 'tag3' ])
136 expect(miscsUtils.dateIsValid(video.createdAt)).to.be.true 135 expect(miscsUtils.dateIsValid(video.createdAt)).to.be.true
137 expect(miscsUtils.dateIsValid(video.updatedAt)).to.be.true 136 expect(miscsUtils.dateIsValid(video.updatedAt)).to.be.true
138 137
138 expect(video.files).to.have.lengthOf(1)
139
140 const file = video.files[0]
141 const magnetUri = file.magnetUri
142 expect(file.magnetUri).to.exist
143 expect(file.resolution).to.equal(0)
144 expect(file.resolutionLabel).to.equal('original')
145 expect(file.size).to.equal(218910)
146
139 videosUtils.testVideoImage(server.url, 'video_short.webm', video.thumbnailPath, function (err, test) { 147 videosUtils.testVideoImage(server.url, 'video_short.webm', video.thumbnailPath, function (err, test) {
140 if (err) throw err 148 if (err) throw err
141 expect(test).to.equal(true) 149 expect(test).to.equal(true)
@@ -143,7 +151,7 @@ describe('Test a single pod', function () {
143 videoId = video.id 151 videoId = video.id
144 videoUUID = video.uuid 152 videoUUID = video.uuid
145 153
146 webtorrent.add(video.magnetUri, function (torrent) { 154 webtorrent.add(magnetUri, function (torrent) {
147 expect(torrent.files).to.exist 155 expect(torrent.files).to.exist
148 expect(torrent.files.length).to.equal(1) 156 expect(torrent.files.length).to.equal(1)
149 expect(torrent.files[0].path).to.exist.and.to.not.equal('') 157 expect(torrent.files[0].path).to.exist.and.to.not.equal('')
@@ -172,13 +180,21 @@ describe('Test a single pod', function () {
172 expect(video.nsfw).to.be.ok 180 expect(video.nsfw).to.be.ok
173 expect(video.description).to.equal('my super description') 181 expect(video.description).to.equal('my super description')
174 expect(video.podHost).to.equal('localhost:9001') 182 expect(video.podHost).to.equal('localhost:9001')
175 expect(video.magnetUri).to.exist
176 expect(video.author).to.equal('root') 183 expect(video.author).to.equal('root')
177 expect(video.isLocal).to.be.true 184 expect(video.isLocal).to.be.true
178 expect(video.tags).to.deep.equal([ 'tag1', 'tag2', 'tag3' ]) 185 expect(video.tags).to.deep.equal([ 'tag1', 'tag2', 'tag3' ])
179 expect(miscsUtils.dateIsValid(video.createdAt)).to.be.true 186 expect(miscsUtils.dateIsValid(video.createdAt)).to.be.true
180 expect(miscsUtils.dateIsValid(video.updatedAt)).to.be.true 187 expect(miscsUtils.dateIsValid(video.updatedAt)).to.be.true
181 188
189 expect(video.files).to.have.lengthOf(1)
190
191 const file = video.files[0]
192 const magnetUri = file.magnetUri
193 expect(file.magnetUri).to.exist
194 expect(file.resolution).to.equal(0)
195 expect(file.resolutionLabel).to.equal('original')
196 expect(file.size).to.equal(218910)
197
182 videosUtils.testVideoImage(server.url, 'video_short.webm', video.thumbnailPath, function (err, test) { 198 videosUtils.testVideoImage(server.url, 'video_short.webm', video.thumbnailPath, function (err, test) {
183 if (err) throw err 199 if (err) throw err
184 expect(test).to.equal(true) 200 expect(test).to.equal(true)
@@ -240,6 +256,15 @@ describe('Test a single pod', function () {
240 expect(miscsUtils.dateIsValid(video.createdAt)).to.be.true 256 expect(miscsUtils.dateIsValid(video.createdAt)).to.be.true
241 expect(miscsUtils.dateIsValid(video.updatedAt)).to.be.true 257 expect(miscsUtils.dateIsValid(video.updatedAt)).to.be.true
242 258
259 expect(video.files).to.have.lengthOf(1)
260
261 const file = video.files[0]
262 const magnetUri = file.magnetUri
263 expect(file.magnetUri).to.exist
264 expect(file.resolution).to.equal(0)
265 expect(file.resolutionLabel).to.equal('original')
266 expect(file.size).to.equal(218910)
267
243 videosUtils.testVideoImage(server.url, 'video_short.webm', video.thumbnailPath, function (err, test) { 268 videosUtils.testVideoImage(server.url, 'video_short.webm', video.thumbnailPath, function (err, test) {
244 if (err) throw err 269 if (err) throw err
245 expect(test).to.equal(true) 270 expect(test).to.equal(true)
@@ -302,6 +327,15 @@ describe('Test a single pod', function () {
302 expect(miscsUtils.dateIsValid(video.createdAt)).to.be.true 327 expect(miscsUtils.dateIsValid(video.createdAt)).to.be.true
303 expect(miscsUtils.dateIsValid(video.updatedAt)).to.be.true 328 expect(miscsUtils.dateIsValid(video.updatedAt)).to.be.true
304 329
330 expect(video.files).to.have.lengthOf(1)
331
332 const file = video.files[0]
333 const magnetUri = file.magnetUri
334 expect(file.magnetUri).to.exist
335 expect(file.resolution).to.equal(0)
336 expect(file.resolutionLabel).to.equal('original')
337 expect(file.size).to.equal(218910)
338
305 videosUtils.testVideoImage(server.url, 'video_short.webm', video.thumbnailPath, function (err, test) { 339 videosUtils.testVideoImage(server.url, 'video_short.webm', video.thumbnailPath, function (err, test) {
306 if (err) throw err 340 if (err) throw err
307 expect(test).to.equal(true) 341 expect(test).to.equal(true)
@@ -564,7 +598,7 @@ describe('Test a single pod', function () {
564 598
565 it('Should search the right magnetUri video', function (done) { 599 it('Should search the right magnetUri video', function (done) {
566 const video = videosListBase[0] 600 const video = videosListBase[0]
567 videosUtils.searchVideoWithPagination(server.url, encodeURIComponent(video.magnetUri), 'magnetUri', 0, 15, function (err, res) { 601 videosUtils.searchVideoWithPagination(server.url, encodeURIComponent(video.files[0].magnetUri), 'magnetUri', 0, 15, function (err, res) {
568 if (err) throw err 602 if (err) throw err
569 603
570 const videos = res.body.data 604 const videos = res.body.data
@@ -650,11 +684,20 @@ describe('Test a single pod', function () {
650 expect(miscsUtils.dateIsValid(video.createdAt)).to.be.true 684 expect(miscsUtils.dateIsValid(video.createdAt)).to.be.true
651 expect(miscsUtils.dateIsValid(video.updatedAt)).to.be.true 685 expect(miscsUtils.dateIsValid(video.updatedAt)).to.be.true
652 686
687 expect(video.files).to.have.lengthOf(1)
688
689 const file = video.files[0]
690 const magnetUri = file.magnetUri
691 expect(file.magnetUri).to.exist
692 expect(file.resolution).to.equal(0)
693 expect(file.resolutionLabel).to.equal('original')
694 expect(file.size).to.equal(292677)
695
653 videosUtils.testVideoImage(server.url, 'video_short3.webm', video.thumbnailPath, function (err, test) { 696 videosUtils.testVideoImage(server.url, 'video_short3.webm', video.thumbnailPath, function (err, test) {
654 if (err) throw err 697 if (err) throw err
655 expect(test).to.equal(true) 698 expect(test).to.equal(true)
656 699
657 webtorrent.add(video.magnetUri, function (torrent) { 700 webtorrent.add(magnetUri, function (torrent) {
658 expect(torrent.files).to.exist 701 expect(torrent.files).to.exist
659 expect(torrent.files.length).to.equal(1) 702 expect(torrent.files.length).to.equal(1)
660 expect(torrent.files[0].path).to.exist.and.to.not.equal('') 703 expect(torrent.files[0].path).to.exist.and.to.not.equal('')
@@ -694,6 +737,15 @@ describe('Test a single pod', function () {
694 expect(miscsUtils.dateIsValid(video.createdAt)).to.be.true 737 expect(miscsUtils.dateIsValid(video.createdAt)).to.be.true
695 expect(miscsUtils.dateIsValid(video.updatedAt)).to.be.true 738 expect(miscsUtils.dateIsValid(video.updatedAt)).to.be.true
696 739
740 expect(video.files).to.have.lengthOf(1)
741
742 const file = video.files[0]
743 const magnetUri = file.magnetUri
744 expect(file.magnetUri).to.exist
745 expect(file.resolution).to.equal(0)
746 expect(file.resolutionLabel).to.equal('original')
747 expect(file.size).to.equal(292677)
748
697 done() 749 done()
698 }) 750 })
699 }) 751 })
@@ -728,6 +780,15 @@ describe('Test a single pod', function () {
728 expect(miscsUtils.dateIsValid(video.createdAt)).to.be.true 780 expect(miscsUtils.dateIsValid(video.createdAt)).to.be.true
729 expect(miscsUtils.dateIsValid(video.updatedAt)).to.be.true 781 expect(miscsUtils.dateIsValid(video.updatedAt)).to.be.true
730 782
783 expect(video.files).to.have.lengthOf(1)
784
785 const file = video.files[0]
786 const magnetUri = file.magnetUri
787 expect(file.magnetUri).to.exist
788 expect(file.resolution).to.equal(0)
789 expect(file.resolutionLabel).to.equal('original')
790 expect(file.size).to.equal(292677)
791
731 done() 792 done()
732 }) 793 })
733 }) 794 })
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 () {
56 if (err) throw err 56 if (err) throw err
57 57
58 const video = res.body.data[0] 58 const video = res.body.data[0]
59 expect(video.magnetUri).to.match(/\.webm/) 59 const magnetUri = video.files[0].magnetUri
60 expect(magnetUri).to.match(/\.webm/)
60 61
61 webtorrent.add(video.magnetUri, function (torrent) { 62 webtorrent.add(magnetUri, function (torrent) {
62 expect(torrent.files).to.exist 63 expect(torrent.files).to.exist
63 expect(torrent.files.length).to.equal(1) 64 expect(torrent.files.length).to.equal(1)
64 expect(torrent.files[0].path).match(/\.webm$/) 65 expect(torrent.files[0].path).match(/\.webm$/)
@@ -86,9 +87,10 @@ describe('Test video transcoding', function () {
86 if (err) throw err 87 if (err) throw err
87 88
88 const video = res.body.data[0] 89 const video = res.body.data[0]
89 expect(video.magnetUri).to.match(/\.mp4/) 90 const magnetUri = video.files[0].magnetUri
91 expect(magnetUri).to.match(/\.mp4/)
90 92
91 webtorrent.add(video.magnetUri, function (torrent) { 93 webtorrent.add(magnetUri, function (torrent) {
92 expect(torrent.files).to.exist 94 expect(torrent.files).to.exist
93 expect(torrent.files.length).to.equal(1) 95 expect(torrent.files.length).to.equal(1)
94 expect(torrent.files[0].path).match(/\.mp4$/) 96 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 {
5 author: string 5 author: string
6 tags: string[] 6 tags: string[]
7 name: string 7 name: string
8 extname: string
9 infoHash: string
10 category: number 8 category: number
11 licence: number 9 licence: number
12 language: number 10 language: number
@@ -19,6 +17,12 @@ export interface RemoteVideoCreateData {
19 likes: number 17 likes: number
20 dislikes: number 18 dislikes: number
21 thumbnailData: string 19 thumbnailData: string
20 files: {
21 infoHash: string
22 extname: string
23 resolution: number
24 size: number
25 }[]
22} 26}
23 27
24export interface RemoteVideoCreateRequest extends RemoteVideoRequest { 28export 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 {
15 views: number 15 views: number
16 likes: number 16 likes: number
17 dislikes: number 17 dislikes: number
18 files: {
19 infoHash: string
20 extname: string
21 resolution: number
22 size: number
23 }[]
18} 24}
19 25
20export interface RemoteVideoUpdateRequest { 26export 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 @@
1export interface VideoFile {
2 magnetUri: string
3 resolution: number
4 resolutionLabel: string
5 size: number // Bytes
6}
7
1export interface Video { 8export interface Video {
2 id: number 9 id: number
3 uuid: string 10 uuid: string
@@ -12,7 +19,6 @@ export interface Video {
12 description: string 19 description: string
13 duration: number 20 duration: number
14 isLocal: boolean 21 isLocal: boolean
15 magnetUri: string
16 name: string 22 name: string
17 podHost: string 23 podHost: string
18 tags: string[] 24 tags: string[]
@@ -22,4 +28,5 @@ export interface Video {
22 likes: number 28 likes: number
23 dislikes: number 29 dislikes: number
24 nsfw: boolean 30 nsfw: boolean
31 files: VideoFile[]
25} 32}