aboutsummaryrefslogtreecommitdiffhomepage
path: root/server
diff options
context:
space:
mode:
authorChocobozzz <florian.bigard@gmail.com>2017-10-24 19:41:09 +0200
committerChocobozzz <florian.bigard@gmail.com>2017-10-26 09:11:38 +0200
commit72c7248b6fdcdb2175e726ff51b42e7555f2bd84 (patch)
tree1bfdee99dbe2392cc997edba8e314e2a8a401c72 /server
parent8113a93a0d9f31aa9e23702bbc31b8a76275ae22 (diff)
downloadPeerTube-72c7248b6fdcdb2175e726ff51b42e7555f2bd84.tar.gz
PeerTube-72c7248b6fdcdb2175e726ff51b42e7555f2bd84.tar.zst
PeerTube-72c7248b6fdcdb2175e726ff51b42e7555f2bd84.zip
Add video channels
Diffstat (limited to 'server')
-rw-r--r--server/controllers/api/remote/pods.ts4
-rw-r--r--server/controllers/api/remote/videos.ts301
-rw-r--r--server/controllers/api/users.ts33
-rw-r--r--server/controllers/api/videos/channel.ts196
-rw-r--r--server/controllers/api/videos/index.ts48
-rw-r--r--server/controllers/services.ts2
-rw-r--r--server/helpers/custom-validators/index.ts2
-rw-r--r--server/helpers/custom-validators/misc.ts24
-rw-r--r--server/helpers/custom-validators/remote/videos.ts104
-rw-r--r--server/helpers/custom-validators/video-authors.ts45
-rw-r--r--server/helpers/custom-validators/video-channels.ts57
-rw-r--r--server/helpers/custom-validators/videos.ts20
-rw-r--r--server/initializers/constants.ts23
-rw-r--r--server/initializers/database.ts2
-rw-r--r--server/initializers/installer.ts19
-rw-r--r--server/lib/cache/videos-preview-cache.ts2
-rw-r--r--server/lib/friends.ts138
-rw-r--r--server/lib/index.ts2
-rw-r--r--server/lib/user.ts46
-rw-r--r--server/lib/video-channel.ts42
-rw-r--r--server/middlewares/sort.ts7
-rw-r--r--server/middlewares/validators/index.ts1
-rw-r--r--server/middlewares/validators/oembed.ts11
-rw-r--r--server/middlewares/validators/sort.ts3
-rw-r--r--server/middlewares/validators/users.ts4
-rw-r--r--server/middlewares/validators/video-blacklist.ts6
-rw-r--r--server/middlewares/validators/video-channels.ts142
-rw-r--r--server/middlewares/validators/videos.ts33
-rw-r--r--server/models/oauth/oauth-token.ts24
-rw-r--r--server/models/request/request-video-event.ts5
-rw-r--r--server/models/user/user-interface.ts8
-rw-r--r--server/models/user/user.ts69
-rw-r--r--server/models/video/author-interface.ts29
-rw-r--r--server/models/video/author.ts103
-rw-r--r--server/models/video/index.ts1
-rw-r--r--server/models/video/video-channel-interface.ts64
-rw-r--r--server/models/video/video-channel.ts349
-rw-r--r--server/models/video/video-interface.ts16
-rw-r--r--server/models/video/video.ts180
39 files changed, 1892 insertions, 273 deletions
diff --git a/server/controllers/api/remote/pods.ts b/server/controllers/api/remote/pods.ts
index 6f7b5f651..a62b9c684 100644
--- a/server/controllers/api/remote/pods.ts
+++ b/server/controllers/api/remote/pods.ts
@@ -7,7 +7,7 @@ import {
7 setBodyHostPort, 7 setBodyHostPort,
8 remotePodsAddValidator 8 remotePodsAddValidator
9} from '../../../middlewares' 9} from '../../../middlewares'
10import { sendOwnedVideosToPod } from '../../../lib' 10import { sendOwnedDataToPod } from '../../../lib'
11import { getMyPublicCert, getFormattedObjects } from '../../../helpers' 11import { getMyPublicCert, getFormattedObjects } from '../../../helpers'
12import { CONFIG } from '../../../initializers' 12import { CONFIG } from '../../../initializers'
13import { PodInstance } from '../../../models' 13import { PodInstance } from '../../../models'
@@ -43,7 +43,7 @@ function addPods (req: express.Request, res: express.Response, next: express.Nex
43 const pod = db.Pod.build(information) 43 const pod = db.Pod.build(information)
44 pod.save() 44 pod.save()
45 .then(podCreated => { 45 .then(podCreated => {
46 return sendOwnedVideosToPod(podCreated.id) 46 return sendOwnedDataToPod(podCreated.id)
47 }) 47 })
48 .then(() => { 48 .then(() => {
49 return getMyPublicCert() 49 return getMyPublicCert()
diff --git a/server/controllers/api/remote/videos.ts b/server/controllers/api/remote/videos.ts
index 23023211f..c8f531490 100644
--- a/server/controllers/api/remote/videos.ts
+++ b/server/controllers/api/remote/videos.ts
@@ -1,5 +1,6 @@
1import * as express from 'express' 1import * as express from 'express'
2import * as Promise from 'bluebird' 2import * as Promise from 'bluebird'
3import * as Sequelize from 'sequelize'
3 4
4import { database as db } from '../../../initializers/database' 5import { database as db } from '../../../initializers/database'
5import { 6import {
@@ -27,17 +28,28 @@ import {
27 RemoteQaduVideoRequest, 28 RemoteQaduVideoRequest,
28 RemoteQaduVideoData, 29 RemoteQaduVideoData,
29 RemoteVideoEventRequest, 30 RemoteVideoEventRequest,
30 RemoteVideoEventData 31 RemoteVideoEventData,
32 RemoteVideoChannelCreateData,
33 RemoteVideoChannelUpdateData,
34 RemoteVideoChannelRemoveData,
35 RemoteVideoAuthorRemoveData,
36 RemoteVideoAuthorCreateData
31} from '../../../../shared' 37} from '../../../../shared'
32 38
33const ENDPOINT_ACTIONS = REQUEST_ENDPOINT_ACTIONS[REQUEST_ENDPOINTS.VIDEOS] 39const ENDPOINT_ACTIONS = REQUEST_ENDPOINT_ACTIONS[REQUEST_ENDPOINTS.VIDEOS]
34 40
35// Functions to call when processing a remote request 41// Functions to call when processing a remote request
42// FIXME: use RemoteVideoRequestType as id type
36const functionsHash: { [ id: string ]: (...args) => Promise<any> } = {} 43const functionsHash: { [ id: string ]: (...args) => Promise<any> } = {}
37functionsHash[ENDPOINT_ACTIONS.ADD] = addRemoteVideoRetryWrapper 44functionsHash[ENDPOINT_ACTIONS.ADD_VIDEO] = addRemoteVideoRetryWrapper
38functionsHash[ENDPOINT_ACTIONS.UPDATE] = updateRemoteVideoRetryWrapper 45functionsHash[ENDPOINT_ACTIONS.UPDATE_VIDEO] = updateRemoteVideoRetryWrapper
39functionsHash[ENDPOINT_ACTIONS.REMOVE] = removeRemoteVideo 46functionsHash[ENDPOINT_ACTIONS.REMOVE_VIDEO] = removeRemoteVideoRetryWrapper
40functionsHash[ENDPOINT_ACTIONS.REPORT_ABUSE] = reportAbuseRemoteVideo 47functionsHash[ENDPOINT_ACTIONS.ADD_CHANNEL] = addRemoteVideoChannelRetryWrapper
48functionsHash[ENDPOINT_ACTIONS.UPDATE_CHANNEL] = updateRemoteVideoChannelRetryWrapper
49functionsHash[ENDPOINT_ACTIONS.REMOVE_CHANNEL] = removeRemoteVideoChannelRetryWrapper
50functionsHash[ENDPOINT_ACTIONS.REPORT_ABUSE] = reportAbuseRemoteVideoRetryWrapper
51functionsHash[ENDPOINT_ACTIONS.ADD_AUTHOR] = addRemoteVideoAuthorRetryWrapper
52functionsHash[ENDPOINT_ACTIONS.REMOVE_AUTHOR] = removeRemoteVideoAuthorRetryWrapper
41 53
42const remoteVideosRouter = express.Router() 54const remoteVideosRouter = express.Router()
43 55
@@ -133,7 +145,7 @@ function processVideosEventsRetryWrapper (eventData: RemoteVideoEventData, fromP
133function processVideosEvents (eventData: RemoteVideoEventData, fromPod: PodInstance) { 145function processVideosEvents (eventData: RemoteVideoEventData, fromPod: PodInstance) {
134 146
135 return db.sequelize.transaction(t => { 147 return db.sequelize.transaction(t => {
136 return fetchVideoByUUID(eventData.uuid) 148 return fetchVideoByUUID(eventData.uuid, t)
137 .then(videoInstance => { 149 .then(videoInstance => {
138 const options = { transaction: t } 150 const options = { transaction: t }
139 151
@@ -196,7 +208,7 @@ function quickAndDirtyUpdateVideo (videoData: RemoteQaduVideoData, fromPod: PodI
196 let videoUUID = '' 208 let videoUUID = ''
197 209
198 return db.sequelize.transaction(t => { 210 return db.sequelize.transaction(t => {
199 return fetchVideoByHostAndUUID(fromPod.host, videoData.uuid) 211 return fetchVideoByHostAndUUID(fromPod.host, videoData.uuid, t)
200 .then(videoInstance => { 212 .then(videoInstance => {
201 const options = { transaction: t } 213 const options = { transaction: t }
202 214
@@ -239,22 +251,16 @@ function addRemoteVideo (videoToCreateData: RemoteVideoCreateData, fromPod: PodI
239 .then(video => { 251 .then(video => {
240 if (video) throw new Error('UUID already exists.') 252 if (video) throw new Error('UUID already exists.')
241 253
242 return undefined 254 return db.VideoChannel.loadByHostAndUUID(fromPod.host, videoToCreateData.channelUUID, t)
243 }) 255 })
244 .then(() => { 256 .then(videoChannel => {
245 const name = videoToCreateData.author 257 if (!videoChannel) throw new Error('Video channel ' + videoToCreateData.channelUUID + ' not found.')
246 const podId = fromPod.id
247 // This author is from another pod so we do not associate a user
248 const userId = null
249 258
250 return db.Author.findOrCreateAuthor(name, podId, userId, t)
251 })
252 .then(author => {
253 const tags = videoToCreateData.tags 259 const tags = videoToCreateData.tags
254 260
255 return db.Tag.findOrCreateTags(tags, t).then(tagInstances => ({ author, tagInstances })) 261 return db.Tag.findOrCreateTags(tags, t).then(tagInstances => ({ videoChannel, tagInstances }))
256 }) 262 })
257 .then(({ author, tagInstances }) => { 263 .then(({ videoChannel, tagInstances }) => {
258 const videoData = { 264 const videoData = {
259 name: videoToCreateData.name, 265 name: videoToCreateData.name,
260 uuid: videoToCreateData.uuid, 266 uuid: videoToCreateData.uuid,
@@ -263,7 +269,7 @@ function addRemoteVideo (videoToCreateData: RemoteVideoCreateData, fromPod: PodI
263 language: videoToCreateData.language, 269 language: videoToCreateData.language,
264 nsfw: videoToCreateData.nsfw, 270 nsfw: videoToCreateData.nsfw,
265 description: videoToCreateData.description, 271 description: videoToCreateData.description,
266 authorId: author.id, 272 channelId: videoChannel.id,
267 duration: videoToCreateData.duration, 273 duration: videoToCreateData.duration,
268 createdAt: videoToCreateData.createdAt, 274 createdAt: videoToCreateData.createdAt,
269 // FIXME: updatedAt does not seems to be considered by Sequelize 275 // FIXME: updatedAt does not seems to be considered by Sequelize
@@ -336,7 +342,7 @@ function updateRemoteVideo (videoAttributesToUpdate: RemoteVideoUpdateData, from
336 logger.debug('Updating remote video "%s".', videoAttributesToUpdate.uuid) 342 logger.debug('Updating remote video "%s".', videoAttributesToUpdate.uuid)
337 343
338 return db.sequelize.transaction(t => { 344 return db.sequelize.transaction(t => {
339 return fetchVideoByHostAndUUID(fromPod.host, videoAttributesToUpdate.uuid) 345 return fetchVideoByHostAndUUID(fromPod.host, videoAttributesToUpdate.uuid, t)
340 .then(videoInstance => { 346 .then(videoInstance => {
341 const tags = videoAttributesToUpdate.tags 347 const tags = videoAttributesToUpdate.tags
342 348
@@ -365,7 +371,7 @@ function updateRemoteVideo (videoAttributesToUpdate: RemoteVideoUpdateData, from
365 371
366 // Remove old video files 372 // Remove old video files
367 videoInstance.VideoFiles.forEach(videoFile => { 373 videoInstance.VideoFiles.forEach(videoFile => {
368 tasks.push(videoFile.destroy()) 374 tasks.push(videoFile.destroy({ transaction: t }))
369 }) 375 })
370 376
371 return Promise.all(tasks).then(() => ({ tagInstances, videoInstance })) 377 return Promise.all(tasks).then(() => ({ tagInstances, videoInstance }))
@@ -404,37 +410,231 @@ function updateRemoteVideo (videoAttributesToUpdate: RemoteVideoUpdateData, from
404 }) 410 })
405} 411}
406 412
413function removeRemoteVideoRetryWrapper (videoToRemoveData: RemoteVideoRemoveData, fromPod: PodInstance) {
414 const options = {
415 arguments: [ videoToRemoveData, fromPod ],
416 errorMessage: 'Cannot remove the remote video channel with many retries.'
417 }
418
419 return retryTransactionWrapper(removeRemoteVideo, options)
420}
421
407function removeRemoteVideo (videoToRemoveData: RemoteVideoRemoveData, fromPod: PodInstance) { 422function removeRemoteVideo (videoToRemoveData: RemoteVideoRemoveData, fromPod: PodInstance) {
408 // We need the instance because we have to remove some other stuffs (thumbnail etc) 423 logger.debug('Removing remote video "%s".', videoToRemoveData.uuid)
409 return fetchVideoByHostAndUUID(fromPod.host, videoToRemoveData.uuid) 424
410 .then(video => { 425 return db.sequelize.transaction(t => {
411 logger.debug('Removing remote video with uuid %s.', video.uuid) 426 // We need the instance because we have to remove some other stuffs (thumbnail etc)
412 return video.destroy() 427 return fetchVideoByHostAndUUID(fromPod.host, videoToRemoveData.uuid, t)
413 }) 428 .then(video => video.destroy({ transaction: t }))
429 })
430 .then(() => logger.info('Remote video with uuid %s removed.', videoToRemoveData.uuid))
431 .catch(err => {
432 logger.debug('Cannot remove the remote video.', err)
433 throw err
434 })
435}
436
437function addRemoteVideoAuthorRetryWrapper (authorToCreateData: RemoteVideoAuthorCreateData, fromPod: PodInstance) {
438 const options = {
439 arguments: [ authorToCreateData, fromPod ],
440 errorMessage: 'Cannot insert the remote video author with many retries.'
441 }
442
443 return retryTransactionWrapper(addRemoteVideoAuthor, options)
444}
445
446function addRemoteVideoAuthor (authorToCreateData: RemoteVideoAuthorCreateData, fromPod: PodInstance) {
447 logger.debug('Adding remote video author "%s".', authorToCreateData.uuid)
448
449 return db.sequelize.transaction(t => {
450 return db.Author.loadAuthorByPodAndUUID(authorToCreateData.uuid, fromPod.id, t)
451 .then(author => {
452 if (author) throw new Error('UUID already exists.')
453
454 return undefined
455 })
456 .then(() => {
457 const videoAuthorData = {
458 name: authorToCreateData.name,
459 uuid: authorToCreateData.uuid,
460 userId: null, // Not on our pod
461 podId: fromPod.id
462 }
463
464 const author = db.Author.build(videoAuthorData)
465 return author.save({ transaction: t })
466 })
467 })
468 .then(() => logger.info('Remote video author with uuid %s inserted.', authorToCreateData.uuid))
414 .catch(err => { 469 .catch(err => {
415 logger.debug('Could not fetch remote video.', { host: fromPod.host, uuid: videoToRemoveData.uuid, error: err.stack }) 470 logger.debug('Cannot insert the remote video author.', err)
471 throw err
416 }) 472 })
417} 473}
418 474
475function removeRemoteVideoAuthorRetryWrapper (authorAttributesToRemove: RemoteVideoAuthorRemoveData, fromPod: PodInstance) {
476 const options = {
477 arguments: [ authorAttributesToRemove, fromPod ],
478 errorMessage: 'Cannot remove the remote video author with many retries.'
479 }
480
481 return retryTransactionWrapper(removeRemoteVideoAuthor, options)
482}
483
484function removeRemoteVideoAuthor (authorAttributesToRemove: RemoteVideoAuthorRemoveData, fromPod: PodInstance) {
485 logger.debug('Removing remote video author "%s".', authorAttributesToRemove.uuid)
486
487 return db.sequelize.transaction(t => {
488 return db.Author.loadAuthorByPodAndUUID(authorAttributesToRemove.uuid, fromPod.id, t)
489 .then(videoAuthor => videoAuthor.destroy({ transaction: t }))
490 })
491 .then(() => logger.info('Remote video author with uuid %s removed.', authorAttributesToRemove.uuid))
492 .catch(err => {
493 logger.debug('Cannot remove the remote video author.', err)
494 throw err
495 })
496}
497
498function addRemoteVideoChannelRetryWrapper (videoChannelToCreateData: RemoteVideoChannelCreateData, fromPod: PodInstance) {
499 const options = {
500 arguments: [ videoChannelToCreateData, fromPod ],
501 errorMessage: 'Cannot insert the remote video channel with many retries.'
502 }
503
504 return retryTransactionWrapper(addRemoteVideoChannel, options)
505}
506
507function addRemoteVideoChannel (videoChannelToCreateData: RemoteVideoChannelCreateData, fromPod: PodInstance) {
508 logger.debug('Adding remote video channel "%s".', videoChannelToCreateData.uuid)
509
510 return db.sequelize.transaction(t => {
511 return db.VideoChannel.loadByUUID(videoChannelToCreateData.uuid)
512 .then(videoChannel => {
513 if (videoChannel) throw new Error('UUID already exists.')
514
515 return undefined
516 })
517 .then(() => {
518 const authorUUID = videoChannelToCreateData.ownerUUID
519 const podId = fromPod.id
520
521 return db.Author.loadAuthorByPodAndUUID(authorUUID, podId, t)
522 })
523 .then(author => {
524 if (!author) throw new Error('Unknown author UUID.')
525
526 const videoChannelData = {
527 name: videoChannelToCreateData.name,
528 description: videoChannelToCreateData.description,
529 uuid: videoChannelToCreateData.uuid,
530 createdAt: videoChannelToCreateData.createdAt,
531 updatedAt: videoChannelToCreateData.updatedAt,
532 remote: true,
533 authorId: author.id
534 }
535
536 const videoChannel = db.VideoChannel.build(videoChannelData)
537 return videoChannel.save({ transaction: t })
538 })
539 })
540 .then(() => logger.info('Remote video channel with uuid %s inserted.', videoChannelToCreateData.uuid))
541 .catch(err => {
542 logger.debug('Cannot insert the remote video channel.', err)
543 throw err
544 })
545}
546
547function updateRemoteVideoChannelRetryWrapper (videoChannelAttributesToUpdate: RemoteVideoChannelUpdateData, fromPod: PodInstance) {
548 const options = {
549 arguments: [ videoChannelAttributesToUpdate, fromPod ],
550 errorMessage: 'Cannot update the remote video channel with many retries.'
551 }
552
553 return retryTransactionWrapper(updateRemoteVideoChannel, options)
554}
555
556function updateRemoteVideoChannel (videoChannelAttributesToUpdate: RemoteVideoChannelUpdateData, fromPod: PodInstance) {
557 logger.debug('Updating remote video channel "%s".', videoChannelAttributesToUpdate.uuid)
558
559 return db.sequelize.transaction(t => {
560 return fetchVideoChannelByHostAndUUID(fromPod.host, videoChannelAttributesToUpdate.uuid, t)
561 .then(videoChannelInstance => {
562 const options = { transaction: t }
563
564 videoChannelInstance.set('name', videoChannelAttributesToUpdate.name)
565 videoChannelInstance.set('description', videoChannelAttributesToUpdate.description)
566 videoChannelInstance.set('createdAt', videoChannelAttributesToUpdate.createdAt)
567 videoChannelInstance.set('updatedAt', videoChannelAttributesToUpdate.updatedAt)
568
569 return videoChannelInstance.save(options)
570 })
571 })
572 .then(() => logger.info('Remote video channel with uuid %s updated', videoChannelAttributesToUpdate.uuid))
573 .catch(err => {
574 // This is just a debug because we will retry the insert
575 logger.debug('Cannot update the remote video channel.', err)
576 throw err
577 })
578}
579
580function removeRemoteVideoChannelRetryWrapper (videoChannelAttributesToRemove: RemoteVideoChannelRemoveData, fromPod: PodInstance) {
581 const options = {
582 arguments: [ videoChannelAttributesToRemove, fromPod ],
583 errorMessage: 'Cannot remove the remote video channel with many retries.'
584 }
585
586 return retryTransactionWrapper(removeRemoteVideoChannel, options)
587}
588
589function removeRemoteVideoChannel (videoChannelAttributesToRemove: RemoteVideoChannelRemoveData, fromPod: PodInstance) {
590 logger.debug('Removing remote video channel "%s".', videoChannelAttributesToRemove.uuid)
591
592 return db.sequelize.transaction(t => {
593 return fetchVideoChannelByHostAndUUID(fromPod.host, videoChannelAttributesToRemove.uuid, t)
594 .then(videoChannel => videoChannel.destroy({ transaction: t }))
595 })
596 .then(() => logger.info('Remote video channel with uuid %s removed.', videoChannelAttributesToRemove.uuid))
597 .catch(err => {
598 logger.debug('Cannot remove the remote video channel.', err)
599 throw err
600 })
601}
602
603function reportAbuseRemoteVideoRetryWrapper (reportData: RemoteVideoReportAbuseData, fromPod: PodInstance) {
604 const options = {
605 arguments: [ reportData, fromPod ],
606 errorMessage: 'Cannot create remote abuse video with many retries.'
607 }
608
609 return retryTransactionWrapper(reportAbuseRemoteVideo, options)
610}
611
419function reportAbuseRemoteVideo (reportData: RemoteVideoReportAbuseData, fromPod: PodInstance) { 612function reportAbuseRemoteVideo (reportData: RemoteVideoReportAbuseData, fromPod: PodInstance) {
420 return fetchVideoByUUID(reportData.videoUUID) 613 logger.debug('Reporting remote abuse for video %s.', reportData.videoUUID)
421 .then(video => {
422 logger.debug('Reporting remote abuse for video %s.', video.id)
423 614
424 const videoAbuseData = { 615 return db.sequelize.transaction(t => {
425 reporterUsername: reportData.reporterUsername, 616 return fetchVideoByUUID(reportData.videoUUID, t)
426 reason: reportData.reportReason, 617 .then(video => {
427 reporterPodId: fromPod.id, 618 const videoAbuseData = {
428 videoId: video.id 619 reporterUsername: reportData.reporterUsername,
429 } 620 reason: reportData.reportReason,
621 reporterPodId: fromPod.id,
622 videoId: video.id
623 }
430 624
431 return db.VideoAbuse.create(videoAbuseData) 625 return db.VideoAbuse.create(videoAbuseData)
432 }) 626 })
433 .catch(err => logger.error('Cannot create remote abuse video.', err)) 627 })
628 .then(() => logger.info('Remote abuse for video uuid %s created', reportData.videoUUID))
629 .catch(err => {
630 // This is just a debug because we will retry the insert
631 logger.debug('Cannot create remote abuse video', err)
632 throw err
633 })
434} 634}
435 635
436function fetchVideoByUUID (id: string) { 636function fetchVideoByUUID (id: string, t: Sequelize.Transaction) {
437 return db.Video.loadByUUID(id) 637 return db.Video.loadByUUID(id, t)
438 .then(video => { 638 .then(video => {
439 if (!video) throw new Error('Video not found') 639 if (!video) throw new Error('Video not found')
440 640
@@ -446,8 +646,8 @@ function fetchVideoByUUID (id: string) {
446 }) 646 })
447} 647}
448 648
449function fetchVideoByHostAndUUID (podHost: string, uuid: string) { 649function fetchVideoByHostAndUUID (podHost: string, uuid: string, t: Sequelize.Transaction) {
450 return db.Video.loadByHostAndUUID(podHost, uuid) 650 return db.Video.loadByHostAndUUID(podHost, uuid, t)
451 .then(video => { 651 .then(video => {
452 if (!video) throw new Error('Video not found') 652 if (!video) throw new Error('Video not found')
453 653
@@ -458,3 +658,16 @@ function fetchVideoByHostAndUUID (podHost: string, uuid: string) {
458 throw err 658 throw err
459 }) 659 })
460} 660}
661
662function fetchVideoChannelByHostAndUUID (podHost: string, uuid: string, t: Sequelize.Transaction) {
663 return db.VideoChannel.loadByHostAndUUID(podHost, uuid, t)
664 .then(videoChannel => {
665 if (!videoChannel) throw new Error('Video channel not found')
666
667 return videoChannel
668 })
669 .catch(err => {
670 logger.error('Cannot load video channel from host and uuid.', { error: err.stack, podHost, uuid })
671 throw err
672 })
673}
diff --git a/server/controllers/api/users.ts b/server/controllers/api/users.ts
index 1ecaaf93f..6576e4333 100644
--- a/server/controllers/api/users.ts
+++ b/server/controllers/api/users.ts
@@ -2,7 +2,7 @@ import * as express from 'express'
2 2
3import { database as db } from '../../initializers/database' 3import { database as db } from '../../initializers/database'
4import { USER_ROLES, CONFIG } from '../../initializers' 4import { USER_ROLES, CONFIG } from '../../initializers'
5import { logger, getFormattedObjects } from '../../helpers' 5import { logger, getFormattedObjects, retryTransactionWrapper } from '../../helpers'
6import { 6import {
7 authenticate, 7 authenticate,
8 ensureIsAdmin, 8 ensureIsAdmin,
@@ -26,6 +26,7 @@ import {
26 UserUpdate, 26 UserUpdate,
27 UserUpdateMe 27 UserUpdateMe
28} from '../../../shared' 28} from '../../../shared'
29import { createUserAuthorAndChannel } from '../../lib'
29import { UserInstance } from '../../models' 30import { UserInstance } from '../../models'
30 31
31const usersRouter = express.Router() 32const usersRouter = express.Router()
@@ -58,7 +59,7 @@ usersRouter.post('/',
58 authenticate, 59 authenticate,
59 ensureIsAdmin, 60 ensureIsAdmin,
60 usersAddValidator, 61 usersAddValidator,
61 createUser 62 createUserRetryWrapper
62) 63)
63 64
64usersRouter.post('/register', 65usersRouter.post('/register',
@@ -98,9 +99,22 @@ export {
98 99
99// --------------------------------------------------------------------------- 100// ---------------------------------------------------------------------------
100 101
102function createUserRetryWrapper (req: express.Request, res: express.Response, next: express.NextFunction) {
103 const options = {
104 arguments: [ req, res ],
105 errorMessage: 'Cannot insert the user with many retries.'
106 }
107
108 retryTransactionWrapper(createUser, options)
109 .then(() => {
110 // TODO : include Location of the new user -> 201
111 res.type('json').status(204).end()
112 })
113 .catch(err => next(err))
114}
115
101function createUser (req: express.Request, res: express.Response, next: express.NextFunction) { 116function createUser (req: express.Request, res: express.Response, next: express.NextFunction) {
102 const body: UserCreate = req.body 117 const body: UserCreate = req.body
103
104 const user = db.User.build({ 118 const user = db.User.build({
105 username: body.username, 119 username: body.username,
106 password: body.password, 120 password: body.password,
@@ -110,9 +124,12 @@ function createUser (req: express.Request, res: express.Response, next: express.
110 videoQuota: body.videoQuota 124 videoQuota: body.videoQuota
111 }) 125 })
112 126
113 user.save() 127 return createUserAuthorAndChannel(user)
114 .then(() => res.type('json').status(204).end()) 128 .then(() => logger.info('User %s with its channel and author created.', body.username))
115 .catch(err => next(err)) 129 .catch((err: Error) => {
130 logger.debug('Cannot insert the user.', err)
131 throw err
132 })
116} 133}
117 134
118function registerUser (req: express.Request, res: express.Response, next: express.NextFunction) { 135function registerUser (req: express.Request, res: express.Response, next: express.NextFunction) {
@@ -127,13 +144,13 @@ function registerUser (req: express.Request, res: express.Response, next: expres
127 videoQuota: CONFIG.USER.VIDEO_QUOTA 144 videoQuota: CONFIG.USER.VIDEO_QUOTA
128 }) 145 })
129 146
130 user.save() 147 return createUserAuthorAndChannel(user)
131 .then(() => res.type('json').status(204).end()) 148 .then(() => res.type('json').status(204).end())
132 .catch(err => next(err)) 149 .catch(err => next(err))
133} 150}
134 151
135function getUserInformation (req: express.Request, res: express.Response, next: express.NextFunction) { 152function getUserInformation (req: express.Request, res: express.Response, next: express.NextFunction) {
136 db.User.loadByUsername(res.locals.oauth.token.user.username) 153 db.User.loadByUsernameAndPopulateChannels(res.locals.oauth.token.user.username)
137 .then(user => res.json(user.toFormattedJSON())) 154 .then(user => res.json(user.toFormattedJSON()))
138 .catch(err => next(err)) 155 .catch(err => next(err))
139} 156}
diff --git a/server/controllers/api/videos/channel.ts b/server/controllers/api/videos/channel.ts
new file mode 100644
index 000000000..630fc4f53
--- /dev/null
+++ b/server/controllers/api/videos/channel.ts
@@ -0,0 +1,196 @@
1import * as express from 'express'
2
3import { database as db } from '../../../initializers'
4import {
5 logger,
6 getFormattedObjects,
7 retryTransactionWrapper
8} from '../../../helpers'
9import {
10 authenticate,
11 paginationValidator,
12 videoChannelsSortValidator,
13 videoChannelsAddValidator,
14 setVideoChannelsSort,
15 setPagination,
16 videoChannelsRemoveValidator,
17 videoChannelGetValidator,
18 videoChannelsUpdateValidator,
19 listVideoAuthorChannelsValidator
20} from '../../../middlewares'
21import {
22 createVideoChannel,
23 updateVideoChannelToFriends
24} from '../../../lib'
25import { VideoChannelInstance, AuthorInstance } from '../../../models'
26import { VideoChannelCreate, VideoChannelUpdate } from '../../../../shared'
27
28const videoChannelRouter = express.Router()
29
30videoChannelRouter.get('/channels',
31 paginationValidator,
32 videoChannelsSortValidator,
33 setVideoChannelsSort,
34 setPagination,
35 listVideoChannels
36)
37
38videoChannelRouter.get('/authors/:authorId/channels',
39 listVideoAuthorChannelsValidator,
40 listVideoAuthorChannels
41)
42
43videoChannelRouter.post('/channels',
44 authenticate,
45 videoChannelsAddValidator,
46 addVideoChannelRetryWrapper
47)
48
49videoChannelRouter.put('/channels/:id',
50 authenticate,
51 videoChannelsUpdateValidator,
52 updateVideoChannelRetryWrapper
53)
54
55videoChannelRouter.delete('/channels/:id',
56 authenticate,
57 videoChannelsRemoveValidator,
58 removeVideoChannelRetryWrapper
59)
60
61videoChannelRouter.get('/channels/:id',
62 videoChannelGetValidator,
63 getVideoChannel
64)
65
66// ---------------------------------------------------------------------------
67
68export {
69 videoChannelRouter
70}
71
72// ---------------------------------------------------------------------------
73
74function listVideoChannels (req: express.Request, res: express.Response, next: express.NextFunction) {
75 db.VideoChannel.listForApi(req.query.start, req.query.count, req.query.sort)
76 .then(result => res.json(getFormattedObjects(result.data, result.total)))
77 .catch(err => next(err))
78}
79
80function listVideoAuthorChannels (req: express.Request, res: express.Response, next: express.NextFunction) {
81 db.VideoChannel.listByAuthor(res.locals.author.id)
82 .then(result => res.json(getFormattedObjects(result.data, result.total)))
83 .catch(err => next(err))
84}
85
86// Wrapper to video channel add that retry the function if there is a database error
87// We need this because we run the transaction in SERIALIZABLE isolation that can fail
88function addVideoChannelRetryWrapper (req: express.Request, res: express.Response, next: express.NextFunction) {
89 const options = {
90 arguments: [ req, res ],
91 errorMessage: 'Cannot insert the video video channel with many retries.'
92 }
93
94 retryTransactionWrapper(addVideoChannel, options)
95 .then(() => {
96 // TODO : include Location of the new video channel -> 201
97 res.type('json').status(204).end()
98 })
99 .catch(err => next(err))
100}
101
102function addVideoChannel (req: express.Request, res: express.Response) {
103 const videoChannelInfo: VideoChannelCreate = req.body
104 const author: AuthorInstance = res.locals.oauth.token.User.Author
105
106 return db.sequelize.transaction(t => {
107 return createVideoChannel(videoChannelInfo, author, t)
108 })
109 .then(videoChannelUUID => logger.info('Video channel with uuid %s created.', videoChannelUUID))
110 .catch((err: Error) => {
111 logger.debug('Cannot insert the video channel.', err)
112 throw err
113 })
114}
115
116function updateVideoChannelRetryWrapper (req: express.Request, res: express.Response, next: express.NextFunction) {
117 const options = {
118 arguments: [ req, res ],
119 errorMessage: 'Cannot update the video with many retries.'
120 }
121
122 retryTransactionWrapper(updateVideoChannel, options)
123 .then(() => res.type('json').status(204).end())
124 .catch(err => next(err))
125}
126
127function updateVideoChannel (req: express.Request, res: express.Response) {
128 const videoChannelInstance: VideoChannelInstance = res.locals.videoChannel
129 const videoChannelFieldsSave = videoChannelInstance.toJSON()
130 const videoChannelInfoToUpdate: VideoChannelUpdate = req.body
131
132 return db.sequelize.transaction(t => {
133 const options = {
134 transaction: t
135 }
136
137 if (videoChannelInfoToUpdate.name !== undefined) videoChannelInstance.set('name', videoChannelInfoToUpdate.name)
138 if (videoChannelInfoToUpdate.description !== undefined) videoChannelInstance.set('description', videoChannelInfoToUpdate.description)
139
140 return videoChannelInstance.save(options)
141 .then(() => {
142 const json = videoChannelInstance.toUpdateRemoteJSON()
143
144 // Now we'll update the video channel's meta data to our friends
145 return updateVideoChannelToFriends(json, t)
146 })
147 })
148 .then(() => {
149 logger.info('Video channel with name %s and uuid %s updated.', videoChannelInstance.name, videoChannelInstance.uuid)
150 })
151 .catch(err => {
152 logger.debug('Cannot update the video channel.', err)
153
154 // Force fields we want to update
155 // If the transaction is retried, sequelize will think the object has not changed
156 // So it will skip the SQL request, even if the last one was ROLLBACKed!
157 Object.keys(videoChannelFieldsSave).forEach(key => {
158 const value = videoChannelFieldsSave[key]
159 videoChannelInstance.set(key, value)
160 })
161
162 throw err
163 })
164}
165
166function removeVideoChannelRetryWrapper (req: express.Request, res: express.Response, next: express.NextFunction) {
167 const options = {
168 arguments: [ req, res ],
169 errorMessage: 'Cannot remove the video channel with many retries.'
170 }
171
172 retryTransactionWrapper(removeVideoChannel, options)
173 .then(() => res.type('json').status(204).end())
174 .catch(err => next(err))
175}
176
177function removeVideoChannel (req: express.Request, res: express.Response) {
178 const videoChannelInstance: VideoChannelInstance = res.locals.videoChannel
179
180 return db.sequelize.transaction(t => {
181 return videoChannelInstance.destroy({ transaction: t })
182 })
183 .then(() => {
184 logger.info('Video channel with name %s and uuid %s deleted.', videoChannelInstance.name, videoChannelInstance.uuid)
185 })
186 .catch(err => {
187 logger.error('Errors when removed the video channel.', err)
188 throw err
189 })
190}
191
192function getVideoChannel (req: express.Request, res: express.Response, next: express.NextFunction) {
193 db.VideoChannel.loadAndPopulateAuthorAndVideos(res.locals.videoChannel.id)
194 .then(videoChannelWithVideos => res.json(videoChannelWithVideos.toFormattedJSON()))
195 .catch(err => next(err))
196}
diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts
index 2b7ead954..ec855ee8e 100644
--- a/server/controllers/api/videos/index.ts
+++ b/server/controllers/api/videos/index.ts
@@ -46,6 +46,7 @@ import { VideoCreate, VideoUpdate } from '../../../../shared'
46import { abuseVideoRouter } from './abuse' 46import { abuseVideoRouter } from './abuse'
47import { blacklistRouter } from './blacklist' 47import { blacklistRouter } from './blacklist'
48import { rateVideoRouter } from './rate' 48import { rateVideoRouter } from './rate'
49import { videoChannelRouter } from './channel'
49 50
50const videosRouter = express.Router() 51const videosRouter = express.Router()
51 52
@@ -76,6 +77,7 @@ const reqFiles = multer({ storage: storage }).fields([{ name: 'videofile', maxCo
76videosRouter.use('/', abuseVideoRouter) 77videosRouter.use('/', abuseVideoRouter)
77videosRouter.use('/', blacklistRouter) 78videosRouter.use('/', blacklistRouter)
78videosRouter.use('/', rateVideoRouter) 79videosRouter.use('/', rateVideoRouter)
80videosRouter.use('/', videoChannelRouter)
79 81
80videosRouter.get('/categories', listVideoCategories) 82videosRouter.get('/categories', listVideoCategories)
81videosRouter.get('/licences', listVideoLicences) 83videosRouter.get('/licences', listVideoLicences)
@@ -161,21 +163,13 @@ function addVideo (req: express.Request, res: express.Response, videoPhysicalFil
161 let videoUUID = '' 163 let videoUUID = ''
162 164
163 return db.sequelize.transaction(t => { 165 return db.sequelize.transaction(t => {
164 const user = res.locals.oauth.token.User 166 let p: Promise<TagInstance[]>
165 167
166 const name = user.username 168 if (!videoInfo.tags) p = Promise.resolve(undefined)
167 // null because it is OUR pod 169 else p = db.Tag.findOrCreateTags(videoInfo.tags, t)
168 const podId = null
169 const userId = user.id
170 170
171 return db.Author.findOrCreateAuthor(name, podId, userId, t) 171 return p
172 .then(author => { 172 .then(tagInstances => {
173 const tags = videoInfo.tags
174 if (!tags) return { author, tagInstances: undefined }
175
176 return db.Tag.findOrCreateTags(tags, t).then(tagInstances => ({ author, tagInstances }))
177 })
178 .then(({ author, tagInstances }) => {
179 const videoData = { 173 const videoData = {
180 name: videoInfo.name, 174 name: videoInfo.name,
181 remote: false, 175 remote: false,
@@ -186,18 +180,18 @@ function addVideo (req: express.Request, res: express.Response, videoPhysicalFil
186 nsfw: videoInfo.nsfw, 180 nsfw: videoInfo.nsfw,
187 description: videoInfo.description, 181 description: videoInfo.description,
188 duration: videoPhysicalFile['duration'], // duration was added by a previous middleware 182 duration: videoPhysicalFile['duration'], // duration was added by a previous middleware
189 authorId: author.id 183 channelId: res.locals.videoChannel.id
190 } 184 }
191 185
192 const video = db.Video.build(videoData) 186 const video = db.Video.build(videoData)
193 return { author, tagInstances, video } 187 return { tagInstances, video }
194 }) 188 })
195 .then(({ author, tagInstances, video }) => { 189 .then(({ tagInstances, video }) => {
196 const videoFilePath = join(CONFIG.STORAGE.VIDEOS_DIR, videoPhysicalFile.filename) 190 const videoFilePath = join(CONFIG.STORAGE.VIDEOS_DIR, videoPhysicalFile.filename)
197 return getVideoFileHeight(videoFilePath) 191 return getVideoFileHeight(videoFilePath)
198 .then(height => ({ author, tagInstances, video, videoFileHeight: height })) 192 .then(height => ({ tagInstances, video, videoFileHeight: height }))
199 }) 193 })
200 .then(({ author, tagInstances, video, videoFileHeight }) => { 194 .then(({ tagInstances, video, videoFileHeight }) => {
201 const videoFileData = { 195 const videoFileData = {
202 extname: extname(videoPhysicalFile.filename), 196 extname: extname(videoPhysicalFile.filename),
203 resolution: videoFileHeight, 197 resolution: videoFileHeight,
@@ -205,9 +199,9 @@ function addVideo (req: express.Request, res: express.Response, videoPhysicalFil
205 } 199 }
206 200
207 const videoFile = db.VideoFile.build(videoFileData) 201 const videoFile = db.VideoFile.build(videoFileData)
208 return { author, tagInstances, video, videoFile } 202 return { tagInstances, video, videoFile }
209 }) 203 })
210 .then(({ author, tagInstances, video, videoFile }) => { 204 .then(({ tagInstances, video, videoFile }) => {
211 const videoDir = CONFIG.STORAGE.VIDEOS_DIR 205 const videoDir = CONFIG.STORAGE.VIDEOS_DIR
212 const source = join(videoDir, videoPhysicalFile.filename) 206 const source = join(videoDir, videoPhysicalFile.filename)
213 const destination = join(videoDir, video.getVideoFilename(videoFile)) 207 const destination = join(videoDir, video.getVideoFilename(videoFile))
@@ -216,10 +210,10 @@ function addVideo (req: express.Request, res: express.Response, videoPhysicalFil
216 .then(() => { 210 .then(() => {
217 // 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
218 videoPhysicalFile.filename = video.getVideoFilename(videoFile) 212 videoPhysicalFile.filename = video.getVideoFilename(videoFile)
219 return { author, tagInstances, video, videoFile } 213 return { tagInstances, video, videoFile }
220 }) 214 })
221 }) 215 })
222 .then(({ author, tagInstances, video, videoFile }) => { 216 .then(({ tagInstances, video, videoFile }) => {
223 const tasks = [] 217 const tasks = []
224 218
225 tasks.push( 219 tasks.push(
@@ -239,15 +233,15 @@ function addVideo (req: express.Request, res: express.Response, videoPhysicalFil
239 ) 233 )
240 } 234 }
241 235
242 return Promise.all(tasks).then(() => ({ author, tagInstances, video, videoFile })) 236 return Promise.all(tasks).then(() => ({ tagInstances, video, videoFile }))
243 }) 237 })
244 .then(({ author, tagInstances, video, videoFile }) => { 238 .then(({ tagInstances, video, videoFile }) => {
245 const options = { transaction: t } 239 const options = { transaction: t }
246 240
247 return video.save(options) 241 return video.save(options)
248 .then(videoCreated => { 242 .then(videoCreated => {
249 // Do not forget to add Author information to the created video 243 // Do not forget to add video channel information to the created video
250 videoCreated.Author = author 244 videoCreated.VideoChannel = res.locals.videoChannel
251 videoUUID = videoCreated.uuid 245 videoUUID = videoCreated.uuid
252 246
253 return { tagInstances, video: videoCreated, videoFile } 247 return { tagInstances, video: videoCreated, videoFile }
@@ -392,7 +386,7 @@ function getVideo (req: express.Request, res: express.Response) {
392 } 386 }
393 387
394 // Do not wait the view system 388 // Do not wait the view system
395 res.json(videoInstance.toFormattedJSON()) 389 res.json(videoInstance.toFormattedDetailsJSON())
396} 390}
397 391
398function listVideos (req: express.Request, res: express.Response, next: express.NextFunction) { 392function listVideos (req: express.Request, res: express.Response, next: express.NextFunction) {
diff --git a/server/controllers/services.ts b/server/controllers/services.ts
index 4bbe56a8a..99a33a716 100644
--- a/server/controllers/services.ts
+++ b/server/controllers/services.ts
@@ -47,7 +47,7 @@ function generateOEmbed (req: express.Request, res: express.Response, next: expr
47 width: embedWidth, 47 width: embedWidth,
48 height: embedHeight, 48 height: embedHeight,
49 title: video.name, 49 title: video.name,
50 author_name: video.Author.name, 50 author_name: video.VideoChannel.Author.name,
51 provider_name: 'PeerTube', 51 provider_name: 'PeerTube',
52 provider_url: webserverUrl 52 provider_url: webserverUrl
53 } 53 }
diff --git a/server/helpers/custom-validators/index.ts b/server/helpers/custom-validators/index.ts
index 1dcab624a..c79982660 100644
--- a/server/helpers/custom-validators/index.ts
+++ b/server/helpers/custom-validators/index.ts
@@ -3,4 +3,6 @@ export * from './misc'
3export * from './pods' 3export * from './pods'
4export * from './pods' 4export * from './pods'
5export * from './users' 5export * from './users'
6export * from './video-authors'
7export * from './video-channels'
6export * from './videos' 8export * from './videos'
diff --git a/server/helpers/custom-validators/misc.ts b/server/helpers/custom-validators/misc.ts
index 60fcdd5bb..160ec91f3 100644
--- a/server/helpers/custom-validators/misc.ts
+++ b/server/helpers/custom-validators/misc.ts
@@ -1,4 +1,4 @@
1import 'express-validator' 1import * as validator from 'validator'
2 2
3function exists (value: any) { 3function exists (value: any) {
4 return value !== undefined && value !== null 4 return value !== undefined && value !== null
@@ -8,9 +8,29 @@ function isArray (value: any) {
8 return Array.isArray(value) 8 return Array.isArray(value)
9} 9}
10 10
11function isDateValid (value: string) {
12 return exists(value) && validator.isISO8601(value)
13}
14
15function isIdValid (value: string) {
16 return exists(value) && validator.isInt('' + value)
17}
18
19function isUUIDValid (value: string) {
20 return exists(value) && validator.isUUID('' + value, 4)
21}
22
23function isIdOrUUIDValid (value: string) {
24 return isIdValid(value) || isUUIDValid(value)
25}
26
11// --------------------------------------------------------------------------- 27// ---------------------------------------------------------------------------
12 28
13export { 29export {
14 exists, 30 exists,
15 isArray 31 isArray,
32 isIdValid,
33 isUUIDValid,
34 isIdOrUUIDValid,
35 isDateValid
16} 36}
diff --git a/server/helpers/custom-validators/remote/videos.ts b/server/helpers/custom-validators/remote/videos.ts
index e261e05a8..057996f1c 100644
--- a/server/helpers/custom-validators/remote/videos.ts
+++ b/server/helpers/custom-validators/remote/videos.ts
@@ -6,18 +6,15 @@ import {
6 REQUEST_ENDPOINT_ACTIONS, 6 REQUEST_ENDPOINT_ACTIONS,
7 REQUEST_VIDEO_EVENT_TYPES 7 REQUEST_VIDEO_EVENT_TYPES
8} from '../../../initializers' 8} from '../../../initializers'
9import { isArray } from '../misc' 9import { isArray, isDateValid, isUUIDValid } from '../misc'
10import { 10import {
11 isVideoAuthorValid,
12 isVideoThumbnailDataValid, 11 isVideoThumbnailDataValid,
13 isVideoUUIDValid,
14 isVideoAbuseReasonValid, 12 isVideoAbuseReasonValid,
15 isVideoAbuseReporterUsernameValid, 13 isVideoAbuseReporterUsernameValid,
16 isVideoViewsValid, 14 isVideoViewsValid,
17 isVideoLikesValid, 15 isVideoLikesValid,
18 isVideoDislikesValid, 16 isVideoDislikesValid,
19 isVideoEventCountValid, 17 isVideoEventCountValid,
20 isVideoDateValid,
21 isVideoCategoryValid, 18 isVideoCategoryValid,
22 isVideoLicenceValid, 19 isVideoLicenceValid,
23 isVideoLanguageValid, 20 isVideoLanguageValid,
@@ -30,9 +27,22 @@ import {
30 isVideoFileExtnameValid, 27 isVideoFileExtnameValid,
31 isVideoFileResolutionValid 28 isVideoFileResolutionValid
32} from '../videos' 29} from '../videos'
30import { isVideoChannelDescriptionValid, isVideoChannelNameValid } from '../video-channels'
31import { isVideoAuthorNameValid } from '../video-authors'
33 32
34const ENDPOINT_ACTIONS = REQUEST_ENDPOINT_ACTIONS[REQUEST_ENDPOINTS.VIDEOS] 33const ENDPOINT_ACTIONS = REQUEST_ENDPOINT_ACTIONS[REQUEST_ENDPOINTS.VIDEOS]
35 34
35const checkers: { [ id: string ]: (obj: any) => boolean } = {}
36checkers[ENDPOINT_ACTIONS.ADD_VIDEO] = checkAddVideo
37checkers[ENDPOINT_ACTIONS.UPDATE_VIDEO] = checkUpdateVideo
38checkers[ENDPOINT_ACTIONS.REMOVE_VIDEO] = checkRemoveVideo
39checkers[ENDPOINT_ACTIONS.REPORT_ABUSE] = checkReportVideo
40checkers[ENDPOINT_ACTIONS.ADD_CHANNEL] = checkAddVideoChannel
41checkers[ENDPOINT_ACTIONS.UPDATE_CHANNEL] = checkUpdateVideoChannel
42checkers[ENDPOINT_ACTIONS.REMOVE_CHANNEL] = checkRemoveVideoChannel
43checkers[ENDPOINT_ACTIONS.ADD_AUTHOR] = checkAddAuthor
44checkers[ENDPOINT_ACTIONS.REMOVE_AUTHOR] = checkRemoveAuthor
45
36function isEachRemoteRequestVideosValid (requests: any[]) { 46function isEachRemoteRequestVideosValid (requests: any[]) {
37 return isArray(requests) && 47 return isArray(requests) &&
38 requests.every(request => { 48 requests.every(request => {
@@ -40,26 +50,11 @@ function isEachRemoteRequestVideosValid (requests: any[]) {
40 50
41 if (!video) return false 51 if (!video) return false
42 52
43 return ( 53 const checker = checkers[request.type]
44 isRequestTypeAddValid(request.type) && 54 // We don't know the request type
45 isCommonVideoAttributesValid(video) && 55 if (checker === undefined) return false
46 isVideoAuthorValid(video.author) && 56
47 isVideoThumbnailDataValid(video.thumbnailData) 57 return checker(video)
48 ) ||
49 (
50 isRequestTypeUpdateValid(request.type) &&
51 isCommonVideoAttributesValid(video)
52 ) ||
53 (
54 isRequestTypeRemoveValid(request.type) &&
55 isVideoUUIDValid(video.uuid)
56 ) ||
57 (
58 isRequestTypeReportAbuseValid(request.type) &&
59 isVideoUUIDValid(request.data.videoUUID) &&
60 isVideoAbuseReasonValid(request.data.reportReason) &&
61 isVideoAbuseReporterUsernameValid(request.data.reporterUsername)
62 )
63 }) 58 })
64} 59}
65 60
@@ -71,7 +66,7 @@ function isEachRemoteRequestVideosQaduValid (requests: any[]) {
71 if (!video) return false 66 if (!video) return false
72 67
73 return ( 68 return (
74 isVideoUUIDValid(video.uuid) && 69 isUUIDValid(video.uuid) &&
75 (has(video, 'views') === false || isVideoViewsValid(video.views)) && 70 (has(video, 'views') === false || isVideoViewsValid(video.views)) &&
76 (has(video, 'likes') === false || isVideoLikesValid(video.likes)) && 71 (has(video, 'likes') === false || isVideoLikesValid(video.likes)) &&
77 (has(video, 'dislikes') === false || isVideoDislikesValid(video.dislikes)) 72 (has(video, 'dislikes') === false || isVideoDislikesValid(video.dislikes))
@@ -87,7 +82,7 @@ function isEachRemoteRequestVideosEventsValid (requests: any[]) {
87 if (!eventData) return false 82 if (!eventData) return false
88 83
89 return ( 84 return (
90 isVideoUUIDValid(eventData.uuid) && 85 isUUIDValid(eventData.uuid) &&
91 values(REQUEST_VIDEO_EVENT_TYPES).indexOf(eventData.eventType) !== -1 && 86 values(REQUEST_VIDEO_EVENT_TYPES).indexOf(eventData.eventType) !== -1 &&
92 isVideoEventCountValid(eventData.count) 87 isVideoEventCountValid(eventData.count)
93 ) 88 )
@@ -105,8 +100,8 @@ export {
105// --------------------------------------------------------------------------- 100// ---------------------------------------------------------------------------
106 101
107function isCommonVideoAttributesValid (video: any) { 102function isCommonVideoAttributesValid (video: any) {
108 return isVideoDateValid(video.createdAt) && 103 return isDateValid(video.createdAt) &&
109 isVideoDateValid(video.updatedAt) && 104 isDateValid(video.updatedAt) &&
110 isVideoCategoryValid(video.category) && 105 isVideoCategoryValid(video.category) &&
111 isVideoLicenceValid(video.licence) && 106 isVideoLicenceValid(video.licence) &&
112 isVideoLanguageValid(video.language) && 107 isVideoLanguageValid(video.language) &&
@@ -115,7 +110,7 @@ function isCommonVideoAttributesValid (video: any) {
115 isVideoDurationValid(video.duration) && 110 isVideoDurationValid(video.duration) &&
116 isVideoNameValid(video.name) && 111 isVideoNameValid(video.name) &&
117 isVideoTagsValid(video.tags) && 112 isVideoTagsValid(video.tags) &&
118 isVideoUUIDValid(video.uuid) && 113 isUUIDValid(video.uuid) &&
119 isVideoViewsValid(video.views) && 114 isVideoViewsValid(video.views) &&
120 isVideoLikesValid(video.likes) && 115 isVideoLikesValid(video.likes) &&
121 isVideoDislikesValid(video.dislikes) && 116 isVideoDislikesValid(video.dislikes) &&
@@ -131,18 +126,53 @@ function isCommonVideoAttributesValid (video: any) {
131 }) 126 })
132} 127}
133 128
134function isRequestTypeAddValid (value: string) { 129function checkAddVideo (video: any) {
135 return value === ENDPOINT_ACTIONS.ADD 130 return isCommonVideoAttributesValid(video) &&
131 isUUIDValid(video.channelUUID) &&
132 isVideoThumbnailDataValid(video.thumbnailData)
133}
134
135function checkUpdateVideo (video: any) {
136 return isCommonVideoAttributesValid(video)
137}
138
139function checkRemoveVideo (video: any) {
140 return isUUIDValid(video.uuid)
141}
142
143function checkReportVideo (abuse: any) {
144 return isUUIDValid(abuse.videoUUID) &&
145 isVideoAbuseReasonValid(abuse.reportReason) &&
146 isVideoAbuseReporterUsernameValid(abuse.reporterUsername)
147}
148
149function checkAddVideoChannel (videoChannel: any) {
150 return isUUIDValid(videoChannel.uuid) &&
151 isVideoChannelNameValid(videoChannel.name) &&
152 isVideoChannelDescriptionValid(videoChannel.description) &&
153 isDateValid(videoChannel.createdAt) &&
154 isDateValid(videoChannel.updatedAt) &&
155 isUUIDValid(videoChannel.ownerUUID)
156}
157
158function checkUpdateVideoChannel (videoChannel: any) {
159 return isUUIDValid(videoChannel.uuid) &&
160 isVideoChannelNameValid(videoChannel.name) &&
161 isVideoChannelDescriptionValid(videoChannel.description) &&
162 isDateValid(videoChannel.createdAt) &&
163 isDateValid(videoChannel.updatedAt) &&
164 isUUIDValid(videoChannel.ownerUUID)
136} 165}
137 166
138function isRequestTypeUpdateValid (value: string) { 167function checkRemoveVideoChannel (videoChannel: any) {
139 return value === ENDPOINT_ACTIONS.UPDATE 168 return isUUIDValid(videoChannel.uuid)
140} 169}
141 170
142function isRequestTypeRemoveValid (value: string) { 171function checkAddAuthor (author: any) {
143 return value === ENDPOINT_ACTIONS.REMOVE 172 return isUUIDValid(author.uuid) &&
173 isVideoAuthorNameValid(author.name)
144} 174}
145 175
146function isRequestTypeReportAbuseValid (value: string) { 176function checkRemoveAuthor (author: any) {
147 return value === ENDPOINT_ACTIONS.REPORT_ABUSE 177 return isUUIDValid(author.uuid)
148} 178}
diff --git a/server/helpers/custom-validators/video-authors.ts b/server/helpers/custom-validators/video-authors.ts
new file mode 100644
index 000000000..48ca9b200
--- /dev/null
+++ b/server/helpers/custom-validators/video-authors.ts
@@ -0,0 +1,45 @@
1import * as Promise from 'bluebird'
2import * as validator from 'validator'
3import * as express from 'express'
4import 'express-validator'
5
6import { database as db } from '../../initializers'
7import { AuthorInstance } from '../../models'
8import { logger } from '../logger'
9
10import { isUserUsernameValid } from './users'
11
12function isVideoAuthorNameValid (value: string) {
13 return isUserUsernameValid(value)
14}
15
16function checkVideoAuthorExists (id: string, res: express.Response, callback: () => void) {
17 let promise: Promise<AuthorInstance>
18 if (validator.isInt(id)) {
19 promise = db.Author.load(+id)
20 } else { // UUID
21 promise = db.Author.loadByUUID(id)
22 }
23
24 promise.then(author => {
25 if (!author) {
26 return res.status(404)
27 .json({ error: 'Video author not found' })
28 .end()
29 }
30
31 res.locals.author = author
32 callback()
33 })
34 .catch(err => {
35 logger.error('Error in video author request validator.', err)
36 return res.sendStatus(500)
37 })
38}
39
40// ---------------------------------------------------------------------------
41
42export {
43 checkVideoAuthorExists,
44 isVideoAuthorNameValid
45}
diff --git a/server/helpers/custom-validators/video-channels.ts b/server/helpers/custom-validators/video-channels.ts
new file mode 100644
index 000000000..b6be557e6
--- /dev/null
+++ b/server/helpers/custom-validators/video-channels.ts
@@ -0,0 +1,57 @@
1import * as Promise from 'bluebird'
2import * as validator from 'validator'
3import * as express from 'express'
4import 'express-validator'
5import 'multer'
6
7import { database as db, CONSTRAINTS_FIELDS } from '../../initializers'
8import { VideoChannelInstance } from '../../models'
9import { logger } from '../logger'
10import { exists } from './misc'
11
12const VIDEO_CHANNELS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEO_CHANNELS
13
14function isVideoChannelDescriptionValid (value: string) {
15 return value === null || validator.isLength(value, VIDEO_CHANNELS_CONSTRAINTS_FIELDS.DESCRIPTION)
16}
17
18function isVideoChannelNameValid (value: string) {
19 return exists(value) && validator.isLength(value, VIDEO_CHANNELS_CONSTRAINTS_FIELDS.NAME)
20}
21
22function isVideoChannelUUIDValid (value: string) {
23 return exists(value) && validator.isUUID('' + value, 4)
24}
25
26function checkVideoChannelExists (id: string, res: express.Response, callback: () => void) {
27 let promise: Promise<VideoChannelInstance>
28 if (validator.isInt(id)) {
29 promise = db.VideoChannel.loadAndPopulateAuthor(+id)
30 } else { // UUID
31 promise = db.VideoChannel.loadByUUIDAndPopulateAuthor(id)
32 }
33
34 promise.then(videoChannel => {
35 if (!videoChannel) {
36 return res.status(404)
37 .json({ error: 'Video channel not found' })
38 .end()
39 }
40
41 res.locals.videoChannel = videoChannel
42 callback()
43 })
44 .catch(err => {
45 logger.error('Error in video channel request validator.', err)
46 return res.sendStatus(500)
47 })
48}
49
50// ---------------------------------------------------------------------------
51
52export {
53 isVideoChannelDescriptionValid,
54 isVideoChannelNameValid,
55 isVideoChannelUUIDValid,
56 checkVideoChannelExists
57}
diff --git a/server/helpers/custom-validators/videos.ts b/server/helpers/custom-validators/videos.ts
index 05d1dc607..4e441fe5f 100644
--- a/server/helpers/custom-validators/videos.ts
+++ b/server/helpers/custom-validators/videos.ts
@@ -23,18 +23,6 @@ const VIDEOS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEOS
23const VIDEO_ABUSES_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEO_ABUSES 23const VIDEO_ABUSES_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEO_ABUSES
24const VIDEO_EVENTS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEO_EVENTS 24const VIDEO_EVENTS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEO_EVENTS
25 25
26function isVideoIdOrUUIDValid (value: string) {
27 return validator.isInt(value) || isVideoUUIDValid(value)
28}
29
30function isVideoAuthorValid (value: string) {
31 return isUserUsernameValid(value)
32}
33
34function isVideoDateValid (value: string) {
35 return exists(value) && validator.isISO8601(value)
36}
37
38function isVideoCategoryValid (value: number) { 26function isVideoCategoryValid (value: number) {
39 return VIDEO_CATEGORIES[value] !== undefined 27 return VIDEO_CATEGORIES[value] !== undefined
40} 28}
@@ -79,10 +67,6 @@ function isVideoThumbnailDataValid (value: string) {
79 return exists(value) && validator.isByteLength(value, VIDEOS_CONSTRAINTS_FIELDS.THUMBNAIL_DATA) 67 return exists(value) && validator.isByteLength(value, VIDEOS_CONSTRAINTS_FIELDS.THUMBNAIL_DATA)
80} 68}
81 69
82function isVideoUUIDValid (value: string) {
83 return exists(value) && validator.isUUID('' + value, 4)
84}
85
86function isVideoAbuseReasonValid (value: string) { 70function isVideoAbuseReasonValid (value: string) {
87 return exists(value) && validator.isLength(value, VIDEO_ABUSES_CONSTRAINTS_FIELDS.REASON) 71 return exists(value) && validator.isLength(value, VIDEO_ABUSES_CONSTRAINTS_FIELDS.REASON)
88} 72}
@@ -170,9 +154,6 @@ function checkVideoExists (id: string, res: express.Response, callback: () => vo
170// --------------------------------------------------------------------------- 154// ---------------------------------------------------------------------------
171 155
172export { 156export {
173 isVideoIdOrUUIDValid,
174 isVideoAuthorValid,
175 isVideoDateValid,
176 isVideoCategoryValid, 157 isVideoCategoryValid,
177 isVideoLicenceValid, 158 isVideoLicenceValid,
178 isVideoLanguageValid, 159 isVideoLanguageValid,
@@ -185,7 +166,6 @@ export {
185 isVideoThumbnailValid, 166 isVideoThumbnailValid,
186 isVideoThumbnailDataValid, 167 isVideoThumbnailDataValid,
187 isVideoFileExtnameValid, 168 isVideoFileExtnameValid,
188 isVideoUUIDValid,
189 isVideoAbuseReasonValid, 169 isVideoAbuseReasonValid,
190 isVideoAbuseReporterUsernameValid, 170 isVideoAbuseReporterUsernameValid,
191 isVideoFile, 171 isVideoFile,
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts
index 132164746..54dce980f 100644
--- a/server/initializers/constants.ts
+++ b/server/initializers/constants.ts
@@ -10,6 +10,7 @@ import {
10 RequestEndpoint, 10 RequestEndpoint,
11 RequestVideoEventType, 11 RequestVideoEventType,
12 RequestVideoQaduType, 12 RequestVideoQaduType,
13 RemoteVideoRequestType,
13 JobState 14 JobState
14} from '../../shared/models' 15} from '../../shared/models'
15 16
@@ -35,6 +36,7 @@ const SORTABLE_COLUMNS = {
35 PODS: [ 'id', 'host', 'score', 'createdAt' ], 36 PODS: [ 'id', 'host', 'score', 'createdAt' ],
36 USERS: [ 'id', 'username', 'createdAt' ], 37 USERS: [ 'id', 'username', 'createdAt' ],
37 VIDEO_ABUSES: [ 'id', 'createdAt' ], 38 VIDEO_ABUSES: [ 'id', 'createdAt' ],
39 VIDEO_CHANNELS: [ 'id', 'name', 'updatedAt', 'createdAt' ],
38 VIDEOS: [ 'name', 'duration', 'createdAt', 'views', 'likes' ], 40 VIDEOS: [ 'name', 'duration', 'createdAt', 'views', 'likes' ],
39 BLACKLISTS: [ 'id', 'name', 'duration', 'views', 'likes', 'dislikes', 'uuid', 'createdAt' ] 41 BLACKLISTS: [ 'id', 'name', 'duration', 'views', 'likes', 'dislikes', 'uuid', 'createdAt' ]
40} 42}
@@ -115,6 +117,10 @@ const CONSTRAINTS_FIELDS = {
115 VIDEO_ABUSES: { 117 VIDEO_ABUSES: {
116 REASON: { min: 2, max: 300 } // Length 118 REASON: { min: 2, max: 300 } // Length
117 }, 119 },
120 VIDEO_CHANNELS: {
121 NAME: { min: 3, max: 50 }, // Length
122 DESCRIPTION: { min: 3, max: 250 } // Length
123 },
118 VIDEOS: { 124 VIDEOS: {
119 NAME: { min: 3, max: 50 }, // Length 125 NAME: { min: 3, max: 50 }, // Length
120 DESCRIPTION: { min: 3, max: 250 }, // Length 126 DESCRIPTION: { min: 3, max: 250 }, // Length
@@ -232,11 +238,20 @@ const REQUEST_ENDPOINTS: { [ id: string ]: RequestEndpoint } = {
232 VIDEOS: 'videos' 238 VIDEOS: 'videos'
233} 239}
234 240
235const REQUEST_ENDPOINT_ACTIONS: { [ id: string ]: any } = {} 241const REQUEST_ENDPOINT_ACTIONS: {
242 [ id: string ]: {
243 [ id: string ]: RemoteVideoRequestType
244 }
245} = {}
236REQUEST_ENDPOINT_ACTIONS[REQUEST_ENDPOINTS.VIDEOS] = { 246REQUEST_ENDPOINT_ACTIONS[REQUEST_ENDPOINTS.VIDEOS] = {
237 ADD: 'add', 247 ADD_VIDEO: 'add-video',
238 UPDATE: 'update', 248 UPDATE_VIDEO: 'update-video',
239 REMOVE: 'remove', 249 REMOVE_VIDEO: 'remove-video',
250 ADD_CHANNEL: 'add-channel',
251 UPDATE_CHANNEL: 'update-channel',
252 REMOVE_CHANNEL: 'remove-channel',
253 ADD_AUTHOR: 'add-author',
254 REMOVE_AUTHOR: 'remove-author',
240 REPORT_ABUSE: 'report-abuse' 255 REPORT_ABUSE: 'report-abuse'
241} 256}
242 257
diff --git a/server/initializers/database.ts b/server/initializers/database.ts
index c5a385361..d461cb440 100644
--- a/server/initializers/database.ts
+++ b/server/initializers/database.ts
@@ -14,6 +14,7 @@ import { VideoTagModel } from './../models/video/video-tag-interface'
14import { BlacklistedVideoModel } from './../models/video/video-blacklist-interface' 14import { BlacklistedVideoModel } from './../models/video/video-blacklist-interface'
15import { VideoFileModel } from './../models/video/video-file-interface' 15import { VideoFileModel } from './../models/video/video-file-interface'
16import { VideoAbuseModel } from './../models/video/video-abuse-interface' 16import { VideoAbuseModel } from './../models/video/video-abuse-interface'
17import { VideoChannelModel } from './../models/video/video-channel-interface'
17import { UserModel } from './../models/user/user-interface' 18import { UserModel } from './../models/user/user-interface'
18import { UserVideoRateModel } from './../models/user/user-video-rate-interface' 19import { UserVideoRateModel } from './../models/user/user-video-rate-interface'
19import { TagModel } from './../models/video/tag-interface' 20import { TagModel } from './../models/video/tag-interface'
@@ -50,6 +51,7 @@ const database: {
50 UserVideoRate?: UserVideoRateModel, 51 UserVideoRate?: UserVideoRateModel,
51 User?: UserModel, 52 User?: UserModel,
52 VideoAbuse?: VideoAbuseModel, 53 VideoAbuse?: VideoAbuseModel,
54 VideoChannel?: VideoChannelModel,
53 VideoFile?: VideoFileModel, 55 VideoFile?: VideoFileModel,
54 BlacklistedVideo?: BlacklistedVideoModel, 56 BlacklistedVideo?: BlacklistedVideoModel,
55 VideoTag?: VideoTagModel, 57 VideoTag?: VideoTagModel,
diff --git a/server/initializers/installer.ts b/server/initializers/installer.ts
index 10b74b85f..b997de07f 100644
--- a/server/initializers/installer.ts
+++ b/server/initializers/installer.ts
@@ -5,6 +5,7 @@ import { database as db } from './database'
5import { USER_ROLES, CONFIG, LAST_MIGRATION_VERSION, CACHE } from './constants' 5import { USER_ROLES, CONFIG, LAST_MIGRATION_VERSION, CACHE } from './constants'
6import { clientsExist, usersExist } from './checker' 6import { clientsExist, usersExist } from './checker'
7import { logger, createCertsIfNotExist, mkdirpPromise, rimrafPromise } from '../helpers' 7import { logger, createCertsIfNotExist, mkdirpPromise, rimrafPromise } from '../helpers'
8import { createUserAuthorAndChannel } from '../lib'
8 9
9function installApplication () { 10function installApplication () {
10 return db.sequelize.sync() 11 return db.sequelize.sync()
@@ -91,7 +92,7 @@ function createOAuthAdminIfNotExist () {
91 const username = 'root' 92 const username = 'root'
92 const role = USER_ROLES.ADMIN 93 const role = USER_ROLES.ADMIN
93 const email = CONFIG.ADMIN.EMAIL 94 const email = CONFIG.ADMIN.EMAIL
94 const createOptions: { validate?: boolean } = {} 95 let validatePassword = true
95 let password = '' 96 let password = ''
96 97
97 // Do not generate a random password for tests 98 // Do not generate a random password for tests
@@ -103,7 +104,7 @@ function createOAuthAdminIfNotExist () {
103 } 104 }
104 105
105 // Our password is weak so do not validate it 106 // Our password is weak so do not validate it
106 createOptions.validate = false 107 validatePassword = false
107 } else { 108 } else {
108 password = passwordGenerator(8, true) 109 password = passwordGenerator(8, true)
109 } 110 }
@@ -115,13 +116,15 @@ function createOAuthAdminIfNotExist () {
115 role, 116 role,
116 videoQuota: -1 117 videoQuota: -1
117 } 118 }
119 const user = db.User.build(userData)
118 120
119 return db.User.create(userData, createOptions).then(createdUser => { 121 return createUserAuthorAndChannel(user, validatePassword)
120 logger.info('Username: ' + username) 122 .then(({ user }) => {
121 logger.info('User password: ' + password) 123 logger.info('Username: ' + username)
124 logger.info('User password: ' + password)
122 125
123 logger.info('Creating Application table.') 126 logger.info('Creating Application table.')
124 return db.Application.create({ migrationVersion: LAST_MIGRATION_VERSION }) 127 return db.Application.create({ migrationVersion: LAST_MIGRATION_VERSION })
125 }) 128 })
126 }) 129 })
127} 130}
diff --git a/server/lib/cache/videos-preview-cache.ts b/server/lib/cache/videos-preview-cache.ts
index 09e2e9a0d..fecdca6ef 100644
--- a/server/lib/cache/videos-preview-cache.ts
+++ b/server/lib/cache/videos-preview-cache.ts
@@ -55,7 +55,7 @@ class VideosPreviewCache {
55 } 55 }
56 56
57 private saveRemotePreviewAndReturnPath (video: VideoInstance) { 57 private saveRemotePreviewAndReturnPath (video: VideoInstance) {
58 const req = fetchRemotePreview(video.Author.Pod, video) 58 const req = fetchRemotePreview(video)
59 59
60 return new Promise<string>((res, rej) => { 60 return new Promise<string>((res, rej) => {
61 const path = join(CACHE.DIRECTORIES.PREVIEWS, video.getPreviewName()) 61 const path = join(CACHE.DIRECTORIES.PREVIEWS, video.getPreviewName())
diff --git a/server/lib/friends.ts b/server/lib/friends.ts
index 65349ef5f..f035b099b 100644
--- a/server/lib/friends.ts
+++ b/server/lib/friends.ts
@@ -42,7 +42,13 @@ import {
42 RemoteVideoRemoveData, 42 RemoteVideoRemoveData,
43 RemoteVideoReportAbuseData, 43 RemoteVideoReportAbuseData,
44 ResultList, 44 ResultList,
45 Pod as FormattedPod 45 RemoteVideoRequestType,
46 Pod as FormattedPod,
47 RemoteVideoChannelCreateData,
48 RemoteVideoChannelUpdateData,
49 RemoteVideoChannelRemoveData,
50 RemoteVideoAuthorCreateData,
51 RemoteVideoAuthorRemoveData
46} from '../../shared' 52} from '../../shared'
47 53
48type QaduParam = { videoId: number, type: RequestVideoQaduType } 54type QaduParam = { videoId: number, type: RequestVideoQaduType }
@@ -62,7 +68,7 @@ function activateSchedulers () {
62 68
63function addVideoToFriends (videoData: RemoteVideoCreateData, transaction: Sequelize.Transaction) { 69function addVideoToFriends (videoData: RemoteVideoCreateData, transaction: Sequelize.Transaction) {
64 const options = { 70 const options = {
65 type: ENDPOINT_ACTIONS.ADD, 71 type: ENDPOINT_ACTIONS.ADD_VIDEO,
66 endpoint: REQUEST_ENDPOINTS.VIDEOS, 72 endpoint: REQUEST_ENDPOINTS.VIDEOS,
67 data: videoData, 73 data: videoData,
68 transaction 74 transaction
@@ -72,7 +78,7 @@ function addVideoToFriends (videoData: RemoteVideoCreateData, transaction: Seque
72 78
73function updateVideoToFriends (videoData: RemoteVideoUpdateData, transaction: Sequelize.Transaction) { 79function updateVideoToFriends (videoData: RemoteVideoUpdateData, transaction: Sequelize.Transaction) {
74 const options = { 80 const options = {
75 type: ENDPOINT_ACTIONS.UPDATE, 81 type: ENDPOINT_ACTIONS.UPDATE_VIDEO,
76 endpoint: REQUEST_ENDPOINTS.VIDEOS, 82 endpoint: REQUEST_ENDPOINTS.VIDEOS,
77 data: videoData, 83 data: videoData,
78 transaction 84 transaction
@@ -82,7 +88,7 @@ function updateVideoToFriends (videoData: RemoteVideoUpdateData, transaction: Se
82 88
83function removeVideoToFriends (videoParams: RemoteVideoRemoveData, transaction: Sequelize.Transaction) { 89function removeVideoToFriends (videoParams: RemoteVideoRemoveData, transaction: Sequelize.Transaction) {
84 const options = { 90 const options = {
85 type: ENDPOINT_ACTIONS.REMOVE, 91 type: ENDPOINT_ACTIONS.REMOVE_VIDEO,
86 endpoint: REQUEST_ENDPOINTS.VIDEOS, 92 endpoint: REQUEST_ENDPOINTS.VIDEOS,
87 data: videoParams, 93 data: videoParams,
88 transaction 94 transaction
@@ -90,12 +96,62 @@ function removeVideoToFriends (videoParams: RemoteVideoRemoveData, transaction:
90 return createRequest(options) 96 return createRequest(options)
91} 97}
92 98
99function addVideoAuthorToFriends (authorData: RemoteVideoAuthorCreateData, transaction: Sequelize.Transaction) {
100 const options = {
101 type: ENDPOINT_ACTIONS.ADD_AUTHOR,
102 endpoint: REQUEST_ENDPOINTS.VIDEOS,
103 data: authorData,
104 transaction
105 }
106 return createRequest(options)
107}
108
109function removeVideoAuthorToFriends (authorData: RemoteVideoAuthorRemoveData, transaction: Sequelize.Transaction) {
110 const options = {
111 type: ENDPOINT_ACTIONS.REMOVE_AUTHOR,
112 endpoint: REQUEST_ENDPOINTS.VIDEOS,
113 data: authorData,
114 transaction
115 }
116 return createRequest(options)
117}
118
119function addVideoChannelToFriends (videoChannelData: RemoteVideoChannelCreateData, transaction: Sequelize.Transaction) {
120 const options = {
121 type: ENDPOINT_ACTIONS.ADD_CHANNEL,
122 endpoint: REQUEST_ENDPOINTS.VIDEOS,
123 data: videoChannelData,
124 transaction
125 }
126 return createRequest(options)
127}
128
129function updateVideoChannelToFriends (videoChannelData: RemoteVideoChannelUpdateData, transaction: Sequelize.Transaction) {
130 const options = {
131 type: ENDPOINT_ACTIONS.UPDATE_CHANNEL,
132 endpoint: REQUEST_ENDPOINTS.VIDEOS,
133 data: videoChannelData,
134 transaction
135 }
136 return createRequest(options)
137}
138
139function removeVideoChannelToFriends (videoChannelParams: RemoteVideoChannelRemoveData, transaction: Sequelize.Transaction) {
140 const options = {
141 type: ENDPOINT_ACTIONS.REMOVE_CHANNEL,
142 endpoint: REQUEST_ENDPOINTS.VIDEOS,
143 data: videoChannelParams,
144 transaction
145 }
146 return createRequest(options)
147}
148
93function reportAbuseVideoToFriend (reportData: RemoteVideoReportAbuseData, video: VideoInstance, transaction: Sequelize.Transaction) { 149function reportAbuseVideoToFriend (reportData: RemoteVideoReportAbuseData, video: VideoInstance, transaction: Sequelize.Transaction) {
94 const options = { 150 const options = {
95 type: ENDPOINT_ACTIONS.REPORT_ABUSE, 151 type: ENDPOINT_ACTIONS.REPORT_ABUSE,
96 endpoint: REQUEST_ENDPOINTS.VIDEOS, 152 endpoint: REQUEST_ENDPOINTS.VIDEOS,
97 data: reportData, 153 data: reportData,
98 toIds: [ video.Author.podId ], 154 toIds: [ video.VideoChannel.Author.podId ],
99 transaction 155 transaction
100 } 156 }
101 return createRequest(options) 157 return createRequest(options)
@@ -207,15 +263,66 @@ function quitFriends () {
207 .finally(() => requestScheduler.activate()) 263 .finally(() => requestScheduler.activate())
208} 264}
209 265
266function sendOwnedDataToPod (podId: number) {
267 // First send authors
268 return sendOwnedAuthorsToPod(podId)
269 .then(() => sendOwnedChannelsToPod(podId))
270 .then(() => sendOwnedVideosToPod(podId))
271}
272
273function sendOwnedChannelsToPod (podId: number) {
274 return db.VideoChannel.listOwned()
275 .then(videoChannels => {
276 const tasks = []
277 videoChannels.forEach(videoChannel => {
278 const remoteVideoChannel = videoChannel.toAddRemoteJSON()
279 const options = {
280 type: 'add-channel' as 'add-channel',
281 endpoint: REQUEST_ENDPOINTS.VIDEOS,
282 data: remoteVideoChannel,
283 toIds: [ podId ],
284 transaction: null
285 }
286
287 const p = createRequest(options)
288 tasks.push(p)
289 })
290
291 return Promise.all(tasks)
292 })
293}
294
295function sendOwnedAuthorsToPod (podId: number) {
296 return db.Author.listOwned()
297 .then(authors => {
298 const tasks = []
299 authors.forEach(author => {
300 const remoteAuthor = author.toAddRemoteJSON()
301 const options = {
302 type: 'add-author' as 'add-author',
303 endpoint: REQUEST_ENDPOINTS.VIDEOS,
304 data: remoteAuthor,
305 toIds: [ podId ],
306 transaction: null
307 }
308
309 const p = createRequest(options)
310 tasks.push(p)
311 })
312
313 return Promise.all(tasks)
314 })
315}
316
210function sendOwnedVideosToPod (podId: number) { 317function sendOwnedVideosToPod (podId: number) {
211 db.Video.listOwnedAndPopulateAuthorAndTags() 318 return db.Video.listOwnedAndPopulateAuthorAndTags()
212 .then(videosList => { 319 .then(videosList => {
213 const tasks = [] 320 const tasks = []
214 videosList.forEach(video => { 321 videosList.forEach(video => {
215 const promise = video.toAddRemoteJSON() 322 const promise = video.toAddRemoteJSON()
216 .then(remoteVideo => { 323 .then(remoteVideo => {
217 const options = { 324 const options = {
218 type: 'add', 325 type: 'add-video' as 'add-video',
219 endpoint: REQUEST_ENDPOINTS.VIDEOS, 326 endpoint: REQUEST_ENDPOINTS.VIDEOS,
220 data: remoteVideo, 327 data: remoteVideo,
221 toIds: [ podId ], 328 toIds: [ podId ],
@@ -236,8 +343,8 @@ function sendOwnedVideosToPod (podId: number) {
236 }) 343 })
237} 344}
238 345
239function fetchRemotePreview (pod: PodInstance, video: VideoInstance) { 346function fetchRemotePreview (video: VideoInstance) {
240 const host = video.Author.Pod.host 347 const host = video.VideoChannel.Author.Pod.host
241 const path = join(STATIC_PATHS.PREVIEWS, video.getPreviewName()) 348 const path = join(STATIC_PATHS.PREVIEWS, video.getPreviewName())
242 349
243 return request.get(REMOTE_SCHEME.HTTP + '://' + host + path) 350 return request.get(REMOTE_SCHEME.HTTP + '://' + host + path)
@@ -274,7 +381,9 @@ function getRequestVideoEventScheduler () {
274export { 381export {
275 activateSchedulers, 382 activateSchedulers,
276 addVideoToFriends, 383 addVideoToFriends,
384 removeVideoAuthorToFriends,
277 updateVideoToFriends, 385 updateVideoToFriends,
386 addVideoAuthorToFriends,
278 reportAbuseVideoToFriend, 387 reportAbuseVideoToFriend,
279 quickAndDirtyUpdateVideoToFriends, 388 quickAndDirtyUpdateVideoToFriends,
280 quickAndDirtyUpdatesVideoToFriends, 389 quickAndDirtyUpdatesVideoToFriends,
@@ -285,11 +394,14 @@ export {
285 quitFriends, 394 quitFriends,
286 removeFriend, 395 removeFriend,
287 removeVideoToFriends, 396 removeVideoToFriends,
288 sendOwnedVideosToPod, 397 sendOwnedDataToPod,
289 getRequestScheduler, 398 getRequestScheduler,
290 getRequestVideoQaduScheduler, 399 getRequestVideoQaduScheduler,
291 getRequestVideoEventScheduler, 400 getRequestVideoEventScheduler,
292 fetchRemotePreview 401 fetchRemotePreview,
402 addVideoChannelToFriends,
403 updateVideoChannelToFriends,
404 removeVideoChannelToFriends
293} 405}
294 406
295// --------------------------------------------------------------------------- 407// ---------------------------------------------------------------------------
@@ -373,7 +485,7 @@ function makeRequestsToWinningPods (cert: string, podsList: PodInstance[]) {
373 .then(podCreated => { 485 .then(podCreated => {
374 486
375 // Add our videos to the request scheduler 487 // Add our videos to the request scheduler
376 sendOwnedVideosToPod(podCreated.id) 488 sendOwnedDataToPod(podCreated.id)
377 }) 489 })
378 .catch(err => { 490 .catch(err => {
379 logger.error('Cannot add friend %s pod.', pod.host, err) 491 logger.error('Cannot add friend %s pod.', pod.host, err)
@@ -397,7 +509,7 @@ function makeRequestsToWinningPods (cert: string, podsList: PodInstance[]) {
397 509
398// Wrapper that populate "toIds" argument with all our friends if it is not specified 510// Wrapper that populate "toIds" argument with all our friends if it is not specified
399type CreateRequestOptions = { 511type CreateRequestOptions = {
400 type: string 512 type: RemoteVideoRequestType
401 endpoint: RequestEndpoint 513 endpoint: RequestEndpoint
402 data: Object 514 data: Object
403 toIds?: number[] 515 toIds?: number[]
diff --git a/server/lib/index.ts b/server/lib/index.ts
index 8628da4dd..d1534b085 100644
--- a/server/lib/index.ts
+++ b/server/lib/index.ts
@@ -3,3 +3,5 @@ export * from './jobs'
3export * from './request' 3export * from './request'
4export * from './friends' 4export * from './friends'
5export * from './oauth-model' 5export * from './oauth-model'
6export * from './user'
7export * from './video-channel'
diff --git a/server/lib/user.ts b/server/lib/user.ts
new file mode 100644
index 000000000..8609e72d8
--- /dev/null
+++ b/server/lib/user.ts
@@ -0,0 +1,46 @@
1import { database as db } from '../initializers'
2import { UserInstance } from '../models'
3import { addVideoAuthorToFriends } from './friends'
4import { createVideoChannel } from './video-channel'
5
6function createUserAuthorAndChannel (user: UserInstance, validateUser = true) {
7 return db.sequelize.transaction(t => {
8 const userOptions = {
9 transaction: t,
10 validate: validateUser
11 }
12
13 return user.save(userOptions)
14 .then(user => {
15 const author = db.Author.build({
16 name: user.username,
17 podId: null, // It is our pod
18 userId: user.id
19 })
20
21 return author.save({ transaction: t })
22 .then(author => ({ author, user }))
23 })
24 .then(({ author, user }) => {
25 const remoteVideoAuthor = author.toAddRemoteJSON()
26
27 // Now we'll add the video channel's meta data to our friends
28 return addVideoAuthorToFriends(remoteVideoAuthor, t)
29 .then(() => ({ author, user }))
30 })
31 .then(({ author, user }) => {
32 const videoChannelInfo = {
33 name: `Default ${user.username} channel`
34 }
35
36 return createVideoChannel(videoChannelInfo, author, t)
37 .then(videoChannel => ({ author, user, videoChannel }))
38 })
39 })
40}
41
42// ---------------------------------------------------------------------------
43
44export {
45 createUserAuthorAndChannel
46}
diff --git a/server/lib/video-channel.ts b/server/lib/video-channel.ts
new file mode 100644
index 000000000..224179973
--- /dev/null
+++ b/server/lib/video-channel.ts
@@ -0,0 +1,42 @@
1import * as Sequelize from 'sequelize'
2
3import { addVideoChannelToFriends } from './friends'
4import { database as db } from '../initializers'
5import { AuthorInstance } from '../models'
6import { VideoChannelCreate } from '../../shared/models'
7
8function createVideoChannel (videoChannelInfo: VideoChannelCreate, author: AuthorInstance, t: Sequelize.Transaction) {
9 let videoChannelUUID = ''
10
11 const videoChannelData = {
12 name: videoChannelInfo.name,
13 description: videoChannelInfo.description,
14 remote: false,
15 authorId: author.id
16 }
17
18 const videoChannel = db.VideoChannel.build(videoChannelData)
19 const options = { transaction: t }
20
21 return videoChannel.save(options)
22 .then(videoChannelCreated => {
23 // Do not forget to add Author information to the created video channel
24 videoChannelCreated.Author = author
25 videoChannelUUID = videoChannelCreated.uuid
26
27 return videoChannelCreated
28 })
29 .then(videoChannel => {
30 const remoteVideoChannel = videoChannel.toAddRemoteJSON()
31
32 // Now we'll add the video channel's meta data to our friends
33 return addVideoChannelToFriends(remoteVideoChannel, t)
34 })
35 .then(() => videoChannelUUID) // Return video channel UUID
36}
37
38// ---------------------------------------------------------------------------
39
40export {
41 createVideoChannel
42}
diff --git a/server/middlewares/sort.ts b/server/middlewares/sort.ts
index 2c70ff5f0..91aa3e5b6 100644
--- a/server/middlewares/sort.ts
+++ b/server/middlewares/sort.ts
@@ -22,6 +22,12 @@ function setVideoAbusesSort (req: express.Request, res: express.Response, next:
22 return next() 22 return next()
23} 23}
24 24
25function setVideoChannelsSort (req: express.Request, res: express.Response, next: express.NextFunction) {
26 if (!req.query.sort) req.query.sort = '-createdAt'
27
28 return next()
29}
30
25function setVideosSort (req: express.Request, res: express.Response, next: express.NextFunction) { 31function setVideosSort (req: express.Request, res: express.Response, next: express.NextFunction) {
26 if (!req.query.sort) req.query.sort = '-createdAt' 32 if (!req.query.sort) req.query.sort = '-createdAt'
27 33
@@ -55,6 +61,7 @@ export {
55 setPodsSort, 61 setPodsSort,
56 setUsersSort, 62 setUsersSort,
57 setVideoAbusesSort, 63 setVideoAbusesSort,
64 setVideoChannelsSort,
58 setVideosSort, 65 setVideosSort,
59 setBlacklistSort 66 setBlacklistSort
60} 67}
diff --git a/server/middlewares/validators/index.ts b/server/middlewares/validators/index.ts
index 068c41b24..247f6039e 100644
--- a/server/middlewares/validators/index.ts
+++ b/server/middlewares/validators/index.ts
@@ -6,3 +6,4 @@ export * from './sort'
6export * from './users' 6export * from './users'
7export * from './videos' 7export * from './videos'
8export * from './video-blacklist' 8export * from './video-blacklist'
9export * from './video-channels'
diff --git a/server/middlewares/validators/oembed.ts b/server/middlewares/validators/oembed.ts
index 4b8c03faf..f8e34d2d4 100644
--- a/server/middlewares/validators/oembed.ts
+++ b/server/middlewares/validators/oembed.ts
@@ -4,9 +4,12 @@ import { join } from 'path'
4 4
5import { checkErrors } from './utils' 5import { checkErrors } from './utils'
6import { CONFIG } from '../../initializers' 6import { CONFIG } from '../../initializers'
7import { logger } from '../../helpers' 7import {
8import { checkVideoExists, isVideoIdOrUUIDValid } from '../../helpers/custom-validators/videos' 8 logger,
9import { isTestInstance } from '../../helpers/core-utils' 9 isTestInstance,
10 checkVideoExists,
11 isIdOrUUIDValid
12} from '../../helpers'
10 13
11const urlShouldStartWith = CONFIG.WEBSERVER.SCHEME + '://' + join(CONFIG.WEBSERVER.HOST, 'videos', 'watch') + '/' 14const urlShouldStartWith = CONFIG.WEBSERVER.SCHEME + '://' + join(CONFIG.WEBSERVER.HOST, 'videos', 'watch') + '/'
12const videoWatchRegex = new RegExp('([^/]+)$') 15const videoWatchRegex = new RegExp('([^/]+)$')
@@ -45,7 +48,7 @@ const oembedValidator = [
45 } 48 }
46 49
47 const videoId = matches[1] 50 const videoId = matches[1]
48 if (isVideoIdOrUUIDValid(videoId) === false) { 51 if (isIdOrUUIDValid(videoId) === false) {
49 return res.status(400) 52 return res.status(400)
50 .json({ error: 'Invalid video id.' }) 53 .json({ error: 'Invalid video id.' })
51 .end() 54 .end()
diff --git a/server/middlewares/validators/sort.ts b/server/middlewares/validators/sort.ts
index 227f309ad..d23a95537 100644
--- a/server/middlewares/validators/sort.ts
+++ b/server/middlewares/validators/sort.ts
@@ -11,12 +11,14 @@ const SORTABLE_USERS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.USERS)
11const SORTABLE_VIDEO_ABUSES_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_ABUSES) 11const SORTABLE_VIDEO_ABUSES_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_ABUSES)
12const SORTABLE_VIDEOS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEOS) 12const SORTABLE_VIDEOS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEOS)
13const SORTABLE_BLACKLISTS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.BLACKLISTS) 13const SORTABLE_BLACKLISTS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.BLACKLISTS)
14const SORTABLE_VIDEO_CHANNELS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_CHANNELS)
14 15
15const podsSortValidator = checkSort(SORTABLE_PODS_COLUMNS) 16const podsSortValidator = checkSort(SORTABLE_PODS_COLUMNS)
16const usersSortValidator = checkSort(SORTABLE_USERS_COLUMNS) 17const usersSortValidator = checkSort(SORTABLE_USERS_COLUMNS)
17const videoAbusesSortValidator = checkSort(SORTABLE_VIDEO_ABUSES_COLUMNS) 18const videoAbusesSortValidator = checkSort(SORTABLE_VIDEO_ABUSES_COLUMNS)
18const videosSortValidator = checkSort(SORTABLE_VIDEOS_COLUMNS) 19const videosSortValidator = checkSort(SORTABLE_VIDEOS_COLUMNS)
19const blacklistSortValidator = checkSort(SORTABLE_BLACKLISTS_COLUMNS) 20const blacklistSortValidator = checkSort(SORTABLE_BLACKLISTS_COLUMNS)
21const videoChannelsSortValidator = checkSort(SORTABLE_VIDEO_CHANNELS_COLUMNS)
20 22
21// --------------------------------------------------------------------------- 23// ---------------------------------------------------------------------------
22 24
@@ -24,6 +26,7 @@ export {
24 podsSortValidator, 26 podsSortValidator,
25 usersSortValidator, 27 usersSortValidator,
26 videoAbusesSortValidator, 28 videoAbusesSortValidator,
29 videoChannelsSortValidator,
27 videosSortValidator, 30 videosSortValidator,
28 blacklistSortValidator 31 blacklistSortValidator
29} 32}
diff --git a/server/middlewares/validators/users.ts b/server/middlewares/validators/users.ts
index ab9d0938c..1a33cfd8c 100644
--- a/server/middlewares/validators/users.ts
+++ b/server/middlewares/validators/users.ts
@@ -13,7 +13,7 @@ import {
13 isUserPasswordValid, 13 isUserPasswordValid,
14 isUserVideoQuotaValid, 14 isUserVideoQuotaValid,
15 isUserDisplayNSFWValid, 15 isUserDisplayNSFWValid,
16 isVideoIdOrUUIDValid 16 isIdOrUUIDValid
17} from '../../helpers' 17} from '../../helpers'
18import { UserInstance, VideoInstance } from '../../models' 18import { UserInstance, VideoInstance } from '../../models'
19 19
@@ -109,7 +109,7 @@ const usersGetValidator = [
109] 109]
110 110
111const usersVideoRatingValidator = [ 111const usersVideoRatingValidator = [
112 param('videoId').custom(isVideoIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid video id'), 112 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid video id'),
113 113
114 (req: express.Request, res: express.Response, next: express.NextFunction) => { 114 (req: express.Request, res: express.Response, next: express.NextFunction) => {
115 logger.debug('Checking usersVideoRating parameters', { parameters: req.params }) 115 logger.debug('Checking usersVideoRating parameters', { parameters: req.params })
diff --git a/server/middlewares/validators/video-blacklist.ts b/server/middlewares/validators/video-blacklist.ts
index 30c6d4bd9..3c8c31519 100644
--- a/server/middlewares/validators/video-blacklist.ts
+++ b/server/middlewares/validators/video-blacklist.ts
@@ -3,10 +3,10 @@ import * as express from 'express'
3 3
4import { database as db } from '../../initializers/database' 4import { database as db } from '../../initializers/database'
5import { checkErrors } from './utils' 5import { checkErrors } from './utils'
6import { logger, isVideoIdOrUUIDValid, checkVideoExists } from '../../helpers' 6import { logger, isIdOrUUIDValid, checkVideoExists } from '../../helpers'
7 7
8const videosBlacklistRemoveValidator = [ 8const videosBlacklistRemoveValidator = [
9 param('videoId').custom(isVideoIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), 9 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
10 10
11 (req: express.Request, res: express.Response, next: express.NextFunction) => { 11 (req: express.Request, res: express.Response, next: express.NextFunction) => {
12 logger.debug('Checking blacklistRemove parameters.', { parameters: req.params }) 12 logger.debug('Checking blacklistRemove parameters.', { parameters: req.params })
@@ -20,7 +20,7 @@ const videosBlacklistRemoveValidator = [
20] 20]
21 21
22const videosBlacklistAddValidator = [ 22const videosBlacklistAddValidator = [
23 param('videoId').custom(isVideoIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), 23 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
24 24
25 (req: express.Request, res: express.Response, next: express.NextFunction) => { 25 (req: express.Request, res: express.Response, next: express.NextFunction) => {
26 logger.debug('Checking videosBlacklist parameters', { parameters: req.params }) 26 logger.debug('Checking videosBlacklist parameters', { parameters: req.params })
diff --git a/server/middlewares/validators/video-channels.ts b/server/middlewares/validators/video-channels.ts
new file mode 100644
index 000000000..979fbd34a
--- /dev/null
+++ b/server/middlewares/validators/video-channels.ts
@@ -0,0 +1,142 @@
1import { body, param } from 'express-validator/check'
2import * as express from 'express'
3
4import { checkErrors } from './utils'
5import { database as db } from '../../initializers'
6import {
7 logger,
8 isIdOrUUIDValid,
9 isVideoChannelDescriptionValid,
10 isVideoChannelNameValid,
11 checkVideoChannelExists,
12 checkVideoAuthorExists
13} from '../../helpers'
14
15const listVideoAuthorChannelsValidator = [
16 param('authorId').custom(isIdOrUUIDValid).withMessage('Should have a valid author id'),
17
18 (req: express.Request, res: express.Response, next: express.NextFunction) => {
19 logger.debug('Checking listVideoAuthorChannelsValidator parameters', { parameters: req.body })
20
21 checkErrors(req, res, () => {
22 checkVideoAuthorExists(req.params.authorId, res, next)
23 })
24 }
25]
26
27const videoChannelsAddValidator = [
28 body('name').custom(isVideoChannelNameValid).withMessage('Should have a valid name'),
29 body('description').custom(isVideoChannelDescriptionValid).withMessage('Should have a valid description'),
30
31 (req: express.Request, res: express.Response, next: express.NextFunction) => {
32 logger.debug('Checking videoChannelsAdd parameters', { parameters: req.body })
33
34 checkErrors(req, res, next)
35 }
36]
37
38const videoChannelsUpdateValidator = [
39 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
40 body('name').optional().custom(isVideoChannelNameValid).withMessage('Should have a valid name'),
41 body('description').optional().custom(isVideoChannelDescriptionValid).withMessage('Should have a valid description'),
42
43 (req: express.Request, res: express.Response, next: express.NextFunction) => {
44 logger.debug('Checking videoChannelsUpdate parameters', { parameters: req.body })
45
46 checkErrors(req, res, () => {
47 checkVideoChannelExists(req.params.id, res, () => {
48 // We need to make additional checks
49 if (res.locals.videoChannel.isOwned() === false) {
50 return res.status(403)
51 .json({ error: 'Cannot update video channel of another pod' })
52 .end()
53 }
54
55 if (res.locals.videoChannel.Author.userId !== res.locals.oauth.token.User.id) {
56 return res.status(403)
57 .json({ error: 'Cannot update video channel of another user' })
58 .end()
59 }
60
61 next()
62 })
63 })
64 }
65]
66
67const videoChannelsRemoveValidator = [
68 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
69
70 (req: express.Request, res: express.Response, next: express.NextFunction) => {
71 logger.debug('Checking videoChannelsRemove parameters', { parameters: req.params })
72
73 checkErrors(req, res, () => {
74 checkVideoChannelExists(req.params.id, res, () => {
75 // Check if the user who did the request is able to delete the video
76 checkUserCanDeleteVideoChannel(res, () => {
77 checkVideoChannelIsNotTheLastOne(res, next)
78 })
79 })
80 })
81 }
82]
83
84const videoChannelGetValidator = [
85 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
86
87 (req: express.Request, res: express.Response, next: express.NextFunction) => {
88 logger.debug('Checking videoChannelsGet parameters', { parameters: req.params })
89
90 checkErrors(req, res, () => {
91 checkVideoChannelExists(req.params.id, res, next)
92 })
93 }
94]
95
96// ---------------------------------------------------------------------------
97
98export {
99 listVideoAuthorChannelsValidator,
100 videoChannelsAddValidator,
101 videoChannelsUpdateValidator,
102 videoChannelsRemoveValidator,
103 videoChannelGetValidator
104}
105
106// ---------------------------------------------------------------------------
107
108function checkUserCanDeleteVideoChannel (res: express.Response, callback: () => void) {
109 const user = res.locals.oauth.token.User
110
111 // Retrieve the user who did the request
112 if (res.locals.videoChannel.isOwned() === false) {
113 return res.status(403)
114 .json({ error: 'Cannot remove video channel of another pod.' })
115 .end()
116 }
117
118 // Check if the user can delete the video channel
119 // The user can delete it if s/he is an admin
120 // Or if s/he is the video channel's author
121 if (user.isAdmin() === false && res.locals.videoChannel.Author.userId !== user.id) {
122 return res.status(403)
123 .json({ error: 'Cannot remove video channel of another user' })
124 .end()
125 }
126
127 // If we reach this comment, we can delete the video
128 callback()
129}
130
131function checkVideoChannelIsNotTheLastOne (res: express.Response, callback: () => void) {
132 db.VideoChannel.countByAuthor(res.locals.oauth.token.User.Author.id)
133 .then(count => {
134 if (count <= 1) {
135 return res.status(409)
136 .json({ error: 'Cannot remove the last channel of this user' })
137 .end()
138 }
139
140 callback()
141 })
142}
diff --git a/server/middlewares/validators/videos.ts b/server/middlewares/validators/videos.ts
index 3f881e1b5..8a9b383b8 100644
--- a/server/middlewares/validators/videos.ts
+++ b/server/middlewares/validators/videos.ts
@@ -15,11 +15,12 @@ import {
15 isVideoLanguageValid, 15 isVideoLanguageValid,
16 isVideoTagsValid, 16 isVideoTagsValid,
17 isVideoNSFWValid, 17 isVideoNSFWValid,
18 isVideoIdOrUUIDValid, 18 isIdOrUUIDValid,
19 isVideoAbuseReasonValid, 19 isVideoAbuseReasonValid,
20 isVideoRatingTypeValid, 20 isVideoRatingTypeValid,
21 getDurationFromVideoFile, 21 getDurationFromVideoFile,
22 checkVideoExists 22 checkVideoExists,
23 isIdValid
23} from '../../helpers' 24} from '../../helpers'
24 25
25const videosAddValidator = [ 26const videosAddValidator = [
@@ -33,6 +34,7 @@ const videosAddValidator = [
33 body('language').optional().custom(isVideoLanguageValid).withMessage('Should have a valid language'), 34 body('language').optional().custom(isVideoLanguageValid).withMessage('Should have a valid language'),
34 body('nsfw').custom(isVideoNSFWValid).withMessage('Should have a valid NSFW attribute'), 35 body('nsfw').custom(isVideoNSFWValid).withMessage('Should have a valid NSFW attribute'),
35 body('description').custom(isVideoDescriptionValid).withMessage('Should have a valid description'), 36 body('description').custom(isVideoDescriptionValid).withMessage('Should have a valid description'),
37 body('channelId').custom(isIdValid).withMessage('Should have correct video channel id'),
36 body('tags').optional().custom(isVideoTagsValid).withMessage('Should have correct tags'), 38 body('tags').optional().custom(isVideoTagsValid).withMessage('Should have correct tags'),
37 39
38 (req: express.Request, res: express.Response, next: express.NextFunction) => { 40 (req: express.Request, res: express.Response, next: express.NextFunction) => {
@@ -42,7 +44,20 @@ const videosAddValidator = [
42 const videoFile: Express.Multer.File = req.files['videofile'][0] 44 const videoFile: Express.Multer.File = req.files['videofile'][0]
43 const user = res.locals.oauth.token.User 45 const user = res.locals.oauth.token.User
44 46
45 user.isAbleToUploadVideo(videoFile) 47 return db.VideoChannel.loadByIdAndAuthor(req.body.channelId, user.Author.id)
48 .then(videoChannel => {
49 if (!videoChannel) {
50 res.status(400)
51 .json({ error: 'Unknown video video channel for this author.' })
52 .end()
53
54 return undefined
55 }
56
57 res.locals.videoChannel = videoChannel
58
59 return user.isAbleToUploadVideo(videoFile)
60 })
46 .then(isAble => { 61 .then(isAble => {
47 if (isAble === false) { 62 if (isAble === false) {
48 res.status(403) 63 res.status(403)
@@ -88,7 +103,7 @@ const videosAddValidator = [
88] 103]
89 104
90const videosUpdateValidator = [ 105const videosUpdateValidator = [
91 param('id').custom(isVideoIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), 106 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
92 body('name').optional().custom(isVideoNameValid).withMessage('Should have a valid name'), 107 body('name').optional().custom(isVideoNameValid).withMessage('Should have a valid name'),
93 body('category').optional().custom(isVideoCategoryValid).withMessage('Should have a valid category'), 108 body('category').optional().custom(isVideoCategoryValid).withMessage('Should have a valid category'),
94 body('licence').optional().custom(isVideoLicenceValid).withMessage('Should have a valid licence'), 109 body('licence').optional().custom(isVideoLicenceValid).withMessage('Should have a valid licence'),
@@ -109,7 +124,7 @@ const videosUpdateValidator = [
109 .end() 124 .end()
110 } 125 }
111 126
112 if (res.locals.video.Author.userId !== res.locals.oauth.token.User.id) { 127 if (res.locals.video.VideoChannel.Author.userId !== res.locals.oauth.token.User.id) {
113 return res.status(403) 128 return res.status(403)
114 .json({ error: 'Cannot update video of another user' }) 129 .json({ error: 'Cannot update video of another user' })
115 .end() 130 .end()
@@ -122,7 +137,7 @@ const videosUpdateValidator = [
122] 137]
123 138
124const videosGetValidator = [ 139const videosGetValidator = [
125 param('id').custom(isVideoIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), 140 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
126 141
127 (req: express.Request, res: express.Response, next: express.NextFunction) => { 142 (req: express.Request, res: express.Response, next: express.NextFunction) => {
128 logger.debug('Checking videosGet parameters', { parameters: req.params }) 143 logger.debug('Checking videosGet parameters', { parameters: req.params })
@@ -134,7 +149,7 @@ const videosGetValidator = [
134] 149]
135 150
136const videosRemoveValidator = [ 151const videosRemoveValidator = [
137 param('id').custom(isVideoIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), 152 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
138 153
139 (req: express.Request, res: express.Response, next: express.NextFunction) => { 154 (req: express.Request, res: express.Response, next: express.NextFunction) => {
140 logger.debug('Checking videosRemove parameters', { parameters: req.params }) 155 logger.debug('Checking videosRemove parameters', { parameters: req.params })
@@ -162,7 +177,7 @@ const videosSearchValidator = [
162] 177]
163 178
164const videoAbuseReportValidator = [ 179const videoAbuseReportValidator = [
165 param('id').custom(isVideoIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), 180 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
166 body('reason').custom(isVideoAbuseReasonValid).withMessage('Should have a valid reason'), 181 body('reason').custom(isVideoAbuseReasonValid).withMessage('Should have a valid reason'),
167 182
168 (req: express.Request, res: express.Response, next: express.NextFunction) => { 183 (req: express.Request, res: express.Response, next: express.NextFunction) => {
@@ -175,7 +190,7 @@ const videoAbuseReportValidator = [
175] 190]
176 191
177const videoRateValidator = [ 192const videoRateValidator = [
178 param('id').custom(isVideoIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), 193 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
179 body('rating').custom(isVideoRatingTypeValid).withMessage('Should have a valid rate type'), 194 body('rating').custom(isVideoRatingTypeValid).withMessage('Should have a valid rate type'),
180 195
181 (req: express.Request, res: express.Response, next: express.NextFunction) => { 196 (req: express.Request, res: express.Response, next: express.NextFunction) => {
diff --git a/server/models/oauth/oauth-token.ts b/server/models/oauth/oauth-token.ts
index e3de9468e..dc8bcd872 100644
--- a/server/models/oauth/oauth-token.ts
+++ b/server/models/oauth/oauth-token.ts
@@ -126,7 +126,17 @@ getByTokenAndPopulateUser = function (bearerToken: string) {
126 where: { 126 where: {
127 accessToken: bearerToken 127 accessToken: bearerToken
128 }, 128 },
129 include: [ OAuthToken['sequelize'].models.User ] 129 include: [
130 {
131 model: OAuthToken['sequelize'].models.User,
132 include: [
133 {
134 model: OAuthToken['sequelize'].models.Author,
135 required: true
136 }
137 ]
138 }
139 ]
130 } 140 }
131 141
132 return OAuthToken.findOne(query).then(token => { 142 return OAuthToken.findOne(query).then(token => {
@@ -141,7 +151,17 @@ getByRefreshTokenAndPopulateUser = function (refreshToken: string) {
141 where: { 151 where: {
142 refreshToken: refreshToken 152 refreshToken: refreshToken
143 }, 153 },
144 include: [ OAuthToken['sequelize'].models.User ] 154 include: [
155 {
156 model: OAuthToken['sequelize'].models.User,
157 include: [
158 {
159 model: OAuthToken['sequelize'].models.Author,
160 required: true
161 }
162 ]
163 }
164 ]
145 } 165 }
146 166
147 return OAuthToken.findOne(query).then(token => { 167 return OAuthToken.findOne(query).then(token => {
diff --git a/server/models/request/request-video-event.ts b/server/models/request/request-video-event.ts
index 4862a5745..34d5c7162 100644
--- a/server/models/request/request-video-event.ts
+++ b/server/models/request/request-video-event.ts
@@ -85,7 +85,8 @@ listWithLimitAndRandom = function (limitPods: number, limitRequestsPerPod: numbe
85 const Pod = db.Pod 85 const Pod = db.Pod
86 86
87 // We make a join between videos and authors to find the podId of our video event requests 87 // We make a join between videos and authors to find the podId of our video event requests
88 const podJoins = 'INNER JOIN "Videos" ON "Videos"."authorId" = "Authors"."id" ' + 88 const podJoins = 'INNER JOIN "VideoChannels" ON "VideoChannels"."authorId" = "Authors"."id" ' +
89 'INNER JOIN "Videos" ON "Videos"."channelId" = "VideoChannels"."id" ' +
89 'INNER JOIN "RequestVideoEvents" ON "RequestVideoEvents"."videoId" = "Videos"."id"' 90 'INNER JOIN "RequestVideoEvents" ON "RequestVideoEvents"."videoId" = "Videos"."id"'
90 91
91 return Pod.listRandomPodIdsWithRequest(limitPods, 'Authors', podJoins).then(podIds => { 92 return Pod.listRandomPodIdsWithRequest(limitPods, 'Authors', podJoins).then(podIds => {
@@ -161,7 +162,7 @@ function groupAndTruncateRequests (events: RequestVideoEventInstance[], limitReq
161 const eventsGrouped: RequestsVideoEventGrouped = {} 162 const eventsGrouped: RequestsVideoEventGrouped = {}
162 163
163 events.forEach(event => { 164 events.forEach(event => {
164 const pod = event.Video.Author.Pod 165 const pod = event.Video.VideoChannel.Author.Pod
165 166
166 if (!eventsGrouped[pod.id]) eventsGrouped[pod.id] = [] 167 if (!eventsGrouped[pod.id]) eventsGrouped[pod.id] = []
167 168
diff --git a/server/models/user/user-interface.ts b/server/models/user/user-interface.ts
index 8974a9a97..1b5233eaf 100644
--- a/server/models/user/user-interface.ts
+++ b/server/models/user/user-interface.ts
@@ -5,6 +5,7 @@ import * as Promise from 'bluebird'
5import { User as FormattedUser } from '../../../shared/models/users/user.model' 5import { User as FormattedUser } from '../../../shared/models/users/user.model'
6import { UserRole } from '../../../shared/models/users/user-role.type' 6import { UserRole } from '../../../shared/models/users/user-role.type'
7import { ResultList } from '../../../shared/models/result-list.model' 7import { ResultList } from '../../../shared/models/result-list.model'
8import { AuthorInstance } from '../video/author-interface'
8 9
9export namespace UserMethods { 10export namespace UserMethods {
10 export type IsPasswordMatch = (this: UserInstance, password: string) => Promise<boolean> 11 export type IsPasswordMatch = (this: UserInstance, password: string) => Promise<boolean>
@@ -17,13 +18,12 @@ export namespace UserMethods {
17 18
18 export type GetByUsername = (username: string) => Promise<UserInstance> 19 export type GetByUsername = (username: string) => Promise<UserInstance>
19 20
20 export type List = () => Promise<UserInstance[]>
21
22 export type ListForApi = (start: number, count: number, sort: string) => Promise< ResultList<UserInstance> > 21 export type ListForApi = (start: number, count: number, sort: string) => Promise< ResultList<UserInstance> >
23 22
24 export type LoadById = (id: number) => Promise<UserInstance> 23 export type LoadById = (id: number) => Promise<UserInstance>
25 24
26 export type LoadByUsername = (username: string) => Promise<UserInstance> 25 export type LoadByUsername = (username: string) => Promise<UserInstance>
26 export type LoadByUsernameAndPopulateChannels = (username: string) => Promise<UserInstance>
27 27
28 export type LoadByUsernameOrEmail = (username: string, email: string) => Promise<UserInstance> 28 export type LoadByUsernameOrEmail = (username: string, email: string) => Promise<UserInstance>
29} 29}
@@ -36,10 +36,10 @@ export interface UserClass {
36 36
37 countTotal: UserMethods.CountTotal, 37 countTotal: UserMethods.CountTotal,
38 getByUsername: UserMethods.GetByUsername, 38 getByUsername: UserMethods.GetByUsername,
39 list: UserMethods.List,
40 listForApi: UserMethods.ListForApi, 39 listForApi: UserMethods.ListForApi,
41 loadById: UserMethods.LoadById, 40 loadById: UserMethods.LoadById,
42 loadByUsername: UserMethods.LoadByUsername, 41 loadByUsername: UserMethods.LoadByUsername,
42 loadByUsernameAndPopulateChannels: UserMethods.LoadByUsernameAndPopulateChannels,
43 loadByUsernameOrEmail: UserMethods.LoadByUsernameOrEmail 43 loadByUsernameOrEmail: UserMethods.LoadByUsernameOrEmail
44} 44}
45 45
@@ -51,6 +51,8 @@ export interface UserAttributes {
51 displayNSFW?: boolean 51 displayNSFW?: boolean
52 role: UserRole 52 role: UserRole
53 videoQuota: number 53 videoQuota: number
54
55 Author?: AuthorInstance
54} 56}
55 57
56export interface UserInstance extends UserClass, UserAttributes, Sequelize.Instance<UserAttributes> { 58export interface UserInstance extends UserClass, UserAttributes, Sequelize.Instance<UserAttributes> {
diff --git a/server/models/user/user.ts b/server/models/user/user.ts
index 0dc52d3cf..f8598c40f 100644
--- a/server/models/user/user.ts
+++ b/server/models/user/user.ts
@@ -27,10 +27,10 @@ let toFormattedJSON: UserMethods.ToFormattedJSON
27let isAdmin: UserMethods.IsAdmin 27let isAdmin: UserMethods.IsAdmin
28let countTotal: UserMethods.CountTotal 28let countTotal: UserMethods.CountTotal
29let getByUsername: UserMethods.GetByUsername 29let getByUsername: UserMethods.GetByUsername
30let list: UserMethods.List
31let listForApi: UserMethods.ListForApi 30let listForApi: UserMethods.ListForApi
32let loadById: UserMethods.LoadById 31let loadById: UserMethods.LoadById
33let loadByUsername: UserMethods.LoadByUsername 32let loadByUsername: UserMethods.LoadByUsername
33let loadByUsernameAndPopulateChannels: UserMethods.LoadByUsernameAndPopulateChannels
34let loadByUsernameOrEmail: UserMethods.LoadByUsernameOrEmail 34let loadByUsernameOrEmail: UserMethods.LoadByUsernameOrEmail
35let isAbleToUploadVideo: UserMethods.IsAbleToUploadVideo 35let isAbleToUploadVideo: UserMethods.IsAbleToUploadVideo
36 36
@@ -113,10 +113,10 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
113 113
114 countTotal, 114 countTotal,
115 getByUsername, 115 getByUsername,
116 list,
117 listForApi, 116 listForApi,
118 loadById, 117 loadById,
119 loadByUsername, 118 loadByUsername,
119 loadByUsernameAndPopulateChannels,
120 loadByUsernameOrEmail 120 loadByUsernameOrEmail
121 ] 121 ]
122 const instanceMethods = [ 122 const instanceMethods = [
@@ -144,15 +144,34 @@ isPasswordMatch = function (this: UserInstance, password: string) {
144} 144}
145 145
146toFormattedJSON = function (this: UserInstance) { 146toFormattedJSON = function (this: UserInstance) {
147 return { 147 const json = {
148 id: this.id, 148 id: this.id,
149 username: this.username, 149 username: this.username,
150 email: this.email, 150 email: this.email,
151 displayNSFW: this.displayNSFW, 151 displayNSFW: this.displayNSFW,
152 role: this.role, 152 role: this.role,
153 videoQuota: this.videoQuota, 153 videoQuota: this.videoQuota,
154 createdAt: this.createdAt 154 createdAt: this.createdAt,
155 author: {
156 id: this.Author.id,
157 uuid: this.Author.uuid
158 }
155 } 159 }
160
161 if (Array.isArray(this.Author.VideoChannels) === true) {
162 const videoChannels = this.Author.VideoChannels
163 .map(c => c.toFormattedJSON())
164 .sort((v1, v2) => {
165 if (v1.createdAt < v2.createdAt) return -1
166 if (v1.createdAt === v2.createdAt) return 0
167
168 return 1
169 })
170
171 json['videoChannels'] = videoChannels
172 }
173
174 return json
156} 175}
157 176
158isAdmin = function (this: UserInstance) { 177isAdmin = function (this: UserInstance) {
@@ -189,21 +208,19 @@ getByUsername = function (username: string) {
189 const query = { 208 const query = {
190 where: { 209 where: {
191 username: username 210 username: username
192 } 211 },
212 include: [ { model: User['sequelize'].models.Author, required: true } ]
193 } 213 }
194 214
195 return User.findOne(query) 215 return User.findOne(query)
196} 216}
197 217
198list = function () {
199 return User.findAll()
200}
201
202listForApi = function (start: number, count: number, sort: string) { 218listForApi = function (start: number, count: number, sort: string) {
203 const query = { 219 const query = {
204 offset: start, 220 offset: start,
205 limit: count, 221 limit: count,
206 order: [ getSort(sort) ] 222 order: [ getSort(sort) ],
223 include: [ { model: User['sequelize'].models.Author, required: true } ]
207 } 224 }
208 225
209 return User.findAndCountAll(query).then(({ rows, count }) => { 226 return User.findAndCountAll(query).then(({ rows, count }) => {
@@ -215,14 +232,36 @@ listForApi = function (start: number, count: number, sort: string) {
215} 232}
216 233
217loadById = function (id: number) { 234loadById = function (id: number) {
218 return User.findById(id) 235 const options = {
236 include: [ { model: User['sequelize'].models.Author, required: true } ]
237 }
238
239 return User.findById(id, options)
219} 240}
220 241
221loadByUsername = function (username: string) { 242loadByUsername = function (username: string) {
222 const query = { 243 const query = {
223 where: { 244 where: {
224 username 245 username
225 } 246 },
247 include: [ { model: User['sequelize'].models.Author, required: true } ]
248 }
249
250 return User.findOne(query)
251}
252
253loadByUsernameAndPopulateChannels = function (username: string) {
254 const query = {
255 where: {
256 username
257 },
258 include: [
259 {
260 model: User['sequelize'].models.Author,
261 required: true,
262 include: [ User['sequelize'].models.VideoChannel ]
263 }
264 ]
226 } 265 }
227 266
228 return User.findOne(query) 267 return User.findOne(query)
@@ -230,6 +269,7 @@ loadByUsername = function (username: string) {
230 269
231loadByUsernameOrEmail = function (username: string, email: string) { 270loadByUsernameOrEmail = function (username: string, email: string) {
232 const query = { 271 const query = {
272 include: [ { model: User['sequelize'].models.Author, required: true } ],
233 where: { 273 where: {
234 $or: [ { username }, { email } ] 274 $or: [ { username }, { email } ]
235 } 275 }
@@ -242,11 +282,12 @@ loadByUsernameOrEmail = function (username: string, email: string) {
242// --------------------------------------------------------------------------- 282// ---------------------------------------------------------------------------
243 283
244function getOriginalVideoFileTotalFromUser (user: UserInstance) { 284function getOriginalVideoFileTotalFromUser (user: UserInstance) {
245 // Don't use sequelize because we need to use a subquery 285 // Don't use sequelize because we need to use a sub query
246 const query = 'SELECT SUM("size") AS "total" FROM ' + 286 const query = 'SELECT SUM("size") AS "total" FROM ' +
247 '(SELECT MAX("VideoFiles"."size") AS "size" FROM "VideoFiles" ' + 287 '(SELECT MAX("VideoFiles"."size") AS "size" FROM "VideoFiles" ' +
248 'INNER JOIN "Videos" ON "VideoFiles"."videoId" = "Videos"."id" ' + 288 'INNER JOIN "Videos" ON "VideoFiles"."videoId" = "Videos"."id" ' +
249 'INNER JOIN "Authors" ON "Videos"."authorId" = "Authors"."id" ' + 289 'INNER JOIN "VideoChannels" ON "VideoChannels"."id" = "Videos"."channelId" ' +
290 'INNER JOIN "Authors" ON "VideoChannels"."authorId" = "Authors"."id" ' +
250 'INNER JOIN "Users" ON "Authors"."userId" = "Users"."id" ' + 291 'INNER JOIN "Users" ON "Authors"."userId" = "Users"."id" ' +
251 'WHERE "Users"."id" = $userId GROUP BY "Videos"."id") t' 292 'WHERE "Users"."id" = $userId GROUP BY "Videos"."id") t'
252 293
diff --git a/server/models/video/author-interface.ts b/server/models/video/author-interface.ts
index 52a00a1d3..fc69ff3c2 100644
--- a/server/models/video/author-interface.ts
+++ b/server/models/video/author-interface.ts
@@ -2,31 +2,44 @@ import * as Sequelize from 'sequelize'
2import * as Promise from 'bluebird' 2import * as Promise from 'bluebird'
3 3
4import { PodInstance } from '../pod/pod-interface' 4import { PodInstance } from '../pod/pod-interface'
5import { RemoteVideoAuthorCreateData } from '../../../shared/models/pods/remote-video/remote-video-author-create-request.model'
6import { VideoChannelInstance } from './video-channel-interface'
5 7
6export namespace AuthorMethods { 8export namespace AuthorMethods {
7 export type FindOrCreateAuthor = ( 9 export type Load = (id: number) => Promise<AuthorInstance>
8 name: string, 10 export type LoadByUUID = (uuid: string) => Promise<AuthorInstance>
9 podId: number, 11 export type LoadAuthorByPodAndUUID = (uuid: string, podId: number, transaction: Sequelize.Transaction) => Promise<AuthorInstance>
10 userId: number, 12 export type ListOwned = () => Promise<AuthorInstance[]>
11 transaction: Sequelize.Transaction 13
12 ) => Promise<AuthorInstance> 14 export type ToAddRemoteJSON = (this: AuthorInstance) => RemoteVideoAuthorCreateData
15 export type IsOwned = (this: AuthorInstance) => boolean
13} 16}
14 17
15export interface AuthorClass { 18export interface AuthorClass {
16 findOrCreateAuthor: AuthorMethods.FindOrCreateAuthor 19 loadAuthorByPodAndUUID: AuthorMethods.LoadAuthorByPodAndUUID
20 load: AuthorMethods.Load
21 loadByUUID: AuthorMethods.LoadByUUID
22 listOwned: AuthorMethods.ListOwned
17} 23}
18 24
19export interface AuthorAttributes { 25export interface AuthorAttributes {
20 name: string 26 name: string
27 uuid?: string
28
29 podId?: number
30 userId?: number
21} 31}
22 32
23export interface AuthorInstance extends AuthorClass, AuthorAttributes, Sequelize.Instance<AuthorAttributes> { 33export interface AuthorInstance extends AuthorClass, AuthorAttributes, Sequelize.Instance<AuthorAttributes> {
34 isOwned: AuthorMethods.IsOwned
35 toAddRemoteJSON: AuthorMethods.ToAddRemoteJSON
36
24 id: number 37 id: number
25 createdAt: Date 38 createdAt: Date
26 updatedAt: Date 39 updatedAt: Date
27 40
28 podId: number
29 Pod: PodInstance 41 Pod: PodInstance
42 VideoChannels: VideoChannelInstance[]
30} 43}
31 44
32export interface AuthorModel extends AuthorClass, Sequelize.Model<AuthorInstance, AuthorAttributes> {} 45export interface AuthorModel extends AuthorClass, Sequelize.Model<AuthorInstance, AuthorAttributes> {}
diff --git a/server/models/video/author.ts b/server/models/video/author.ts
index fd0f44f6b..6f27ea7bd 100644
--- a/server/models/video/author.ts
+++ b/server/models/video/author.ts
@@ -1,6 +1,7 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2 2
3import { isUserUsernameValid } from '../../helpers' 3import { isUserUsernameValid } from '../../helpers'
4import { removeVideoAuthorToFriends } from '../../lib'
4 5
5import { addMethodsToModel } from '../utils' 6import { addMethodsToModel } from '../utils'
6import { 7import {
@@ -11,11 +12,24 @@ import {
11} from './author-interface' 12} from './author-interface'
12 13
13let Author: Sequelize.Model<AuthorInstance, AuthorAttributes> 14let Author: Sequelize.Model<AuthorInstance, AuthorAttributes>
14let findOrCreateAuthor: AuthorMethods.FindOrCreateAuthor 15let loadAuthorByPodAndUUID: AuthorMethods.LoadAuthorByPodAndUUID
16let load: AuthorMethods.Load
17let loadByUUID: AuthorMethods.LoadByUUID
18let listOwned: AuthorMethods.ListOwned
19let isOwned: AuthorMethods.IsOwned
20let toAddRemoteJSON: AuthorMethods.ToAddRemoteJSON
15 21
16export default function defineAuthor (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { 22export default function defineAuthor (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) {
17 Author = sequelize.define<AuthorInstance, AuthorAttributes>('Author', 23 Author = sequelize.define<AuthorInstance, AuthorAttributes>('Author',
18 { 24 {
25 uuid: {
26 type: DataTypes.UUID,
27 defaultValue: DataTypes.UUIDV4,
28 allowNull: false,
29 validate: {
30 isUUID: 4
31 }
32 },
19 name: { 33 name: {
20 type: DataTypes.STRING, 34 type: DataTypes.STRING,
21 allowNull: false, 35 allowNull: false,
@@ -43,12 +57,23 @@ export default function defineAuthor (sequelize: Sequelize.Sequelize, DataTypes:
43 fields: [ 'name', 'podId' ], 57 fields: [ 'name', 'podId' ],
44 unique: true 58 unique: true
45 } 59 }
46 ] 60 ],
61 hooks: { afterDestroy }
47 } 62 }
48 ) 63 )
49 64
50 const classMethods = [ associate, findOrCreateAuthor ] 65 const classMethods = [
51 addMethodsToModel(Author, classMethods) 66 associate,
67 loadAuthorByPodAndUUID,
68 load,
69 loadByUUID,
70 listOwned
71 ]
72 const instanceMethods = [
73 isOwned,
74 toAddRemoteJSON
75 ]
76 addMethodsToModel(Author, classMethods, instanceMethods)
52 77
53 return Author 78 return Author
54} 79}
@@ -72,27 +97,75 @@ function associate (models) {
72 onDelete: 'cascade' 97 onDelete: 'cascade'
73 }) 98 })
74 99
75 Author.hasMany(models.Video, { 100 Author.hasMany(models.VideoChannel, {
76 foreignKey: { 101 foreignKey: {
77 name: 'authorId', 102 name: 'authorId',
78 allowNull: false 103 allowNull: false
79 }, 104 },
80 onDelete: 'cascade' 105 onDelete: 'cascade',
106 hooks: true
81 }) 107 })
82} 108}
83 109
84findOrCreateAuthor = function (name: string, podId: number, userId: number, transaction: Sequelize.Transaction) { 110function afterDestroy (author: AuthorInstance, options: { transaction: Sequelize.Transaction }) {
85 const author = { 111 if (author.isOwned()) {
86 name, 112 const removeVideoAuthorToFriendsParams = {
87 podId, 113 uuid: author.uuid
88 userId 114 }
115
116 return removeVideoAuthorToFriends(removeVideoAuthorToFriendsParams, options.transaction)
117 }
118
119 return undefined
120}
121
122toAddRemoteJSON = function (this: AuthorInstance) {
123 const json = {
124 uuid: this.uuid,
125 name: this.name
126 }
127
128 return json
129}
130
131isOwned = function (this: AuthorInstance) {
132 return this.podId === null
133}
134
135// ------------------------------ STATICS ------------------------------
136
137listOwned = function () {
138 const query: Sequelize.FindOptions<AuthorAttributes> = {
139 where: {
140 podId: null
141 }
142 }
143
144 return Author.findAll(query)
145}
146
147load = function (id: number) {
148 return Author.findById(id)
149}
150
151loadByUUID = function (uuid: string) {
152 const query: Sequelize.FindOptions<AuthorAttributes> = {
153 where: {
154 uuid
155 }
89 } 156 }
90 157
91 const query: Sequelize.FindOrInitializeOptions<AuthorAttributes> = { 158 return Author.findOne(query)
92 where: author, 159}
93 defaults: author, 160
161loadAuthorByPodAndUUID = function (uuid: string, podId: number, transaction: Sequelize.Transaction) {
162 const query: Sequelize.FindOptions<AuthorAttributes> = {
163 where: {
164 podId,
165 uuid
166 },
94 transaction 167 transaction
95 } 168 }
96 169
97 return Author.findOrCreate(query).then(([ authorInstance ]) => authorInstance) 170 return Author.find(query)
98} 171}
diff --git a/server/models/video/index.ts b/server/models/video/index.ts
index 08b360376..dba6a5590 100644
--- a/server/models/video/index.ts
+++ b/server/models/video/index.ts
@@ -2,6 +2,7 @@ export * from './author-interface'
2export * from './tag-interface' 2export * from './tag-interface'
3export * from './video-abuse-interface' 3export * from './video-abuse-interface'
4export * from './video-blacklist-interface' 4export * from './video-blacklist-interface'
5export * from './video-channel-interface'
5export * from './video-tag-interface' 6export * from './video-tag-interface'
6export * from './video-file-interface' 7export * from './video-file-interface'
7export * from './video-interface' 8export * from './video-interface'
diff --git a/server/models/video/video-channel-interface.ts b/server/models/video/video-channel-interface.ts
new file mode 100644
index 000000000..b8d3e0f42
--- /dev/null
+++ b/server/models/video/video-channel-interface.ts
@@ -0,0 +1,64 @@
1import * as Sequelize from 'sequelize'
2import * as Promise from 'bluebird'
3
4import { ResultList, RemoteVideoChannelCreateData, RemoteVideoChannelUpdateData } from '../../../shared'
5
6// Don't use barrel, import just what we need
7import { VideoChannel as FormattedVideoChannel } from '../../../shared/models/videos/video-channel.model'
8import { AuthorInstance } from './author-interface'
9import { VideoInstance } from './video-interface'
10
11export namespace VideoChannelMethods {
12 export type ToFormattedJSON = (this: VideoChannelInstance) => FormattedVideoChannel
13 export type ToAddRemoteJSON = (this: VideoChannelInstance) => RemoteVideoChannelCreateData
14 export type ToUpdateRemoteJSON = (this: VideoChannelInstance) => RemoteVideoChannelUpdateData
15 export type IsOwned = (this: VideoChannelInstance) => boolean
16
17 export type CountByAuthor = (authorId: number) => Promise<number>
18 export type ListOwned = () => Promise<VideoChannelInstance[]>
19 export type ListForApi = (start: number, count: number, sort: string) => Promise< ResultList<VideoChannelInstance> >
20 export type LoadByIdAndAuthor = (id: number, authorId: number) => Promise<VideoChannelInstance>
21 export type ListByAuthor = (authorId: number) => Promise< ResultList<VideoChannelInstance> >
22 export type LoadAndPopulateAuthor = (id: number) => Promise<VideoChannelInstance>
23 export type LoadByUUIDAndPopulateAuthor = (uuid: string) => Promise<VideoChannelInstance>
24 export type LoadByUUID = (uuid: string, t?: Sequelize.Transaction) => Promise<VideoChannelInstance>
25 export type LoadByHostAndUUID = (uuid: string, podHost: string, t?: Sequelize.Transaction) => Promise<VideoChannelInstance>
26 export type LoadAndPopulateAuthorAndVideos = (id: number) => Promise<VideoChannelInstance>
27}
28
29export interface VideoChannelClass {
30 countByAuthor: VideoChannelMethods.CountByAuthor
31 listForApi: VideoChannelMethods.ListForApi
32 listByAuthor: VideoChannelMethods.ListByAuthor
33 listOwned: VideoChannelMethods.ListOwned
34 loadByIdAndAuthor: VideoChannelMethods.LoadByIdAndAuthor
35 loadByUUID: VideoChannelMethods.LoadByUUID
36 loadByHostAndUUID: VideoChannelMethods.LoadByHostAndUUID
37 loadAndPopulateAuthor: VideoChannelMethods.LoadAndPopulateAuthor
38 loadByUUIDAndPopulateAuthor: VideoChannelMethods.LoadByUUIDAndPopulateAuthor
39 loadAndPopulateAuthorAndVideos: VideoChannelMethods.LoadAndPopulateAuthorAndVideos
40}
41
42export interface VideoChannelAttributes {
43 id?: number
44 uuid?: string
45 name: string
46 description: string
47 remote: boolean
48
49 Author?: AuthorInstance
50 Videos?: VideoInstance[]
51}
52
53export interface VideoChannelInstance extends VideoChannelClass, VideoChannelAttributes, Sequelize.Instance<VideoChannelAttributes> {
54 id: number
55 createdAt: Date
56 updatedAt: Date
57
58 isOwned: VideoChannelMethods.IsOwned
59 toFormattedJSON: VideoChannelMethods.ToFormattedJSON
60 toAddRemoteJSON: VideoChannelMethods.ToAddRemoteJSON
61 toUpdateRemoteJSON: VideoChannelMethods.ToUpdateRemoteJSON
62}
63
64export interface VideoChannelModel extends VideoChannelClass, Sequelize.Model<VideoChannelInstance, VideoChannelAttributes> {}
diff --git a/server/models/video/video-channel.ts b/server/models/video/video-channel.ts
new file mode 100644
index 000000000..e469383e9
--- /dev/null
+++ b/server/models/video/video-channel.ts
@@ -0,0 +1,349 @@
1import * as Sequelize from 'sequelize'
2
3import { isVideoChannelNameValid, isVideoChannelDescriptionValid } from '../../helpers'
4import { removeVideoChannelToFriends } from '../../lib'
5
6import { addMethodsToModel, getSort } from '../utils'
7import {
8 VideoChannelInstance,
9 VideoChannelAttributes,
10
11 VideoChannelMethods
12} from './video-channel-interface'
13
14let VideoChannel: Sequelize.Model<VideoChannelInstance, VideoChannelAttributes>
15let toFormattedJSON: VideoChannelMethods.ToFormattedJSON
16let toAddRemoteJSON: VideoChannelMethods.ToAddRemoteJSON
17let toUpdateRemoteJSON: VideoChannelMethods.ToUpdateRemoteJSON
18let isOwned: VideoChannelMethods.IsOwned
19let countByAuthor: VideoChannelMethods.CountByAuthor
20let listOwned: VideoChannelMethods.ListOwned
21let listForApi: VideoChannelMethods.ListForApi
22let listByAuthor: VideoChannelMethods.ListByAuthor
23let loadByIdAndAuthor: VideoChannelMethods.LoadByIdAndAuthor
24let loadByUUID: VideoChannelMethods.LoadByUUID
25let loadAndPopulateAuthor: VideoChannelMethods.LoadAndPopulateAuthor
26let loadByUUIDAndPopulateAuthor: VideoChannelMethods.LoadByUUIDAndPopulateAuthor
27let loadByHostAndUUID: VideoChannelMethods.LoadByHostAndUUID
28let loadAndPopulateAuthorAndVideos: VideoChannelMethods.LoadAndPopulateAuthorAndVideos
29
30export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) {
31 VideoChannel = sequelize.define<VideoChannelInstance, VideoChannelAttributes>('VideoChannel',
32 {
33 uuid: {
34 type: DataTypes.UUID,
35 defaultValue: DataTypes.UUIDV4,
36 allowNull: false,
37 validate: {
38 isUUID: 4
39 }
40 },
41 name: {
42 type: DataTypes.STRING,
43 allowNull: false,
44 validate: {
45 nameValid: value => {
46 const res = isVideoChannelNameValid(value)
47 if (res === false) throw new Error('Video channel name is not valid.')
48 }
49 }
50 },
51 description: {
52 type: DataTypes.STRING,
53 allowNull: true,
54 validate: {
55 descriptionValid: value => {
56 const res = isVideoChannelDescriptionValid(value)
57 if (res === false) throw new Error('Video channel description is not valid.')
58 }
59 }
60 },
61 remote: {
62 type: DataTypes.BOOLEAN,
63 allowNull: false,
64 defaultValue: false
65 }
66 },
67 {
68 indexes: [
69 {
70 fields: [ 'authorId' ]
71 }
72 ],
73 hooks: {
74 afterDestroy
75 }
76 }
77 )
78
79 const classMethods = [
80 associate,
81
82 listForApi,
83 listByAuthor,
84 listOwned,
85 loadByIdAndAuthor,
86 loadAndPopulateAuthor,
87 loadByUUIDAndPopulateAuthor,
88 loadByUUID,
89 loadByHostAndUUID,
90 loadAndPopulateAuthorAndVideos,
91 countByAuthor
92 ]
93 const instanceMethods = [
94 isOwned,
95 toFormattedJSON,
96 toAddRemoteJSON,
97 toUpdateRemoteJSON
98 ]
99 addMethodsToModel(VideoChannel, classMethods, instanceMethods)
100
101 return VideoChannel
102}
103
104// ------------------------------ METHODS ------------------------------
105
106isOwned = function (this: VideoChannelInstance) {
107 return this.remote === false
108}
109
110toFormattedJSON = function (this: VideoChannelInstance) {
111 const json = {
112 id: this.id,
113 uuid: this.uuid,
114 name: this.name,
115 description: this.description,
116 isLocal: this.isOwned(),
117 createdAt: this.createdAt,
118 updatedAt: this.updatedAt
119 }
120
121 if (this.Author !== undefined) {
122 json['owner'] = {
123 name: this.Author.name,
124 uuid: this.Author.uuid
125 }
126 }
127
128 if (Array.isArray(this.Videos)) {
129 json['videos'] = this.Videos.map(v => v.toFormattedJSON())
130 }
131
132 return json
133}
134
135toAddRemoteJSON = function (this: VideoChannelInstance) {
136 const json = {
137 uuid: this.uuid,
138 name: this.name,
139 description: this.description,
140 createdAt: this.createdAt,
141 updatedAt: this.updatedAt,
142 ownerUUID: this.Author.uuid
143 }
144
145 return json
146}
147
148toUpdateRemoteJSON = function (this: VideoChannelInstance) {
149 const json = {
150 uuid: this.uuid,
151 name: this.name,
152 description: this.description,
153 createdAt: this.createdAt,
154 updatedAt: this.updatedAt,
155 ownerUUID: this.Author.uuid
156 }
157
158 return json
159}
160
161// ------------------------------ STATICS ------------------------------
162
163function associate (models) {
164 VideoChannel.belongsTo(models.Author, {
165 foreignKey: {
166 name: 'authorId',
167 allowNull: false
168 },
169 onDelete: 'CASCADE'
170 })
171
172 VideoChannel.hasMany(models.Video, {
173 foreignKey: {
174 name: 'channelId',
175 allowNull: false
176 },
177 onDelete: 'CASCADE'
178 })
179}
180
181function afterDestroy (videoChannel: VideoChannelInstance, options: { transaction: Sequelize.Transaction }) {
182 if (videoChannel.isOwned()) {
183 const removeVideoChannelToFriendsParams = {
184 uuid: videoChannel.uuid
185 }
186
187 return removeVideoChannelToFriends(removeVideoChannelToFriendsParams, options.transaction)
188 }
189
190 return undefined
191}
192
193countByAuthor = function (authorId: number) {
194 const query = {
195 where: {
196 authorId
197 }
198 }
199
200 return VideoChannel.count(query)
201}
202
203listOwned = function () {
204 const query = {
205 where: {
206 remote: false
207 },
208 include: [ VideoChannel['sequelize'].models.Author ]
209 }
210
211 return VideoChannel.findAll(query)
212}
213
214listForApi = function (start: number, count: number, sort: string) {
215 const query = {
216 offset: start,
217 limit: count,
218 order: [ getSort(sort) ],
219 include: [
220 {
221 model: VideoChannel['sequelize'].models.Author,
222 required: true,
223 include: [ { model: VideoChannel['sequelize'].models.Pod, required: false } ]
224 }
225 ]
226 }
227
228 return VideoChannel.findAndCountAll(query).then(({ rows, count }) => {
229 return { total: count, data: rows }
230 })
231}
232
233listByAuthor = function (authorId: number) {
234 const query = {
235 order: [ getSort('createdAt') ],
236 include: [
237 {
238 model: VideoChannel['sequelize'].models.Author,
239 where: {
240 id: authorId
241 },
242 required: true,
243 include: [ { model: VideoChannel['sequelize'].models.Pod, required: false } ]
244 }
245 ]
246 }
247
248 return VideoChannel.findAndCountAll(query).then(({ rows, count }) => {
249 return { total: count, data: rows }
250 })
251}
252
253loadByUUID = function (uuid: string, t?: Sequelize.Transaction) {
254 const query: Sequelize.FindOptions<VideoChannelAttributes> = {
255 where: {
256 uuid
257 }
258 }
259
260 if (t !== undefined) query.transaction = t
261
262 return VideoChannel.findOne(query)
263}
264
265loadByHostAndUUID = function (fromHost: string, uuid: string, t?: Sequelize.Transaction) {
266 const query: Sequelize.FindOptions<VideoChannelAttributes> = {
267 where: {
268 uuid
269 },
270 include: [
271 {
272 model: VideoChannel['sequelize'].models.Author,
273 include: [
274 {
275 model: VideoChannel['sequelize'].models.Pod,
276 required: true,
277 where: {
278 host: fromHost
279 }
280 }
281 ]
282 }
283 ]
284 }
285
286 if (t !== undefined) query.transaction = t
287
288 return VideoChannel.findOne(query)
289}
290
291loadByIdAndAuthor = function (id: number, authorId: number) {
292 const options = {
293 where: {
294 id,
295 authorId
296 },
297 include: [
298 {
299 model: VideoChannel['sequelize'].models.Author,
300 include: [ { model: VideoChannel['sequelize'].models.Pod, required: false } ]
301 }
302 ]
303 }
304
305 return VideoChannel.findOne(options)
306}
307
308loadAndPopulateAuthor = function (id: number) {
309 const options = {
310 include: [
311 {
312 model: VideoChannel['sequelize'].models.Author,
313 include: [ { model: VideoChannel['sequelize'].models.Pod, required: false } ]
314 }
315 ]
316 }
317
318 return VideoChannel.findById(id, options)
319}
320
321loadByUUIDAndPopulateAuthor = function (uuid: string) {
322 const options = {
323 where: {
324 uuid
325 },
326 include: [
327 {
328 model: VideoChannel['sequelize'].models.Author,
329 include: [ { model: VideoChannel['sequelize'].models.Pod, required: false } ]
330 }
331 ]
332 }
333
334 return VideoChannel.findOne(options)
335}
336
337loadAndPopulateAuthorAndVideos = function (id: number) {
338 const options = {
339 include: [
340 {
341 model: VideoChannel['sequelize'].models.Author,
342 include: [ { model: VideoChannel['sequelize'].models.Pod, required: false } ]
343 },
344 VideoChannel['sequelize'].models.Video
345 ]
346 }
347
348 return VideoChannel.findById(id, options)
349}
diff --git a/server/models/video/video-interface.ts b/server/models/video/video-interface.ts
index 86ce84dd9..4b5ae08c2 100644
--- a/server/models/video/video-interface.ts
+++ b/server/models/video/video-interface.ts
@@ -6,16 +6,21 @@ import { TagAttributes, TagInstance } from './tag-interface'
6import { VideoFileAttributes, VideoFileInstance } from './video-file-interface' 6import { VideoFileAttributes, VideoFileInstance } from './video-file-interface'
7 7
8// Don't use barrel, import just what we need 8// Don't use barrel, import just what we need
9import { Video as FormattedVideo } from '../../../shared/models/videos/video.model' 9import {
10 Video as FormattedVideo,
11 VideoDetails as FormattedDetailsVideo
12} from '../../../shared/models/videos/video.model'
10import { RemoteVideoUpdateData } from '../../../shared/models/pods/remote-video/remote-video-update-request.model' 13import { RemoteVideoUpdateData } from '../../../shared/models/pods/remote-video/remote-video-update-request.model'
11import { RemoteVideoCreateData } from '../../../shared/models/pods/remote-video/remote-video-create-request.model' 14import { RemoteVideoCreateData } from '../../../shared/models/pods/remote-video/remote-video-create-request.model'
12import { ResultList } from '../../../shared/models/result-list.model' 15import { ResultList } from '../../../shared/models/result-list.model'
16import { VideoChannelInstance } from './video-channel-interface'
13 17
14export namespace VideoMethods { 18export namespace VideoMethods {
15 export type GetThumbnailName = (this: VideoInstance) => string 19 export type GetThumbnailName = (this: VideoInstance) => string
16 export type GetPreviewName = (this: VideoInstance) => string 20 export type GetPreviewName = (this: VideoInstance) => string
17 export type IsOwned = (this: VideoInstance) => boolean 21 export type IsOwned = (this: VideoInstance) => boolean
18 export type ToFormattedJSON = (this: VideoInstance) => FormattedVideo 22 export type ToFormattedJSON = (this: VideoInstance) => FormattedVideo
23 export type ToFormattedDetailsJSON = (this: VideoInstance) => FormattedDetailsVideo
19 24
20 export type GetOriginalFile = (this: VideoInstance) => VideoFileInstance 25 export type GetOriginalFile = (this: VideoInstance) => VideoFileInstance
21 export type GetTorrentFileName = (this: VideoInstance, videoFile: VideoFileInstance) => string 26 export type GetTorrentFileName = (this: VideoInstance, videoFile: VideoFileInstance) => string
@@ -52,8 +57,8 @@ export namespace VideoMethods {
52 ) => Promise< ResultList<VideoInstance> > 57 ) => Promise< ResultList<VideoInstance> >
53 58
54 export type Load = (id: number) => Promise<VideoInstance> 59 export type Load = (id: number) => Promise<VideoInstance>
55 export type LoadByUUID = (uuid: string) => Promise<VideoInstance> 60 export type LoadByUUID = (uuid: string, t?: Sequelize.Transaction) => Promise<VideoInstance>
56 export type LoadByHostAndUUID = (fromHost: string, uuid: string) => Promise<VideoInstance> 61 export type LoadByHostAndUUID = (fromHost: string, uuid: string, t?: Sequelize.Transaction) => Promise<VideoInstance>
57 export type LoadAndPopulateAuthor = (id: number) => Promise<VideoInstance> 62 export type LoadAndPopulateAuthor = (id: number) => Promise<VideoInstance>
58 export type LoadAndPopulateAuthorAndPodAndTags = (id: number) => Promise<VideoInstance> 63 export type LoadAndPopulateAuthorAndPodAndTags = (id: number) => Promise<VideoInstance>
59 export type LoadByUUIDAndPopulateAuthorAndPodAndTags = (uuid: string) => Promise<VideoInstance> 64 export type LoadByUUIDAndPopulateAuthorAndPodAndTags = (uuid: string) => Promise<VideoInstance>
@@ -94,7 +99,9 @@ export interface VideoAttributes {
94 dislikes?: number 99 dislikes?: number
95 remote: boolean 100 remote: boolean
96 101
97 Author?: AuthorInstance 102 channelId?: number
103
104 VideoChannel?: VideoChannelInstance
98 Tags?: TagInstance[] 105 Tags?: TagInstance[]
99 VideoFiles?: VideoFileInstance[] 106 VideoFiles?: VideoFileInstance[]
100} 107}
@@ -121,6 +128,7 @@ export interface VideoInstance extends VideoClass, VideoAttributes, Sequelize.In
121 removeTorrent: VideoMethods.RemoveTorrent 128 removeTorrent: VideoMethods.RemoveTorrent
122 toAddRemoteJSON: VideoMethods.ToAddRemoteJSON 129 toAddRemoteJSON: VideoMethods.ToAddRemoteJSON
123 toFormattedJSON: VideoMethods.ToFormattedJSON 130 toFormattedJSON: VideoMethods.ToFormattedJSON
131 toFormattedDetailsJSON: VideoMethods.ToFormattedDetailsJSON
124 toUpdateRemoteJSON: VideoMethods.ToUpdateRemoteJSON 132 toUpdateRemoteJSON: VideoMethods.ToUpdateRemoteJSON
125 optimizeOriginalVideofile: VideoMethods.OptimizeOriginalVideofile 133 optimizeOriginalVideofile: VideoMethods.OptimizeOriginalVideofile
126 transcodeOriginalVideofile: VideoMethods.TranscodeOriginalVideofile 134 transcodeOriginalVideofile: VideoMethods.TranscodeOriginalVideofile
diff --git a/server/models/video/video.ts b/server/models/video/video.ts
index 0b1af4d21..d9b976404 100644
--- a/server/models/video/video.ts
+++ b/server/models/video/video.ts
@@ -60,6 +60,7 @@ let getPreviewPath: VideoMethods.GetPreviewPath
60let getTorrentFileName: VideoMethods.GetTorrentFileName 60let getTorrentFileName: VideoMethods.GetTorrentFileName
61let isOwned: VideoMethods.IsOwned 61let isOwned: VideoMethods.IsOwned
62let toFormattedJSON: VideoMethods.ToFormattedJSON 62let toFormattedJSON: VideoMethods.ToFormattedJSON
63let toFormattedDetailsJSON: VideoMethods.ToFormattedDetailsJSON
63let toAddRemoteJSON: VideoMethods.ToAddRemoteJSON 64let toAddRemoteJSON: VideoMethods.ToAddRemoteJSON
64let toUpdateRemoteJSON: VideoMethods.ToUpdateRemoteJSON 65let toUpdateRemoteJSON: VideoMethods.ToUpdateRemoteJSON
65let optimizeOriginalVideofile: VideoMethods.OptimizeOriginalVideofile 66let optimizeOriginalVideofile: VideoMethods.OptimizeOriginalVideofile
@@ -206,9 +207,6 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
206 { 207 {
207 indexes: [ 208 indexes: [
208 { 209 {
209 fields: [ 'authorId' ]
210 },
211 {
212 fields: [ 'name' ] 210 fields: [ 'name' ]
213 }, 211 },
214 { 212 {
@@ -225,6 +223,9 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
225 }, 223 },
226 { 224 {
227 fields: [ 'uuid' ] 225 fields: [ 'uuid' ]
226 },
227 {
228 fields: [ 'channelId' ]
228 } 229 }
229 ], 230 ],
230 hooks: { 231 hooks: {
@@ -268,6 +269,7 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
268 removeTorrent, 269 removeTorrent,
269 toAddRemoteJSON, 270 toAddRemoteJSON,
270 toFormattedJSON, 271 toFormattedJSON,
272 toFormattedDetailsJSON,
271 toUpdateRemoteJSON, 273 toUpdateRemoteJSON,
272 optimizeOriginalVideofile, 274 optimizeOriginalVideofile,
273 transcodeOriginalVideofile, 275 transcodeOriginalVideofile,
@@ -282,9 +284,9 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
282// ------------------------------ METHODS ------------------------------ 284// ------------------------------ METHODS ------------------------------
283 285
284function associate (models) { 286function associate (models) {
285 Video.belongsTo(models.Author, { 287 Video.belongsTo(models.VideoChannel, {
286 foreignKey: { 288 foreignKey: {
287 name: 'authorId', 289 name: 'channelId',
288 allowNull: false 290 allowNull: false
289 }, 291 },
290 onDelete: 'cascade' 292 onDelete: 'cascade'
@@ -439,8 +441,8 @@ getPreviewPath = function (this: VideoInstance) {
439toFormattedJSON = function (this: VideoInstance) { 441toFormattedJSON = function (this: VideoInstance) {
440 let podHost 442 let podHost
441 443
442 if (this.Author.Pod) { 444 if (this.VideoChannel.Author.Pod) {
443 podHost = this.Author.Pod.host 445 podHost = this.VideoChannel.Author.Pod.host
444 } else { 446 } else {
445 // It means it's our video 447 // It means it's our video
446 podHost = CONFIG.WEBSERVER.HOST 448 podHost = CONFIG.WEBSERVER.HOST
@@ -472,7 +474,59 @@ toFormattedJSON = function (this: VideoInstance) {
472 description: this.description, 474 description: this.description,
473 podHost, 475 podHost,
474 isLocal: this.isOwned(), 476 isLocal: this.isOwned(),
475 author: this.Author.name, 477 author: this.VideoChannel.Author.name,
478 duration: this.duration,
479 views: this.views,
480 likes: this.likes,
481 dislikes: this.dislikes,
482 tags: map<TagInstance, string>(this.Tags, 'name'),
483 thumbnailPath: this.getThumbnailPath(),
484 previewPath: this.getPreviewPath(),
485 embedPath: this.getEmbedPath(),
486 createdAt: this.createdAt,
487 updatedAt: this.updatedAt
488 }
489
490 return json
491}
492
493toFormattedDetailsJSON = function (this: VideoInstance) {
494 let podHost
495
496 if (this.VideoChannel.Author.Pod) {
497 podHost = this.VideoChannel.Author.Pod.host
498 } else {
499 // It means it's our video
500 podHost = CONFIG.WEBSERVER.HOST
501 }
502
503 // Maybe our pod is not up to date and there are new categories since our version
504 let categoryLabel = VIDEO_CATEGORIES[this.category]
505 if (!categoryLabel) categoryLabel = 'Misc'
506
507 // Maybe our pod is not up to date and there are new licences since our version
508 let licenceLabel = VIDEO_LICENCES[this.licence]
509 if (!licenceLabel) licenceLabel = 'Unknown'
510
511 // Language is an optional attribute
512 let languageLabel = VIDEO_LANGUAGES[this.language]
513 if (!languageLabel) languageLabel = 'Unknown'
514
515 const json = {
516 id: this.id,
517 uuid: this.uuid,
518 name: this.name,
519 category: this.category,
520 categoryLabel,
521 licence: this.licence,
522 licenceLabel,
523 language: this.language,
524 languageLabel,
525 nsfw: this.nsfw,
526 description: this.description,
527 podHost,
528 isLocal: this.isOwned(),
529 author: this.VideoChannel.Author.name,
476 duration: this.duration, 530 duration: this.duration,
477 views: this.views, 531 views: this.views,
478 likes: this.likes, 532 likes: this.likes,
@@ -483,6 +537,7 @@ toFormattedJSON = function (this: VideoInstance) {
483 embedPath: this.getEmbedPath(), 537 embedPath: this.getEmbedPath(),
484 createdAt: this.createdAt, 538 createdAt: this.createdAt,
485 updatedAt: this.updatedAt, 539 updatedAt: this.updatedAt,
540 channel: this.VideoChannel.toFormattedJSON(),
486 files: [] 541 files: []
487 } 542 }
488 543
@@ -525,7 +580,7 @@ toAddRemoteJSON = function (this: VideoInstance) {
525 language: this.language, 580 language: this.language,
526 nsfw: this.nsfw, 581 nsfw: this.nsfw,
527 description: this.description, 582 description: this.description,
528 author: this.Author.name, 583 channelUUID: this.VideoChannel.uuid,
529 duration: this.duration, 584 duration: this.duration,
530 thumbnailData: thumbnailData.toString('binary'), 585 thumbnailData: thumbnailData.toString('binary'),
531 tags: map<TagInstance, string>(this.Tags, 'name'), 586 tags: map<TagInstance, string>(this.Tags, 'name'),
@@ -559,7 +614,6 @@ toUpdateRemoteJSON = function (this: VideoInstance) {
559 language: this.language, 614 language: this.language,
560 nsfw: this.nsfw, 615 nsfw: this.nsfw,
561 description: this.description, 616 description: this.description,
562 author: this.Author.name,
563 duration: this.duration, 617 duration: this.duration,
564 tags: map<TagInstance, string>(this.Tags, 'name'), 618 tags: map<TagInstance, string>(this.Tags, 'name'),
565 createdAt: this.createdAt, 619 createdAt: this.createdAt,
@@ -723,8 +777,18 @@ listForApi = function (start: number, count: number, sort: string) {
723 order: [ getSort(sort), [ Video['sequelize'].models.Tag, 'name', 'ASC' ] ], 777 order: [ getSort(sort), [ Video['sequelize'].models.Tag, 'name', 'ASC' ] ],
724 include: [ 778 include: [
725 { 779 {
726 model: Video['sequelize'].models.Author, 780 model: Video['sequelize'].models.VideoChannel,
727 include: [ { model: Video['sequelize'].models.Pod, required: false } ] 781 include: [
782 {
783 model: Video['sequelize'].models.Author,
784 include: [
785 {
786 model: Video['sequelize'].models.Pod,
787 required: false
788 }
789 ]
790 }
791 ]
728 }, 792 },
729 Video['sequelize'].models.Tag, 793 Video['sequelize'].models.Tag,
730 Video['sequelize'].models.VideoFile 794 Video['sequelize'].models.VideoFile
@@ -740,8 +804,8 @@ listForApi = function (start: number, count: number, sort: string) {
740 }) 804 })
741} 805}
742 806
743loadByHostAndUUID = function (fromHost: string, uuid: string) { 807loadByHostAndUUID = function (fromHost: string, uuid: string, t?: Sequelize.Transaction) {
744 const query = { 808 const query: Sequelize.FindOptions<VideoAttributes> = {
745 where: { 809 where: {
746 uuid 810 uuid
747 }, 811 },
@@ -750,20 +814,27 @@ loadByHostAndUUID = function (fromHost: string, uuid: string) {
750 model: Video['sequelize'].models.VideoFile 814 model: Video['sequelize'].models.VideoFile
751 }, 815 },
752 { 816 {
753 model: Video['sequelize'].models.Author, 817 model: Video['sequelize'].models.VideoChannel,
754 include: [ 818 include: [
755 { 819 {
756 model: Video['sequelize'].models.Pod, 820 model: Video['sequelize'].models.Author,
757 required: true, 821 include: [
758 where: { 822 {
759 host: fromHost 823 model: Video['sequelize'].models.Pod,
760 } 824 required: true,
825 where: {
826 host: fromHost
827 }
828 }
829 ]
761 } 830 }
762 ] 831 ]
763 } 832 }
764 ] 833 ]
765 } 834 }
766 835
836 if (t !== undefined) query.transaction = t
837
767 return Video.findOne(query) 838 return Video.findOne(query)
768} 839}
769 840
@@ -774,7 +845,10 @@ listOwnedAndPopulateAuthorAndTags = function () {
774 }, 845 },
775 include: [ 846 include: [
776 Video['sequelize'].models.VideoFile, 847 Video['sequelize'].models.VideoFile,
777 Video['sequelize'].models.Author, 848 {
849 model: Video['sequelize'].models.VideoChannel,
850 include: [ Video['sequelize'].models.Author ]
851 },
778 Video['sequelize'].models.Tag 852 Video['sequelize'].models.Tag
779 ] 853 ]
780 } 854 }
@@ -792,10 +866,15 @@ listOwnedByAuthor = function (author: string) {
792 model: Video['sequelize'].models.VideoFile 866 model: Video['sequelize'].models.VideoFile
793 }, 867 },
794 { 868 {
795 model: Video['sequelize'].models.Author, 869 model: Video['sequelize'].models.VideoChannel,
796 where: { 870 include: [
797 name: author 871 {
798 } 872 model: Video['sequelize'].models.Author,
873 where: {
874 name: author
875 }
876 }
877 ]
799 } 878 }
800 ] 879 ]
801 } 880 }
@@ -807,19 +886,28 @@ load = function (id: number) {
807 return Video.findById(id) 886 return Video.findById(id)
808} 887}
809 888
810loadByUUID = function (uuid: string) { 889loadByUUID = function (uuid: string, t?: Sequelize.Transaction) {
811 const query = { 890 const query: Sequelize.FindOptions<VideoAttributes> = {
812 where: { 891 where: {
813 uuid 892 uuid
814 }, 893 },
815 include: [ Video['sequelize'].models.VideoFile ] 894 include: [ Video['sequelize'].models.VideoFile ]
816 } 895 }
896
897 if (t !== undefined) query.transaction = t
898
817 return Video.findOne(query) 899 return Video.findOne(query)
818} 900}
819 901
820loadAndPopulateAuthor = function (id: number) { 902loadAndPopulateAuthor = function (id: number) {
821 const options = { 903 const options = {
822 include: [ Video['sequelize'].models.VideoFile, Video['sequelize'].models.Author ] 904 include: [
905 Video['sequelize'].models.VideoFile,
906 {
907 model: Video['sequelize'].models.VideoChannel,
908 include: [ Video['sequelize'].models.Author ]
909 }
910 ]
823 } 911 }
824 912
825 return Video.findById(id, options) 913 return Video.findById(id, options)
@@ -829,8 +917,13 @@ loadAndPopulateAuthorAndPodAndTags = function (id: number) {
829 const options = { 917 const options = {
830 include: [ 918 include: [
831 { 919 {
832 model: Video['sequelize'].models.Author, 920 model: Video['sequelize'].models.VideoChannel,
833 include: [ { model: Video['sequelize'].models.Pod, required: false } ] 921 include: [
922 {
923 model: Video['sequelize'].models.Author,
924 include: [ { model: Video['sequelize'].models.Pod, required: false } ]
925 }
926 ]
834 }, 927 },
835 Video['sequelize'].models.Tag, 928 Video['sequelize'].models.Tag,
836 Video['sequelize'].models.VideoFile 929 Video['sequelize'].models.VideoFile
@@ -847,8 +940,13 @@ loadByUUIDAndPopulateAuthorAndPodAndTags = function (uuid: string) {
847 }, 940 },
848 include: [ 941 include: [
849 { 942 {
850 model: Video['sequelize'].models.Author, 943 model: Video['sequelize'].models.VideoChannel,
851 include: [ { model: Video['sequelize'].models.Pod, required: false } ] 944 include: [
945 {
946 model: Video['sequelize'].models.Author,
947 include: [ { model: Video['sequelize'].models.Pod, required: false } ]
948 }
949 ]
852 }, 950 },
853 Video['sequelize'].models.Tag, 951 Video['sequelize'].models.Tag,
854 Video['sequelize'].models.VideoFile 952 Video['sequelize'].models.VideoFile
@@ -866,9 +964,13 @@ searchAndPopulateAuthorAndPodAndTags = function (value: string, field: string, s
866 964
867 const authorInclude: Sequelize.IncludeOptions = { 965 const authorInclude: Sequelize.IncludeOptions = {
868 model: Video['sequelize'].models.Author, 966 model: Video['sequelize'].models.Author,
869 include: [ 967 include: [ podInclude ]
870 podInclude 968 }
871 ] 969
970 const videoChannelInclude: Sequelize.IncludeOptions = {
971 model: Video['sequelize'].models.VideoChannel,
972 include: [ authorInclude ],
973 required: true
872 } 974 }
873 975
874 const tagInclude: Sequelize.IncludeOptions = { 976 const tagInclude: Sequelize.IncludeOptions = {
@@ -915,8 +1017,6 @@ searchAndPopulateAuthorAndPodAndTags = function (value: string, field: string, s
915 $iLike: '%' + value + '%' 1017 $iLike: '%' + value + '%'
916 } 1018 }
917 } 1019 }
918
919 // authorInclude.or = true
920 } else { 1020 } else {
921 query.where[field] = { 1021 query.where[field] = {
922 $iLike: '%' + value + '%' 1022 $iLike: '%' + value + '%'
@@ -924,7 +1024,7 @@ searchAndPopulateAuthorAndPodAndTags = function (value: string, field: string, s
924 } 1024 }
925 1025
926 query.include = [ 1026 query.include = [
927 authorInclude, tagInclude, videoFileInclude 1027 videoChannelInclude, tagInclude, videoFileInclude
928 ] 1028 ]
929 1029
930 return Video.findAndCountAll(query).then(({ rows, count }) => { 1030 return Video.findAndCountAll(query).then(({ rows, count }) => {
@@ -955,8 +1055,8 @@ function getBaseUrls (video: VideoInstance) {
955 baseUrlHttp = CONFIG.WEBSERVER.URL 1055 baseUrlHttp = CONFIG.WEBSERVER.URL
956 baseUrlWs = CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT 1056 baseUrlWs = CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT
957 } else { 1057 } else {
958 baseUrlHttp = REMOTE_SCHEME.HTTP + '://' + video.Author.Pod.host 1058 baseUrlHttp = REMOTE_SCHEME.HTTP + '://' + video.VideoChannel.Author.Pod.host
959 baseUrlWs = REMOTE_SCHEME.WS + '://' + video.Author.Pod.host 1059 baseUrlWs = REMOTE_SCHEME.WS + '://' + video.VideoChannel.Author.Pod.host
960 } 1060 }
961 1061
962 return { baseUrlHttp, baseUrlWs } 1062 return { baseUrlHttp, baseUrlWs }