diff options
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 @@ | |||
1 | import { Video as VideoServerModel } from '../../../../../shared' | 1 | import { Video as VideoServerModel, VideoFile } from '../../../../../shared' |
2 | import { User } from '../../shared' | 2 | import { User } from '../../shared' |
3 | 3 | ||
4 | export class Video implements VideoServerModel { | 4 | export 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 | |||
3 | NODE_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 @@ | |||
1 | import { readFileSync, writeFileSync } from 'fs' | 1 | import { readFileSync, writeFileSync } from 'fs' |
2 | import { join } from 'path' | ||
2 | import * as parseTorrent from 'parse-torrent' | 3 | import * as parseTorrent from 'parse-torrent' |
3 | 4 | ||
4 | import { CONFIG, STATIC_PATHS } from '../server/initializers/constants' | 5 | import { 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) |
@@ -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) |
28 | import { logger } from './server/helpers/logger' | 28 | import { logger } from './server/helpers/logger' |
29 | import { API_VERSION, CONFIG } from './server/initializers/constants' | 29 | import { API_VERSION, CONFIG, STATIC_PATHS } from './server/initializers/constants' |
30 | // Initialize database and models | 30 | // Initialize database and models |
31 | import { database as db } from './server/initializers/database' | 31 | import { database as db } from './server/initializers/database' |
32 | db.init(false).then(() => onDatabaseInitDone()) | 32 | db.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 |
59 | if (isTestInstance()) { | 59 | if (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 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import * as Promise from 'bluebird' | 2 | import * as Promise from 'bluebird' |
3 | import * as multer from 'multer' | 3 | import * as multer from 'multer' |
4 | import * as path from 'path' | 4 | import { extname, join } from 'path' |
5 | 5 | ||
6 | import { database as db } from '../../../initializers/database' | 6 | import { database as db } from '../../../initializers/database' |
7 | import { | 7 | import { |
@@ -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' |
21 | import { | 22 | import { |
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 | ||
158 | function addVideo (req: express.Request, res: express.Response, videoFile: Express.Multer.File) { | 159 | function 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 | ||
32 | const ENDPOINT_ACTIONS = REQUEST_ENDPOINT_ACTIONS[REQUEST_ENDPOINTS.VIDEOS] | 33 | const 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 | ||
134 | function isRequestTypeAddValid (value: string) { | 143 | function isRequestTypeAddValid (value: string) { |
diff --git a/server/helpers/custom-validators/videos.ts b/server/helpers/custom-validators/videos.ts index 62132acb1..139fa760f 100644 --- a/server/helpers/custom-validators/videos.ts +++ b/server/helpers/custom-validators/videos.ts | |||
@@ -7,7 +7,8 @@ import { | |||
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' |
12 | import { isUserUsernameValid } from './users' | 13 | import { isUserUsernameValid } from './users' |
13 | import { isArray, exists } from './misc' | 14 | import { 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 | ||
56 | function isVideoExtnameValid (value: string) { | ||
57 | return VIDEOS_CONSTRAINTS_FIELDS.EXTNAME.indexOf(value) !== -1 | ||
58 | } | ||
59 | |||
60 | function isVideoInfoHashValid (value: string) { | ||
61 | return exists(value) && validator.isLength(value, VIDEOS_CONSTRAINTS_FIELDS.INFO_HASH) | ||
62 | } | ||
63 | |||
64 | function isVideoNameValid (value: string) { | 57 | function 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 | ||
124 | function isVideoFileSizeValid (value: string) { | ||
125 | return exists(value) && validator.isInt(value + '', VIDEOS_CONSTRAINTS_FIELDS.FILE_SIZE) | ||
126 | } | ||
127 | |||
128 | function isVideoFileResolutionValid (value: string) { | ||
129 | return VIDEO_FILE_RESOLUTIONS[value] !== undefined | ||
130 | } | ||
131 | |||
132 | function isVideoFileExtnameValid (value: string) { | ||
133 | return VIDEOS_CONSTRAINTS_FIELDS.EXTNAME.indexOf(value) !== -1 | ||
134 | } | ||
135 | |||
136 | function isVideoFileInfoHashValid (value: string) { | ||
137 | return exists(value) && validator.isLength(value, VIDEOS_CONSTRAINTS_FIELDS.INFO_HASH) | ||
138 | } | ||
139 | |||
131 | // --------------------------------------------------------------------------- | 140 | // --------------------------------------------------------------------------- |
132 | 141 | ||
133 | export { | 142 | export { |
@@ -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 | ||
160 | declare global { | 171 | declare 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 | ||
18 | const LAST_MIGRATION_VERSION = 55 | 18 | const 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 | ||
180 | const 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 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | import * as Promise from 'bluebird' | ||
3 | |||
4 | function 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 | |||
27 | function down (options) { | ||
28 | throw new Error('Not implemented.') | ||
29 | } | ||
30 | |||
31 | export { | ||
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 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | import * as Promise from 'bluebird' | ||
3 | import { stat } from 'fs' | ||
4 | |||
5 | import { VideoInstance } from '../../models' | ||
6 | |||
7 | function 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 | |||
39 | function down (options) { | ||
40 | throw new Error('Not implemented.') | ||
41 | } | ||
42 | |||
43 | export { | ||
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 | ||
6 | function process (data: { videoUUID: string }) { | 6 | function 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' | |||
3 | export * from './video-abuse-interface' | 3 | export * from './video-abuse-interface' |
4 | export * from './video-blacklist-interface' | 4 | export * from './video-blacklist-interface' |
5 | export * from './video-tag-interface' | 5 | export * from './video-tag-interface' |
6 | export * from './video-file-interface' | ||
6 | export * from './video-interface' | 7 | export * from './video-interface' |
diff --git a/server/models/video/video-file-interface.ts b/server/models/video/video-file-interface.ts new file mode 100644 index 000000000..c9fb8b8ae --- /dev/null +++ b/server/models/video/video-file-interface.ts | |||
@@ -0,0 +1,24 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | |||
3 | export namespace VideoFileMethods { | ||
4 | } | ||
5 | |||
6 | export interface VideoFileClass { | ||
7 | } | ||
8 | |||
9 | export interface VideoFileAttributes { | ||
10 | resolution: number | ||
11 | size: number | ||
12 | infoHash?: string | ||
13 | extname: string | ||
14 | |||
15 | videoId?: number | ||
16 | } | ||
17 | |||
18 | export interface VideoFileInstance extends VideoFileClass, VideoFileAttributes, Sequelize.Instance<VideoFileAttributes> { | ||
19 | id: number | ||
20 | createdAt: Date | ||
21 | updatedAt: Date | ||
22 | } | ||
23 | |||
24 | export interface VideoFileModel extends VideoFileClass, Sequelize.Model<VideoFileInstance, VideoFileAttributes> {} | ||
diff --git a/server/models/video/video-file.ts b/server/models/video/video-file.ts new file mode 100644 index 000000000..09a30d7e0 --- /dev/null +++ b/server/models/video/video-file.ts | |||
@@ -0,0 +1,89 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | import { values } from 'lodash' | ||
3 | |||
4 | import { CONSTRAINTS_FIELDS } from '../../initializers' | ||
5 | import { | ||
6 | isVideoFileResolutionValid, | ||
7 | isVideoFileSizeValid, | ||
8 | isVideoFileInfoHashValid | ||
9 | } from '../../helpers' | ||
10 | |||
11 | import { addMethodsToModel } from '../utils' | ||
12 | import { | ||
13 | VideoFileInstance, | ||
14 | VideoFileAttributes | ||
15 | } from './video-file-interface' | ||
16 | |||
17 | let VideoFile: Sequelize.Model<VideoFileInstance, VideoFileAttributes> | ||
18 | |||
19 | export 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 | |||
79 | function 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 | ||
4 | import { AuthorInstance } from './author-interface' | 4 | import { AuthorInstance } from './author-interface' |
5 | import { TagAttributes, TagInstance } from './tag-interface' | 5 | import { TagAttributes, TagInstance } from './tag-interface' |
6 | import { 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 |
8 | import { Video as FormatedVideo } from '../../../shared/models/videos/video.model' | 9 | import { Video as FormatedVideo } from '../../../shared/models/videos/video.model' |
9 | import { ResultList } from '../../../shared/models/result-list.model' | 10 | import { ResultList } from '../../../shared/models/result-list.model' |
10 | 11 | ||
12 | export type FormatedRemoteVideoFile = { | ||
13 | infoHash: string | ||
14 | resolution: number | ||
15 | extname: string | ||
16 | size: number | ||
17 | } | ||
18 | |||
11 | export type FormatedAddRemoteVideo = { | 19 | export 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 | ||
32 | export type FormatedUpdateRemoteVideo = { | 39 | export 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 | ||
52 | export namespace VideoMethods { | 58 | export 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 | ||
91 | export interface VideoClass { | 107 | export 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 { | |||
118 | export interface VideoAttributes { | 123 | export 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 | ||
138 | export interface VideoInstance extends VideoClass, VideoAttributes, Sequelize.Instance<VideoAttributes> { | 142 | export 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 | ||
157 | export interface VideoModel extends VideoClass, Sequelize.Model<VideoInstance, VideoAttributes> {} | 170 | export 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' | |||
2 | const Buffer = safeBuffer.Buffer | 2 | const Buffer = safeBuffer.Buffer |
3 | import * as ffmpeg from 'fluent-ffmpeg' | 3 | import * as ffmpeg from 'fluent-ffmpeg' |
4 | import * as magnetUtil from 'magnet-uri' | 4 | import * as magnetUtil from 'magnet-uri' |
5 | import { map, values } from 'lodash' | 5 | import { map } from 'lodash' |
6 | import * as parseTorrent from 'parse-torrent' | 6 | import * as parseTorrent from 'parse-torrent' |
7 | import { join } from 'path' | 7 | import { join } from 'path' |
8 | import * as Sequelize from 'sequelize' | 8 | import * as Sequelize from 'sequelize' |
9 | import * as Promise from 'bluebird' | 9 | import * as Promise from 'bluebird' |
10 | 10 | ||
11 | import { database as db } from '../../initializers/database' | ||
12 | import { TagInstance } from './tag-interface' | 11 | import { TagInstance } from './tag-interface' |
13 | import { | 12 | import { |
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' |
29 | import { | 27 | import { |
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' |
39 | import { JobScheduler, removeVideoToFriends } from '../../lib' | 37 | import { removeVideoToFriends } from '../../lib' |
38 | import { VideoFileInstance } from './video-file-interface' | ||
40 | 39 | ||
41 | import { addMethodsToModel, getSort } from '../utils' | 40 | import { addMethodsToModel, getSort } from '../utils' |
42 | import { | 41 | import { |
@@ -51,12 +50,16 @@ let generateMagnetUri: VideoMethods.GenerateMagnetUri | |||
51 | let getVideoFilename: VideoMethods.GetVideoFilename | 50 | let getVideoFilename: VideoMethods.GetVideoFilename |
52 | let getThumbnailName: VideoMethods.GetThumbnailName | 51 | let getThumbnailName: VideoMethods.GetThumbnailName |
53 | let getPreviewName: VideoMethods.GetPreviewName | 52 | let getPreviewName: VideoMethods.GetPreviewName |
54 | let getTorrentName: VideoMethods.GetTorrentName | 53 | let getTorrentFileName: VideoMethods.GetTorrentFileName |
55 | let isOwned: VideoMethods.IsOwned | 54 | let isOwned: VideoMethods.IsOwned |
56 | let toFormatedJSON: VideoMethods.ToFormatedJSON | 55 | let toFormatedJSON: VideoMethods.ToFormatedJSON |
57 | let toAddRemoteJSON: VideoMethods.ToAddRemoteJSON | 56 | let toAddRemoteJSON: VideoMethods.ToAddRemoteJSON |
58 | let toUpdateRemoteJSON: VideoMethods.ToUpdateRemoteJSON | 57 | let toUpdateRemoteJSON: VideoMethods.ToUpdateRemoteJSON |
59 | let transcodeVideofile: VideoMethods.TranscodeVideofile | 58 | let transcodeVideofile: VideoMethods.TranscodeVideofile |
59 | let createPreview: VideoMethods.CreatePreview | ||
60 | let createThumbnail: VideoMethods.CreateThumbnail | ||
61 | let getVideoFilePath: VideoMethods.GetVideoFilePath | ||
62 | let createTorrentAndSetInfoHash: VideoMethods.CreateTorrentAndSetInfoHash | ||
60 | 63 | ||
61 | let generateThumbnailFromData: VideoMethods.GenerateThumbnailFromData | 64 | let generateThumbnailFromData: VideoMethods.GenerateThumbnailFromData |
62 | let getDurationFromFile: VideoMethods.GetDurationFromFile | 65 | let getDurationFromFile: VideoMethods.GetDurationFromFile |
@@ -71,6 +74,10 @@ let loadAndPopulateAuthor: VideoMethods.LoadAndPopulateAuthor | |||
71 | let loadAndPopulateAuthorAndPodAndTags: VideoMethods.LoadAndPopulateAuthorAndPodAndTags | 74 | let loadAndPopulateAuthorAndPodAndTags: VideoMethods.LoadAndPopulateAuthorAndPodAndTags |
72 | let loadByUUIDAndPopulateAuthorAndPodAndTags: VideoMethods.LoadByUUIDAndPopulateAuthorAndPodAndTags | 75 | let loadByUUIDAndPopulateAuthorAndPodAndTags: VideoMethods.LoadByUUIDAndPopulateAuthorAndPodAndTags |
73 | let searchAndPopulateAuthorAndPodAndTags: VideoMethods.SearchAndPopulateAuthorAndPodAndTags | 76 | let searchAndPopulateAuthorAndPodAndTags: VideoMethods.SearchAndPopulateAuthorAndPodAndTags |
77 | let removeThumbnail: VideoMethods.RemoveThumbnail | ||
78 | let removePreview: VideoMethods.RemovePreview | ||
79 | let removeFile: VideoMethods.RemoveFile | ||
80 | let removeTorrent: VideoMethods.RemoveTorrent | ||
74 | 81 | ||
75 | export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { | 82 | export 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 | ||
274 | function 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 | |||
282 | function 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 | |||
310 | function 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 | ||
335 | function associate (models) { | 271 | function 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 | ||
359 | generateMagnetUri = function (this: VideoInstance) { | 303 | function 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 | ||
386 | getVideoFilename = function (this: VideoInstance) { | 330 | getVideoFilename = 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 | ||
390 | getThumbnailName = function (this: VideoInstance) { | 335 | getThumbnailName = 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 | ||
401 | getTorrentName = function (this: VideoInstance) { | 346 | getTorrentFileName = 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 | ||
356 | createPreview = function (this: VideoInstance, videoFile: VideoFileInstance) { | ||
357 | return generateImage(this, this.getVideoFilePath(videoFile), CONFIG.STORAGE.PREVIEWS_DIR, this.getPreviewName(), null) | ||
358 | } | ||
359 | |||
360 | createThumbnail = function (this: VideoInstance, videoFile: VideoFileInstance) { | ||
361 | return generateImage(this, this.getVideoFilePath(videoFile), CONFIG.STORAGE.THUMBNAILS_DIR, this.getThumbnailName(), THUMBNAILS_SIZE) | ||
362 | } | ||
363 | |||
364 | getVideoFilePath = function (this: VideoInstance, videoFile: VideoFileInstance) { | ||
365 | return join(CONFIG.STORAGE.VIDEOS_DIR, this.getVideoFilename(videoFile)) | ||
366 | } | ||
367 | |||
368 | createTorrentAndSetInfoHash = 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 | |||
390 | generateMagnetUri = 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 | |||
410 | toFormatedJSON = function (this: VideoInstance) { | 417 | toFormatedJSON = 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 | ||
516 | transcodeVideofile = function (this: VideoInstance) { | 553 | transcodeVideofile = 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 | ||
595 | removeThumbnail = function (this: VideoInstance) { | ||
596 | const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, this.getThumbnailName()) | ||
597 | return unlinkPromise(thumbnailPath) | ||
598 | } | ||
599 | |||
600 | removePreview = function (this: VideoInstance) { | ||
601 | // Same name than video thumbnail | ||
602 | return unlinkPromise(CONFIG.STORAGE.PREVIEWS_DIR + this.getPreviewName()) | ||
603 | } | ||
604 | |||
605 | removeFile = function (this: VideoInstance, videoFile: VideoFileInstance) { | ||
606 | const filePath = join(CONFIG.STORAGE.VIDEOS_DIR, this.getVideoFilename(videoFile)) | ||
607 | return unlinkPromise(filePath) | ||
608 | } | ||
609 | |||
610 | removeTorrent = 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 | ||
564 | generateThumbnailFromData = function (video: VideoInstance, thumbnailData: string) { | 617 | generateThumbnailFromData = function (video: VideoInstance, thumbnailData: string) { |
@@ -582,7 +635,11 @@ getDurationFromFile = function (videoPath: string) { | |||
582 | } | 635 | } |
583 | 636 | ||
584 | list = function () { | 637 | list = 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 | ||
588 | listForApi = function (start: number, count: number, sort: string) { | 645 | listForApi = 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 | ||
680 | loadAndPopulateAuthor = function (id: number) { | 748 | loadAndPopulateAuthor = 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 | ||
803 | function removeThumbnail (video: VideoInstance) { | ||
804 | const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, video.getThumbnailName()) | ||
805 | return unlinkPromise(thumbnailPath) | ||
806 | } | ||
807 | |||
808 | function removeFile (video: VideoInstance) { | ||
809 | const filePath = join(CONFIG.STORAGE.VIDEOS_DIR, video.getVideoFilename()) | ||
810 | return unlinkPromise(filePath) | ||
811 | } | ||
812 | |||
813 | function removeTorrent (video: VideoInstance) { | ||
814 | const torrenPath = join(CONFIG.STORAGE.TORRENTS_DIR, video.getTorrentName()) | ||
815 | return unlinkPromise(torrenPath) | ||
816 | } | ||
817 | |||
818 | function removePreview (video: VideoInstance) { | ||
819 | // Same name than video thumnail | ||
820 | return unlinkPromise(CONFIG.STORAGE.PREVIEWS_DIR + video.getPreviewName()) | ||
821 | } | ||
822 | |||
823 | function 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 | |||
845 | function createPreview (video: VideoInstance, videoPath: string) { | ||
846 | return generateImage(video, videoPath, CONFIG.STORAGE.PREVIEWS_DIR, video.getPreviewName(), null) | ||
847 | } | ||
848 | |||
849 | function createThumbnail (video: VideoInstance, videoPath: string) { | ||
850 | return generateImage(video, videoPath, CONFIG.STORAGE.THUMBNAILS_DIR, video.getThumbnailName(), THUMBNAILS_SIZE) | ||
851 | } | ||
852 | |||
853 | function generateImage (video: VideoInstance, videoPath: string, folder: string, imageName: string, size: string) { | 878 | function 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 | |||
872 | function 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 | ||
24 | export interface RemoteVideoCreateRequest extends RemoteVideoRequest { | 28 | export interface RemoteVideoCreateRequest extends RemoteVideoRequest { |
diff --git a/shared/models/pods/remote-video/remote-video-update-request.model.ts b/shared/models/pods/remote-video/remote-video-update-request.model.ts index 805548563..dd3e2ae1a 100644 --- a/shared/models/pods/remote-video/remote-video-update-request.model.ts +++ b/shared/models/pods/remote-video/remote-video-update-request.model.ts | |||
@@ -15,6 +15,12 @@ export interface RemoteVideoUpdateData { | |||
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 | ||
20 | export interface RemoteVideoUpdateRequest { | 26 | export interface RemoteVideoUpdateRequest { |
diff --git a/shared/models/videos/video.model.ts b/shared/models/videos/video.model.ts index 8aa8ee448..82c8763d0 100644 --- a/shared/models/videos/video.model.ts +++ b/shared/models/videos/video.model.ts | |||
@@ -1,3 +1,10 @@ | |||
1 | export interface VideoFile { | ||
2 | magnetUri: string | ||
3 | resolution: number | ||
4 | resolutionLabel: string | ||
5 | size: number // Bytes | ||
6 | } | ||
7 | |||
1 | export interface Video { | 8 | export 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 | } |