diff options
56 files changed, 2011 insertions, 280 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' |
10 | import { sendOwnedVideosToPod } from '../../../lib' | 10 | import { sendOwnedDataToPod } from '../../../lib' |
11 | import { getMyPublicCert, getFormattedObjects } from '../../../helpers' | 11 | import { getMyPublicCert, getFormattedObjects } from '../../../helpers' |
12 | import { CONFIG } from '../../../initializers' | 12 | import { CONFIG } from '../../../initializers' |
13 | import { PodInstance } from '../../../models' | 13 | import { 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 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import * as Promise from 'bluebird' | 2 | import * as Promise from 'bluebird' |
3 | import * as Sequelize from 'sequelize' | ||
3 | 4 | ||
4 | import { database as db } from '../../../initializers/database' | 5 | import { database as db } from '../../../initializers/database' |
5 | import { | 6 | import { |
@@ -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 | ||
33 | const ENDPOINT_ACTIONS = REQUEST_ENDPOINT_ACTIONS[REQUEST_ENDPOINTS.VIDEOS] | 39 | const 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 | ||
36 | const functionsHash: { [ id: string ]: (...args) => Promise<any> } = {} | 43 | const functionsHash: { [ id: string ]: (...args) => Promise<any> } = {} |
37 | functionsHash[ENDPOINT_ACTIONS.ADD] = addRemoteVideoRetryWrapper | 44 | functionsHash[ENDPOINT_ACTIONS.ADD_VIDEO] = addRemoteVideoRetryWrapper |
38 | functionsHash[ENDPOINT_ACTIONS.UPDATE] = updateRemoteVideoRetryWrapper | 45 | functionsHash[ENDPOINT_ACTIONS.UPDATE_VIDEO] = updateRemoteVideoRetryWrapper |
39 | functionsHash[ENDPOINT_ACTIONS.REMOVE] = removeRemoteVideo | 46 | functionsHash[ENDPOINT_ACTIONS.REMOVE_VIDEO] = removeRemoteVideoRetryWrapper |
40 | functionsHash[ENDPOINT_ACTIONS.REPORT_ABUSE] = reportAbuseRemoteVideo | 47 | functionsHash[ENDPOINT_ACTIONS.ADD_CHANNEL] = addRemoteVideoChannelRetryWrapper |
48 | functionsHash[ENDPOINT_ACTIONS.UPDATE_CHANNEL] = updateRemoteVideoChannelRetryWrapper | ||
49 | functionsHash[ENDPOINT_ACTIONS.REMOVE_CHANNEL] = removeRemoteVideoChannelRetryWrapper | ||
50 | functionsHash[ENDPOINT_ACTIONS.REPORT_ABUSE] = reportAbuseRemoteVideoRetryWrapper | ||
51 | functionsHash[ENDPOINT_ACTIONS.ADD_AUTHOR] = addRemoteVideoAuthorRetryWrapper | ||
52 | functionsHash[ENDPOINT_ACTIONS.REMOVE_AUTHOR] = removeRemoteVideoAuthorRetryWrapper | ||
41 | 53 | ||
42 | const remoteVideosRouter = express.Router() | 54 | const remoteVideosRouter = express.Router() |
43 | 55 | ||
@@ -133,7 +145,7 @@ function processVideosEventsRetryWrapper (eventData: RemoteVideoEventData, fromP | |||
133 | function processVideosEvents (eventData: RemoteVideoEventData, fromPod: PodInstance) { | 145 | function 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 | ||
413 | function 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 | |||
407 | function removeRemoteVideo (videoToRemoveData: RemoteVideoRemoveData, fromPod: PodInstance) { | 422 | function 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 | |||
437 | function 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 | |||
446 | function 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 | ||
475 | function 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 | |||
484 | function 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 | |||
498 | function 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 | |||
507 | function 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 | |||
547 | function 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 | |||
556 | function 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 | |||
580 | function 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 | |||
589 | function 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 | |||
603 | function 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 | |||
419 | function reportAbuseRemoteVideo (reportData: RemoteVideoReportAbuseData, fromPod: PodInstance) { | 612 | function 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 | ||
436 | function fetchVideoByUUID (id: string) { | 636 | function 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 | ||
449 | function fetchVideoByHostAndUUID (podHost: string, uuid: string) { | 649 | function 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 | |||
662 | function 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 | ||
3 | import { database as db } from '../../initializers/database' | 3 | import { database as db } from '../../initializers/database' |
4 | import { USER_ROLES, CONFIG } from '../../initializers' | 4 | import { USER_ROLES, CONFIG } from '../../initializers' |
5 | import { logger, getFormattedObjects } from '../../helpers' | 5 | import { logger, getFormattedObjects, retryTransactionWrapper } from '../../helpers' |
6 | import { | 6 | import { |
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' |
29 | import { createUserAuthorAndChannel } from '../../lib' | ||
29 | import { UserInstance } from '../../models' | 30 | import { UserInstance } from '../../models' |
30 | 31 | ||
31 | const usersRouter = express.Router() | 32 | const 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 | ||
64 | usersRouter.post('/register', | 65 | usersRouter.post('/register', |
@@ -98,9 +99,22 @@ export { | |||
98 | 99 | ||
99 | // --------------------------------------------------------------------------- | 100 | // --------------------------------------------------------------------------- |
100 | 101 | ||
102 | function 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 | |||
101 | function createUser (req: express.Request, res: express.Response, next: express.NextFunction) { | 116 | function 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 | ||
118 | function registerUser (req: express.Request, res: express.Response, next: express.NextFunction) { | 135 | function 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 | ||
135 | function getUserInformation (req: express.Request, res: express.Response, next: express.NextFunction) { | 152 | function 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 @@ | |||
1 | import * as express from 'express' | ||
2 | |||
3 | import { database as db } from '../../../initializers' | ||
4 | import { | ||
5 | logger, | ||
6 | getFormattedObjects, | ||
7 | retryTransactionWrapper | ||
8 | } from '../../../helpers' | ||
9 | import { | ||
10 | authenticate, | ||
11 | paginationValidator, | ||
12 | videoChannelsSortValidator, | ||
13 | videoChannelsAddValidator, | ||
14 | setVideoChannelsSort, | ||
15 | setPagination, | ||
16 | videoChannelsRemoveValidator, | ||
17 | videoChannelGetValidator, | ||
18 | videoChannelsUpdateValidator, | ||
19 | listVideoAuthorChannelsValidator | ||
20 | } from '../../../middlewares' | ||
21 | import { | ||
22 | createVideoChannel, | ||
23 | updateVideoChannelToFriends | ||
24 | } from '../../../lib' | ||
25 | import { VideoChannelInstance, AuthorInstance } from '../../../models' | ||
26 | import { VideoChannelCreate, VideoChannelUpdate } from '../../../../shared' | ||
27 | |||
28 | const videoChannelRouter = express.Router() | ||
29 | |||
30 | videoChannelRouter.get('/channels', | ||
31 | paginationValidator, | ||
32 | videoChannelsSortValidator, | ||
33 | setVideoChannelsSort, | ||
34 | setPagination, | ||
35 | listVideoChannels | ||
36 | ) | ||
37 | |||
38 | videoChannelRouter.get('/authors/:authorId/channels', | ||
39 | listVideoAuthorChannelsValidator, | ||
40 | listVideoAuthorChannels | ||
41 | ) | ||
42 | |||
43 | videoChannelRouter.post('/channels', | ||
44 | authenticate, | ||
45 | videoChannelsAddValidator, | ||
46 | addVideoChannelRetryWrapper | ||
47 | ) | ||
48 | |||
49 | videoChannelRouter.put('/channels/:id', | ||
50 | authenticate, | ||
51 | videoChannelsUpdateValidator, | ||
52 | updateVideoChannelRetryWrapper | ||
53 | ) | ||
54 | |||
55 | videoChannelRouter.delete('/channels/:id', | ||
56 | authenticate, | ||
57 | videoChannelsRemoveValidator, | ||
58 | removeVideoChannelRetryWrapper | ||
59 | ) | ||
60 | |||
61 | videoChannelRouter.get('/channels/:id', | ||
62 | videoChannelGetValidator, | ||
63 | getVideoChannel | ||
64 | ) | ||
65 | |||
66 | // --------------------------------------------------------------------------- | ||
67 | |||
68 | export { | ||
69 | videoChannelRouter | ||
70 | } | ||
71 | |||
72 | // --------------------------------------------------------------------------- | ||
73 | |||
74 | function 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 | |||
80 | function 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 | ||
88 | function 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 | |||
102 | function 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 | |||
116 | function 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 | |||
127 | function 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 | |||
166 | function 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 | |||
177 | function 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 | |||
192 | function 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' | |||
46 | import { abuseVideoRouter } from './abuse' | 46 | import { abuseVideoRouter } from './abuse' |
47 | import { blacklistRouter } from './blacklist' | 47 | import { blacklistRouter } from './blacklist' |
48 | import { rateVideoRouter } from './rate' | 48 | import { rateVideoRouter } from './rate' |
49 | import { videoChannelRouter } from './channel' | ||
49 | 50 | ||
50 | const videosRouter = express.Router() | 51 | const videosRouter = express.Router() |
51 | 52 | ||
@@ -76,6 +77,7 @@ const reqFiles = multer({ storage: storage }).fields([{ name: 'videofile', maxCo | |||
76 | videosRouter.use('/', abuseVideoRouter) | 77 | videosRouter.use('/', abuseVideoRouter) |
77 | videosRouter.use('/', blacklistRouter) | 78 | videosRouter.use('/', blacklistRouter) |
78 | videosRouter.use('/', rateVideoRouter) | 79 | videosRouter.use('/', rateVideoRouter) |
80 | videosRouter.use('/', videoChannelRouter) | ||
79 | 81 | ||
80 | videosRouter.get('/categories', listVideoCategories) | 82 | videosRouter.get('/categories', listVideoCategories) |
81 | videosRouter.get('/licences', listVideoLicences) | 83 | videosRouter.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 | ||
398 | function listVideos (req: express.Request, res: express.Response, next: express.NextFunction) { | 392 | function 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' | |||
3 | export * from './pods' | 3 | export * from './pods' |
4 | export * from './pods' | 4 | export * from './pods' |
5 | export * from './users' | 5 | export * from './users' |
6 | export * from './video-authors' | ||
7 | export * from './video-channels' | ||
6 | export * from './videos' | 8 | export * 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 @@ | |||
1 | import 'express-validator' | 1 | import * as validator from 'validator' |
2 | 2 | ||
3 | function exists (value: any) { | 3 | function 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 | ||
11 | function isDateValid (value: string) { | ||
12 | return exists(value) && validator.isISO8601(value) | ||
13 | } | ||
14 | |||
15 | function isIdValid (value: string) { | ||
16 | return exists(value) && validator.isInt('' + value) | ||
17 | } | ||
18 | |||
19 | function isUUIDValid (value: string) { | ||
20 | return exists(value) && validator.isUUID('' + value, 4) | ||
21 | } | ||
22 | |||
23 | function isIdOrUUIDValid (value: string) { | ||
24 | return isIdValid(value) || isUUIDValid(value) | ||
25 | } | ||
26 | |||
11 | // --------------------------------------------------------------------------- | 27 | // --------------------------------------------------------------------------- |
12 | 28 | ||
13 | export { | 29 | export { |
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' |
9 | import { isArray } from '../misc' | 9 | import { isArray, isDateValid, isUUIDValid } from '../misc' |
10 | import { | 10 | import { |
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' |
30 | import { isVideoChannelDescriptionValid, isVideoChannelNameValid } from '../video-channels' | ||
31 | import { isVideoAuthorNameValid } from '../video-authors' | ||
33 | 32 | ||
34 | const ENDPOINT_ACTIONS = REQUEST_ENDPOINT_ACTIONS[REQUEST_ENDPOINTS.VIDEOS] | 33 | const ENDPOINT_ACTIONS = REQUEST_ENDPOINT_ACTIONS[REQUEST_ENDPOINTS.VIDEOS] |
35 | 34 | ||
35 | const checkers: { [ id: string ]: (obj: any) => boolean } = {} | ||
36 | checkers[ENDPOINT_ACTIONS.ADD_VIDEO] = checkAddVideo | ||
37 | checkers[ENDPOINT_ACTIONS.UPDATE_VIDEO] = checkUpdateVideo | ||
38 | checkers[ENDPOINT_ACTIONS.REMOVE_VIDEO] = checkRemoveVideo | ||
39 | checkers[ENDPOINT_ACTIONS.REPORT_ABUSE] = checkReportVideo | ||
40 | checkers[ENDPOINT_ACTIONS.ADD_CHANNEL] = checkAddVideoChannel | ||
41 | checkers[ENDPOINT_ACTIONS.UPDATE_CHANNEL] = checkUpdateVideoChannel | ||
42 | checkers[ENDPOINT_ACTIONS.REMOVE_CHANNEL] = checkRemoveVideoChannel | ||
43 | checkers[ENDPOINT_ACTIONS.ADD_AUTHOR] = checkAddAuthor | ||
44 | checkers[ENDPOINT_ACTIONS.REMOVE_AUTHOR] = checkRemoveAuthor | ||
45 | |||
36 | function isEachRemoteRequestVideosValid (requests: any[]) { | 46 | function 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 | ||
107 | function isCommonVideoAttributesValid (video: any) { | 102 | function 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 | ||
134 | function isRequestTypeAddValid (value: string) { | 129 | function checkAddVideo (video: any) { |
135 | return value === ENDPOINT_ACTIONS.ADD | 130 | return isCommonVideoAttributesValid(video) && |
131 | isUUIDValid(video.channelUUID) && | ||
132 | isVideoThumbnailDataValid(video.thumbnailData) | ||
133 | } | ||
134 | |||
135 | function checkUpdateVideo (video: any) { | ||
136 | return isCommonVideoAttributesValid(video) | ||
137 | } | ||
138 | |||
139 | function checkRemoveVideo (video: any) { | ||
140 | return isUUIDValid(video.uuid) | ||
141 | } | ||
142 | |||
143 | function checkReportVideo (abuse: any) { | ||
144 | return isUUIDValid(abuse.videoUUID) && | ||
145 | isVideoAbuseReasonValid(abuse.reportReason) && | ||
146 | isVideoAbuseReporterUsernameValid(abuse.reporterUsername) | ||
147 | } | ||
148 | |||
149 | function 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 | |||
158 | function 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 | ||
138 | function isRequestTypeUpdateValid (value: string) { | 167 | function checkRemoveVideoChannel (videoChannel: any) { |
139 | return value === ENDPOINT_ACTIONS.UPDATE | 168 | return isUUIDValid(videoChannel.uuid) |
140 | } | 169 | } |
141 | 170 | ||
142 | function isRequestTypeRemoveValid (value: string) { | 171 | function checkAddAuthor (author: any) { |
143 | return value === ENDPOINT_ACTIONS.REMOVE | 172 | return isUUIDValid(author.uuid) && |
173 | isVideoAuthorNameValid(author.name) | ||
144 | } | 174 | } |
145 | 175 | ||
146 | function isRequestTypeReportAbuseValid (value: string) { | 176 | function 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 @@ | |||
1 | import * as Promise from 'bluebird' | ||
2 | import * as validator from 'validator' | ||
3 | import * as express from 'express' | ||
4 | import 'express-validator' | ||
5 | |||
6 | import { database as db } from '../../initializers' | ||
7 | import { AuthorInstance } from '../../models' | ||
8 | import { logger } from '../logger' | ||
9 | |||
10 | import { isUserUsernameValid } from './users' | ||
11 | |||
12 | function isVideoAuthorNameValid (value: string) { | ||
13 | return isUserUsernameValid(value) | ||
14 | } | ||
15 | |||
16 | function 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 | |||
42 | export { | ||
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 @@ | |||
1 | import * as Promise from 'bluebird' | ||
2 | import * as validator from 'validator' | ||
3 | import * as express from 'express' | ||
4 | import 'express-validator' | ||
5 | import 'multer' | ||
6 | |||
7 | import { database as db, CONSTRAINTS_FIELDS } from '../../initializers' | ||
8 | import { VideoChannelInstance } from '../../models' | ||
9 | import { logger } from '../logger' | ||
10 | import { exists } from './misc' | ||
11 | |||
12 | const VIDEO_CHANNELS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEO_CHANNELS | ||
13 | |||
14 | function isVideoChannelDescriptionValid (value: string) { | ||
15 | return value === null || validator.isLength(value, VIDEO_CHANNELS_CONSTRAINTS_FIELDS.DESCRIPTION) | ||
16 | } | ||
17 | |||
18 | function isVideoChannelNameValid (value: string) { | ||
19 | return exists(value) && validator.isLength(value, VIDEO_CHANNELS_CONSTRAINTS_FIELDS.NAME) | ||
20 | } | ||
21 | |||
22 | function isVideoChannelUUIDValid (value: string) { | ||
23 | return exists(value) && validator.isUUID('' + value, 4) | ||
24 | } | ||
25 | |||
26 | function 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 | |||
52 | export { | ||
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 | |||
23 | const VIDEO_ABUSES_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEO_ABUSES | 23 | const VIDEO_ABUSES_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEO_ABUSES |
24 | const VIDEO_EVENTS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEO_EVENTS | 24 | const VIDEO_EVENTS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEO_EVENTS |
25 | 25 | ||
26 | function isVideoIdOrUUIDValid (value: string) { | ||
27 | return validator.isInt(value) || isVideoUUIDValid(value) | ||
28 | } | ||
29 | |||
30 | function isVideoAuthorValid (value: string) { | ||
31 | return isUserUsernameValid(value) | ||
32 | } | ||
33 | |||
34 | function isVideoDateValid (value: string) { | ||
35 | return exists(value) && validator.isISO8601(value) | ||
36 | } | ||
37 | |||
38 | function isVideoCategoryValid (value: number) { | 26 | function 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 | ||
82 | function isVideoUUIDValid (value: string) { | ||
83 | return exists(value) && validator.isUUID('' + value, 4) | ||
84 | } | ||
85 | |||
86 | function isVideoAbuseReasonValid (value: string) { | 70 | function 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 | ||
172 | export { | 156 | export { |
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 | ||
235 | const REQUEST_ENDPOINT_ACTIONS: { [ id: string ]: any } = {} | 241 | const REQUEST_ENDPOINT_ACTIONS: { |
242 | [ id: string ]: { | ||
243 | [ id: string ]: RemoteVideoRequestType | ||
244 | } | ||
245 | } = {} | ||
236 | REQUEST_ENDPOINT_ACTIONS[REQUEST_ENDPOINTS.VIDEOS] = { | 246 | REQUEST_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' | |||
14 | import { BlacklistedVideoModel } from './../models/video/video-blacklist-interface' | 14 | import { BlacklistedVideoModel } from './../models/video/video-blacklist-interface' |
15 | import { VideoFileModel } from './../models/video/video-file-interface' | 15 | import { VideoFileModel } from './../models/video/video-file-interface' |
16 | import { VideoAbuseModel } from './../models/video/video-abuse-interface' | 16 | import { VideoAbuseModel } from './../models/video/video-abuse-interface' |
17 | import { VideoChannelModel } from './../models/video/video-channel-interface' | ||
17 | import { UserModel } from './../models/user/user-interface' | 18 | import { UserModel } from './../models/user/user-interface' |
18 | import { UserVideoRateModel } from './../models/user/user-video-rate-interface' | 19 | import { UserVideoRateModel } from './../models/user/user-video-rate-interface' |
19 | import { TagModel } from './../models/video/tag-interface' | 20 | import { 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' | |||
5 | import { USER_ROLES, CONFIG, LAST_MIGRATION_VERSION, CACHE } from './constants' | 5 | import { USER_ROLES, CONFIG, LAST_MIGRATION_VERSION, CACHE } from './constants' |
6 | import { clientsExist, usersExist } from './checker' | 6 | import { clientsExist, usersExist } from './checker' |
7 | import { logger, createCertsIfNotExist, mkdirpPromise, rimrafPromise } from '../helpers' | 7 | import { logger, createCertsIfNotExist, mkdirpPromise, rimrafPromise } from '../helpers' |
8 | import { createUserAuthorAndChannel } from '../lib' | ||
8 | 9 | ||
9 | function installApplication () { | 10 | function 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 | ||
48 | type QaduParam = { videoId: number, type: RequestVideoQaduType } | 54 | type QaduParam = { videoId: number, type: RequestVideoQaduType } |
@@ -62,7 +68,7 @@ function activateSchedulers () { | |||
62 | 68 | ||
63 | function addVideoToFriends (videoData: RemoteVideoCreateData, transaction: Sequelize.Transaction) { | 69 | function 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 | ||
73 | function updateVideoToFriends (videoData: RemoteVideoUpdateData, transaction: Sequelize.Transaction) { | 79 | function 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 | ||
83 | function removeVideoToFriends (videoParams: RemoteVideoRemoveData, transaction: Sequelize.Transaction) { | 89 | function 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 | ||
99 | function 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 | |||
109 | function 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 | |||
119 | function 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 | |||
129 | function 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 | |||
139 | function 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 | |||
93 | function reportAbuseVideoToFriend (reportData: RemoteVideoReportAbuseData, video: VideoInstance, transaction: Sequelize.Transaction) { | 149 | function 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 | ||
266 | function sendOwnedDataToPod (podId: number) { | ||
267 | // First send authors | ||
268 | return sendOwnedAuthorsToPod(podId) | ||
269 | .then(() => sendOwnedChannelsToPod(podId)) | ||
270 | .then(() => sendOwnedVideosToPod(podId)) | ||
271 | } | ||
272 | |||
273 | function 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 | |||
295 | function 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 | |||
210 | function sendOwnedVideosToPod (podId: number) { | 317 | function 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 | ||
239 | function fetchRemotePreview (pod: PodInstance, video: VideoInstance) { | 346 | function 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 () { | |||
274 | export { | 381 | export { |
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 |
399 | type CreateRequestOptions = { | 511 | type 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' | |||
3 | export * from './request' | 3 | export * from './request' |
4 | export * from './friends' | 4 | export * from './friends' |
5 | export * from './oauth-model' | 5 | export * from './oauth-model' |
6 | export * from './user' | ||
7 | export * 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 @@ | |||
1 | import { database as db } from '../initializers' | ||
2 | import { UserInstance } from '../models' | ||
3 | import { addVideoAuthorToFriends } from './friends' | ||
4 | import { createVideoChannel } from './video-channel' | ||
5 | |||
6 | function 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 | |||
44 | export { | ||
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 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | |||
3 | import { addVideoChannelToFriends } from './friends' | ||
4 | import { database as db } from '../initializers' | ||
5 | import { AuthorInstance } from '../models' | ||
6 | import { VideoChannelCreate } from '../../shared/models' | ||
7 | |||
8 | function 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 | |||
40 | export { | ||
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 | ||
25 | function 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 | |||
25 | function setVideosSort (req: express.Request, res: express.Response, next: express.NextFunction) { | 31 | function 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' | |||
6 | export * from './users' | 6 | export * from './users' |
7 | export * from './videos' | 7 | export * from './videos' |
8 | export * from './video-blacklist' | 8 | export * from './video-blacklist' |
9 | export * 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 | ||
5 | import { checkErrors } from './utils' | 5 | import { checkErrors } from './utils' |
6 | import { CONFIG } from '../../initializers' | 6 | import { CONFIG } from '../../initializers' |
7 | import { logger } from '../../helpers' | 7 | import { |
8 | import { checkVideoExists, isVideoIdOrUUIDValid } from '../../helpers/custom-validators/videos' | 8 | logger, |
9 | import { isTestInstance } from '../../helpers/core-utils' | 9 | isTestInstance, |
10 | checkVideoExists, | ||
11 | isIdOrUUIDValid | ||
12 | } from '../../helpers' | ||
10 | 13 | ||
11 | const urlShouldStartWith = CONFIG.WEBSERVER.SCHEME + '://' + join(CONFIG.WEBSERVER.HOST, 'videos', 'watch') + '/' | 14 | const urlShouldStartWith = CONFIG.WEBSERVER.SCHEME + '://' + join(CONFIG.WEBSERVER.HOST, 'videos', 'watch') + '/' |
12 | const videoWatchRegex = new RegExp('([^/]+)$') | 15 | const 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) | |||
11 | const SORTABLE_VIDEO_ABUSES_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_ABUSES) | 11 | const SORTABLE_VIDEO_ABUSES_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_ABUSES) |
12 | const SORTABLE_VIDEOS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEOS) | 12 | const SORTABLE_VIDEOS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEOS) |
13 | const SORTABLE_BLACKLISTS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.BLACKLISTS) | 13 | const SORTABLE_BLACKLISTS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.BLACKLISTS) |
14 | const SORTABLE_VIDEO_CHANNELS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_CHANNELS) | ||
14 | 15 | ||
15 | const podsSortValidator = checkSort(SORTABLE_PODS_COLUMNS) | 16 | const podsSortValidator = checkSort(SORTABLE_PODS_COLUMNS) |
16 | const usersSortValidator = checkSort(SORTABLE_USERS_COLUMNS) | 17 | const usersSortValidator = checkSort(SORTABLE_USERS_COLUMNS) |
17 | const videoAbusesSortValidator = checkSort(SORTABLE_VIDEO_ABUSES_COLUMNS) | 18 | const videoAbusesSortValidator = checkSort(SORTABLE_VIDEO_ABUSES_COLUMNS) |
18 | const videosSortValidator = checkSort(SORTABLE_VIDEOS_COLUMNS) | 19 | const videosSortValidator = checkSort(SORTABLE_VIDEOS_COLUMNS) |
19 | const blacklistSortValidator = checkSort(SORTABLE_BLACKLISTS_COLUMNS) | 20 | const blacklistSortValidator = checkSort(SORTABLE_BLACKLISTS_COLUMNS) |
21 | const 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' |
18 | import { UserInstance, VideoInstance } from '../../models' | 18 | import { UserInstance, VideoInstance } from '../../models' |
19 | 19 | ||
@@ -109,7 +109,7 @@ const usersGetValidator = [ | |||
109 | ] | 109 | ] |
110 | 110 | ||
111 | const usersVideoRatingValidator = [ | 111 | const 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 | ||
4 | import { database as db } from '../../initializers/database' | 4 | import { database as db } from '../../initializers/database' |
5 | import { checkErrors } from './utils' | 5 | import { checkErrors } from './utils' |
6 | import { logger, isVideoIdOrUUIDValid, checkVideoExists } from '../../helpers' | 6 | import { logger, isIdOrUUIDValid, checkVideoExists } from '../../helpers' |
7 | 7 | ||
8 | const videosBlacklistRemoveValidator = [ | 8 | const 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 | ||
22 | const videosBlacklistAddValidator = [ | 22 | const 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 @@ | |||
1 | import { body, param } from 'express-validator/check' | ||
2 | import * as express from 'express' | ||
3 | |||
4 | import { checkErrors } from './utils' | ||
5 | import { database as db } from '../../initializers' | ||
6 | import { | ||
7 | logger, | ||
8 | isIdOrUUIDValid, | ||
9 | isVideoChannelDescriptionValid, | ||
10 | isVideoChannelNameValid, | ||
11 | checkVideoChannelExists, | ||
12 | checkVideoAuthorExists | ||
13 | } from '../../helpers' | ||
14 | |||
15 | const 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 | |||
27 | const 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 | |||
38 | const 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 | |||
67 | const 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 | |||
84 | const 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 | |||
98 | export { | ||
99 | listVideoAuthorChannelsValidator, | ||
100 | videoChannelsAddValidator, | ||
101 | videoChannelsUpdateValidator, | ||
102 | videoChannelsRemoveValidator, | ||
103 | videoChannelGetValidator | ||
104 | } | ||
105 | |||
106 | // --------------------------------------------------------------------------- | ||
107 | |||
108 | function 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 | |||
131 | function 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 | ||
25 | const videosAddValidator = [ | 26 | const 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 | ||
90 | const videosUpdateValidator = [ | 105 | const 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 | ||
124 | const videosGetValidator = [ | 139 | const 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 | ||
136 | const videosRemoveValidator = [ | 151 | const 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 | ||
164 | const videoAbuseReportValidator = [ | 179 | const 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 | ||
177 | const videoRateValidator = [ | 192 | const 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' | |||
5 | import { User as FormattedUser } from '../../../shared/models/users/user.model' | 5 | import { User as FormattedUser } from '../../../shared/models/users/user.model' |
6 | import { UserRole } from '../../../shared/models/users/user-role.type' | 6 | import { UserRole } from '../../../shared/models/users/user-role.type' |
7 | import { ResultList } from '../../../shared/models/result-list.model' | 7 | import { ResultList } from '../../../shared/models/result-list.model' |
8 | import { AuthorInstance } from '../video/author-interface' | ||
8 | 9 | ||
9 | export namespace UserMethods { | 10 | export 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 | ||
56 | export interface UserInstance extends UserClass, UserAttributes, Sequelize.Instance<UserAttributes> { | 58 | export 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 | |||
27 | let isAdmin: UserMethods.IsAdmin | 27 | let isAdmin: UserMethods.IsAdmin |
28 | let countTotal: UserMethods.CountTotal | 28 | let countTotal: UserMethods.CountTotal |
29 | let getByUsername: UserMethods.GetByUsername | 29 | let getByUsername: UserMethods.GetByUsername |
30 | let list: UserMethods.List | ||
31 | let listForApi: UserMethods.ListForApi | 30 | let listForApi: UserMethods.ListForApi |
32 | let loadById: UserMethods.LoadById | 31 | let loadById: UserMethods.LoadById |
33 | let loadByUsername: UserMethods.LoadByUsername | 32 | let loadByUsername: UserMethods.LoadByUsername |
33 | let loadByUsernameAndPopulateChannels: UserMethods.LoadByUsernameAndPopulateChannels | ||
34 | let loadByUsernameOrEmail: UserMethods.LoadByUsernameOrEmail | 34 | let loadByUsernameOrEmail: UserMethods.LoadByUsernameOrEmail |
35 | let isAbleToUploadVideo: UserMethods.IsAbleToUploadVideo | 35 | let 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 | ||
146 | toFormattedJSON = function (this: UserInstance) { | 146 | toFormattedJSON = 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 | ||
158 | isAdmin = function (this: UserInstance) { | 177 | isAdmin = 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 | ||
198 | list = function () { | ||
199 | return User.findAll() | ||
200 | } | ||
201 | |||
202 | listForApi = function (start: number, count: number, sort: string) { | 218 | listForApi = 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 | ||
217 | loadById = function (id: number) { | 234 | loadById = 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 | ||
221 | loadByUsername = function (username: string) { | 242 | loadByUsername = 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 | |||
253 | loadByUsernameAndPopulateChannels = 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 | ||
231 | loadByUsernameOrEmail = function (username: string, email: string) { | 270 | loadByUsernameOrEmail = 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 | ||
244 | function getOriginalVideoFileTotalFromUser (user: UserInstance) { | 284 | function 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' | |||
2 | import * as Promise from 'bluebird' | 2 | import * as Promise from 'bluebird' |
3 | 3 | ||
4 | import { PodInstance } from '../pod/pod-interface' | 4 | import { PodInstance } from '../pod/pod-interface' |
5 | import { RemoteVideoAuthorCreateData } from '../../../shared/models/pods/remote-video/remote-video-author-create-request.model' | ||
6 | import { VideoChannelInstance } from './video-channel-interface' | ||
5 | 7 | ||
6 | export namespace AuthorMethods { | 8 | export 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 | ||
15 | export interface AuthorClass { | 18 | export 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 | ||
19 | export interface AuthorAttributes { | 25 | export interface AuthorAttributes { |
20 | name: string | 26 | name: string |
27 | uuid?: string | ||
28 | |||
29 | podId?: number | ||
30 | userId?: number | ||
21 | } | 31 | } |
22 | 32 | ||
23 | export interface AuthorInstance extends AuthorClass, AuthorAttributes, Sequelize.Instance<AuthorAttributes> { | 33 | export 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 | ||
32 | export interface AuthorModel extends AuthorClass, Sequelize.Model<AuthorInstance, AuthorAttributes> {} | 45 | export 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 @@ | |||
1 | import * as Sequelize from 'sequelize' | 1 | import * as Sequelize from 'sequelize' |
2 | 2 | ||
3 | import { isUserUsernameValid } from '../../helpers' | 3 | import { isUserUsernameValid } from '../../helpers' |
4 | import { removeVideoAuthorToFriends } from '../../lib' | ||
4 | 5 | ||
5 | import { addMethodsToModel } from '../utils' | 6 | import { addMethodsToModel } from '../utils' |
6 | import { | 7 | import { |
@@ -11,11 +12,24 @@ import { | |||
11 | } from './author-interface' | 12 | } from './author-interface' |
12 | 13 | ||
13 | let Author: Sequelize.Model<AuthorInstance, AuthorAttributes> | 14 | let Author: Sequelize.Model<AuthorInstance, AuthorAttributes> |
14 | let findOrCreateAuthor: AuthorMethods.FindOrCreateAuthor | 15 | let loadAuthorByPodAndUUID: AuthorMethods.LoadAuthorByPodAndUUID |
16 | let load: AuthorMethods.Load | ||
17 | let loadByUUID: AuthorMethods.LoadByUUID | ||
18 | let listOwned: AuthorMethods.ListOwned | ||
19 | let isOwned: AuthorMethods.IsOwned | ||
20 | let toAddRemoteJSON: AuthorMethods.ToAddRemoteJSON | ||
15 | 21 | ||
16 | export default function defineAuthor (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { | 22 | export 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 | ||
84 | findOrCreateAuthor = function (name: string, podId: number, userId: number, transaction: Sequelize.Transaction) { | 110 | function 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 | |||
122 | toAddRemoteJSON = function (this: AuthorInstance) { | ||
123 | const json = { | ||
124 | uuid: this.uuid, | ||
125 | name: this.name | ||
126 | } | ||
127 | |||
128 | return json | ||
129 | } | ||
130 | |||
131 | isOwned = function (this: AuthorInstance) { | ||
132 | return this.podId === null | ||
133 | } | ||
134 | |||
135 | // ------------------------------ STATICS ------------------------------ | ||
136 | |||
137 | listOwned = function () { | ||
138 | const query: Sequelize.FindOptions<AuthorAttributes> = { | ||
139 | where: { | ||
140 | podId: null | ||
141 | } | ||
142 | } | ||
143 | |||
144 | return Author.findAll(query) | ||
145 | } | ||
146 | |||
147 | load = function (id: number) { | ||
148 | return Author.findById(id) | ||
149 | } | ||
150 | |||
151 | loadByUUID = 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 | |
161 | loadAuthorByPodAndUUID = 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' | |||
2 | export * from './tag-interface' | 2 | export * from './tag-interface' |
3 | export * from './video-abuse-interface' | 3 | export * from './video-abuse-interface' |
4 | export * from './video-blacklist-interface' | 4 | export * from './video-blacklist-interface' |
5 | export * from './video-channel-interface' | ||
5 | export * from './video-tag-interface' | 6 | export * from './video-tag-interface' |
6 | export * from './video-file-interface' | 7 | export * from './video-file-interface' |
7 | export * from './video-interface' | 8 | export * 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 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | import * as Promise from 'bluebird' | ||
3 | |||
4 | import { ResultList, RemoteVideoChannelCreateData, RemoteVideoChannelUpdateData } from '../../../shared' | ||
5 | |||
6 | // Don't use barrel, import just what we need | ||
7 | import { VideoChannel as FormattedVideoChannel } from '../../../shared/models/videos/video-channel.model' | ||
8 | import { AuthorInstance } from './author-interface' | ||
9 | import { VideoInstance } from './video-interface' | ||
10 | |||
11 | export 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 | |||
29 | export 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 | |||
42 | export 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 | |||
53 | export 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 | |||
64 | export 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 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | |||
3 | import { isVideoChannelNameValid, isVideoChannelDescriptionValid } from '../../helpers' | ||
4 | import { removeVideoChannelToFriends } from '../../lib' | ||
5 | |||
6 | import { addMethodsToModel, getSort } from '../utils' | ||
7 | import { | ||
8 | VideoChannelInstance, | ||
9 | VideoChannelAttributes, | ||
10 | |||
11 | VideoChannelMethods | ||
12 | } from './video-channel-interface' | ||
13 | |||
14 | let VideoChannel: Sequelize.Model<VideoChannelInstance, VideoChannelAttributes> | ||
15 | let toFormattedJSON: VideoChannelMethods.ToFormattedJSON | ||
16 | let toAddRemoteJSON: VideoChannelMethods.ToAddRemoteJSON | ||
17 | let toUpdateRemoteJSON: VideoChannelMethods.ToUpdateRemoteJSON | ||
18 | let isOwned: VideoChannelMethods.IsOwned | ||
19 | let countByAuthor: VideoChannelMethods.CountByAuthor | ||
20 | let listOwned: VideoChannelMethods.ListOwned | ||
21 | let listForApi: VideoChannelMethods.ListForApi | ||
22 | let listByAuthor: VideoChannelMethods.ListByAuthor | ||
23 | let loadByIdAndAuthor: VideoChannelMethods.LoadByIdAndAuthor | ||
24 | let loadByUUID: VideoChannelMethods.LoadByUUID | ||
25 | let loadAndPopulateAuthor: VideoChannelMethods.LoadAndPopulateAuthor | ||
26 | let loadByUUIDAndPopulateAuthor: VideoChannelMethods.LoadByUUIDAndPopulateAuthor | ||
27 | let loadByHostAndUUID: VideoChannelMethods.LoadByHostAndUUID | ||
28 | let loadAndPopulateAuthorAndVideos: VideoChannelMethods.LoadAndPopulateAuthorAndVideos | ||
29 | |||
30 | export 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 | |||
106 | isOwned = function (this: VideoChannelInstance) { | ||
107 | return this.remote === false | ||
108 | } | ||
109 | |||
110 | toFormattedJSON = 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 | |||
135 | toAddRemoteJSON = 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 | |||
148 | toUpdateRemoteJSON = 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 | |||
163 | function 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 | |||
181 | function 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 | |||
193 | countByAuthor = function (authorId: number) { | ||
194 | const query = { | ||
195 | where: { | ||
196 | authorId | ||
197 | } | ||
198 | } | ||
199 | |||
200 | return VideoChannel.count(query) | ||
201 | } | ||
202 | |||
203 | listOwned = 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 | |||
214 | listForApi = 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 | |||
233 | listByAuthor = 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 | |||
253 | loadByUUID = 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 | |||
265 | loadByHostAndUUID = 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 | |||
291 | loadByIdAndAuthor = 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 | |||
308 | loadAndPopulateAuthor = 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 | |||
321 | loadByUUIDAndPopulateAuthor = 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 | |||
337 | loadAndPopulateAuthorAndVideos = 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' | |||
6 | import { VideoFileAttributes, VideoFileInstance } from './video-file-interface' | 6 | import { 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 |
9 | import { Video as FormattedVideo } from '../../../shared/models/videos/video.model' | 9 | import { |
10 | Video as FormattedVideo, | ||
11 | VideoDetails as FormattedDetailsVideo | ||
12 | } from '../../../shared/models/videos/video.model' | ||
10 | import { RemoteVideoUpdateData } from '../../../shared/models/pods/remote-video/remote-video-update-request.model' | 13 | import { RemoteVideoUpdateData } from '../../../shared/models/pods/remote-video/remote-video-update-request.model' |
11 | import { RemoteVideoCreateData } from '../../../shared/models/pods/remote-video/remote-video-create-request.model' | 14 | import { RemoteVideoCreateData } from '../../../shared/models/pods/remote-video/remote-video-create-request.model' |
12 | import { ResultList } from '../../../shared/models/result-list.model' | 15 | import { ResultList } from '../../../shared/models/result-list.model' |
16 | import { VideoChannelInstance } from './video-channel-interface' | ||
13 | 17 | ||
14 | export namespace VideoMethods { | 18 | export 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 | |||
60 | let getTorrentFileName: VideoMethods.GetTorrentFileName | 60 | let getTorrentFileName: VideoMethods.GetTorrentFileName |
61 | let isOwned: VideoMethods.IsOwned | 61 | let isOwned: VideoMethods.IsOwned |
62 | let toFormattedJSON: VideoMethods.ToFormattedJSON | 62 | let toFormattedJSON: VideoMethods.ToFormattedJSON |
63 | let toFormattedDetailsJSON: VideoMethods.ToFormattedDetailsJSON | ||
63 | let toAddRemoteJSON: VideoMethods.ToAddRemoteJSON | 64 | let toAddRemoteJSON: VideoMethods.ToAddRemoteJSON |
64 | let toUpdateRemoteJSON: VideoMethods.ToUpdateRemoteJSON | 65 | let toUpdateRemoteJSON: VideoMethods.ToUpdateRemoteJSON |
65 | let optimizeOriginalVideofile: VideoMethods.OptimizeOriginalVideofile | 66 | let 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 | ||
284 | function associate (models) { | 286 | function 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) { | |||
439 | toFormattedJSON = function (this: VideoInstance) { | 441 | toFormattedJSON = 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 | |||
493 | toFormattedDetailsJSON = 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 | ||
743 | loadByHostAndUUID = function (fromHost: string, uuid: string) { | 807 | loadByHostAndUUID = 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 | ||
810 | loadByUUID = function (uuid: string) { | 889 | loadByUUID = 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 | ||
820 | loadAndPopulateAuthor = function (id: number) { | 902 | loadAndPopulateAuthor = 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 } |
diff --git a/shared/models/pods/remote-video/index.ts b/shared/models/pods/remote-video/index.ts index c88116849..918b9e1f6 100644 --- a/shared/models/pods/remote-video/index.ts +++ b/shared/models/pods/remote-video/index.ts | |||
@@ -1,7 +1,12 @@ | |||
1 | export * from './remote-qadu-video-request.model' | 1 | export * from './remote-qadu-video-request.model' |
2 | export * from './remote-video-author-create-request.model' | ||
3 | export * from './remote-video-author-remove-request.model' | ||
2 | export * from './remote-video-event-request.model' | 4 | export * from './remote-video-event-request.model' |
3 | export * from './remote-video-request.model' | 5 | export * from './remote-video-request.model' |
4 | export * from './remote-video-create-request.model' | 6 | export * from './remote-video-create-request.model' |
5 | export * from './remote-video-update-request.model' | 7 | export * from './remote-video-update-request.model' |
6 | export * from './remote-video-remove-request.model' | 8 | export * from './remote-video-remove-request.model' |
9 | export * from './remote-video-channel-create-request.model' | ||
10 | export * from './remote-video-channel-update-request.model' | ||
11 | export * from './remote-video-channel-remove-request.model' | ||
7 | export * from './remote-video-report-abuse-request.model' | 12 | export * from './remote-video-report-abuse-request.model' |
diff --git a/shared/models/pods/remote-video/remote-video-author-create-request.model.ts b/shared/models/pods/remote-video/remote-video-author-create-request.model.ts new file mode 100644 index 000000000..ae364d177 --- /dev/null +++ b/shared/models/pods/remote-video/remote-video-author-create-request.model.ts | |||
@@ -0,0 +1,11 @@ | |||
1 | import { RemoteVideoRequest } from './remote-video-request.model' | ||
2 | |||
3 | export interface RemoteVideoAuthorCreateData { | ||
4 | uuid: string | ||
5 | name: string | ||
6 | } | ||
7 | |||
8 | export interface RemoteVideoAuthorCreateRequest extends RemoteVideoRequest { | ||
9 | type: 'add-author' | ||
10 | data: RemoteVideoAuthorCreateData | ||
11 | } | ||
diff --git a/shared/models/pods/remote-video/remote-video-author-remove-request.model.ts b/shared/models/pods/remote-video/remote-video-author-remove-request.model.ts new file mode 100644 index 000000000..8738e92f0 --- /dev/null +++ b/shared/models/pods/remote-video/remote-video-author-remove-request.model.ts | |||
@@ -0,0 +1,10 @@ | |||
1 | import { RemoteVideoRequest } from './remote-video-request.model' | ||
2 | |||
3 | export interface RemoteVideoAuthorRemoveData { | ||
4 | uuid: string | ||
5 | } | ||
6 | |||
7 | export interface RemoteVideoAuthorRemoveRequest extends RemoteVideoRequest { | ||
8 | type: 'remove-author' | ||
9 | data: RemoteVideoAuthorRemoveData | ||
10 | } | ||
diff --git a/shared/models/pods/remote-video/remote-video-channel-create-request.model.ts b/shared/models/pods/remote-video/remote-video-channel-create-request.model.ts new file mode 100644 index 000000000..54163a2db --- /dev/null +++ b/shared/models/pods/remote-video/remote-video-channel-create-request.model.ts | |||
@@ -0,0 +1,15 @@ | |||
1 | import { RemoteVideoRequest } from './remote-video-request.model' | ||
2 | |||
3 | export interface RemoteVideoChannelCreateData { | ||
4 | uuid: string | ||
5 | name: string | ||
6 | description: string | ||
7 | createdAt: Date | ||
8 | updatedAt: Date | ||
9 | ownerUUID: string | ||
10 | } | ||
11 | |||
12 | export interface RemoteVideoChannelCreateRequest extends RemoteVideoRequest { | ||
13 | type: 'add-channel' | ||
14 | data: RemoteVideoChannelCreateData | ||
15 | } | ||
diff --git a/shared/models/pods/remote-video/remote-video-channel-remove-request.model.ts b/shared/models/pods/remote-video/remote-video-channel-remove-request.model.ts new file mode 100644 index 000000000..7aa3fe112 --- /dev/null +++ b/shared/models/pods/remote-video/remote-video-channel-remove-request.model.ts | |||
@@ -0,0 +1,10 @@ | |||
1 | import { RemoteVideoRequest } from './remote-video-request.model' | ||
2 | |||
3 | export interface RemoteVideoChannelRemoveData { | ||
4 | uuid: string | ||
5 | } | ||
6 | |||
7 | export interface RemoteVideoChannelRemoveRequest extends RemoteVideoRequest { | ||
8 | type: 'remove-channel' | ||
9 | data: RemoteVideoChannelRemoveData | ||
10 | } | ||
diff --git a/shared/models/pods/remote-video/remote-video-channel-update-request.model.ts b/shared/models/pods/remote-video/remote-video-channel-update-request.model.ts new file mode 100644 index 000000000..70250d109 --- /dev/null +++ b/shared/models/pods/remote-video/remote-video-channel-update-request.model.ts | |||
@@ -0,0 +1,15 @@ | |||
1 | import { RemoteVideoRequest } from './remote-video-request.model' | ||
2 | |||
3 | export interface RemoteVideoChannelUpdateData { | ||
4 | uuid: string | ||
5 | name: string | ||
6 | description: string | ||
7 | createdAt: Date | ||
8 | updatedAt: Date | ||
9 | ownerUUID: string | ||
10 | } | ||
11 | |||
12 | export interface RemoteVideoChannelUpdateRequest extends RemoteVideoRequest { | ||
13 | type: 'update-channel' | ||
14 | data: RemoteVideoChannelUpdateData | ||
15 | } | ||
diff --git a/shared/models/pods/remote-video/remote-video-create-request.model.ts b/shared/models/pods/remote-video/remote-video-create-request.model.ts index 98425e4d9..e00e81214 100644 --- a/shared/models/pods/remote-video/remote-video-create-request.model.ts +++ b/shared/models/pods/remote-video/remote-video-create-request.model.ts | |||
@@ -2,7 +2,7 @@ import { RemoteVideoRequest } from './remote-video-request.model' | |||
2 | 2 | ||
3 | export interface RemoteVideoCreateData { | 3 | export interface RemoteVideoCreateData { |
4 | uuid: string | 4 | uuid: string |
5 | author: string | 5 | channelUUID: string |
6 | tags: string[] | 6 | tags: string[] |
7 | name: string | 7 | name: string |
8 | category: number | 8 | category: number |
@@ -26,6 +26,6 @@ export interface RemoteVideoCreateData { | |||
26 | } | 26 | } |
27 | 27 | ||
28 | export interface RemoteVideoCreateRequest extends RemoteVideoRequest { | 28 | export interface RemoteVideoCreateRequest extends RemoteVideoRequest { |
29 | type: 'add' | 29 | type: 'add-video' |
30 | data: RemoteVideoCreateData | 30 | data: RemoteVideoCreateData |
31 | } | 31 | } |
diff --git a/shared/models/pods/remote-video/remote-video-remove-request.model.ts b/shared/models/pods/remote-video/remote-video-remove-request.model.ts index 0686dc8ab..79a5e0a5f 100644 --- a/shared/models/pods/remote-video/remote-video-remove-request.model.ts +++ b/shared/models/pods/remote-video/remote-video-remove-request.model.ts | |||
@@ -5,6 +5,6 @@ export interface RemoteVideoRemoveData { | |||
5 | } | 5 | } |
6 | 6 | ||
7 | export interface RemoteVideoRemoveRequest extends RemoteVideoRequest { | 7 | export interface RemoteVideoRemoveRequest extends RemoteVideoRequest { |
8 | type: 'remove' | 8 | type: 'remove-video' |
9 | data: RemoteVideoRemoveData | 9 | data: RemoteVideoRemoveData |
10 | } | 10 | } |
diff --git a/shared/models/pods/remote-video/remote-video-request.model.ts b/shared/models/pods/remote-video/remote-video-request.model.ts index e5052a23d..56f8136c2 100644 --- a/shared/models/pods/remote-video/remote-video-request.model.ts +++ b/shared/models/pods/remote-video/remote-video-request.model.ts | |||
@@ -1,4 +1,9 @@ | |||
1 | export interface RemoteVideoRequest { | 1 | export interface RemoteVideoRequest { |
2 | type: 'add' | 'update' | 'remove' | 'report-abuse' | 2 | type: RemoteVideoRequestType |
3 | data: any | 3 | data: any |
4 | } | 4 | } |
5 | |||
6 | export type RemoteVideoRequestType = 'add-video' | 'update-video' | 'remove-video' | | ||
7 | 'add-channel' | 'update-channel' | 'remove-channel' | | ||
8 | 'report-abuse' | | ||
9 | 'add-author' | 'remove-author' | ||
diff --git a/shared/models/pods/remote-video/remote-video-update-request.model.ts b/shared/models/pods/remote-video/remote-video-update-request.model.ts index 7f34a30ae..90c42fc28 100644 --- a/shared/models/pods/remote-video/remote-video-update-request.model.ts +++ b/shared/models/pods/remote-video/remote-video-update-request.model.ts | |||
@@ -1,3 +1,5 @@ | |||
1 | import { RemoteVideoRequest } from './remote-video-request.model' | ||
2 | |||
1 | export interface RemoteVideoUpdateData { | 3 | export interface RemoteVideoUpdateData { |
2 | uuid: string | 4 | uuid: string |
3 | tags: string[] | 5 | tags: string[] |
@@ -21,7 +23,7 @@ export interface RemoteVideoUpdateData { | |||
21 | }[] | 23 | }[] |
22 | } | 24 | } |
23 | 25 | ||
24 | export interface RemoteVideoUpdateRequest { | 26 | export interface RemoteVideoUpdateRequest extends RemoteVideoRequest { |
25 | type: 'update' | 27 | type: 'update-video' |
26 | data: RemoteVideoUpdateData | 28 | data: RemoteVideoUpdateData |
27 | } | 29 | } |
diff --git a/shared/models/users/user.model.ts b/shared/models/users/user.model.ts index 867a6dde5..175e72f28 100644 --- a/shared/models/users/user.model.ts +++ b/shared/models/users/user.model.ts | |||
@@ -1,4 +1,5 @@ | |||
1 | import { UserRole } from './user-role.type' | 1 | import { UserRole } from './user-role.type' |
2 | import { VideoChannel } from '../videos/video-channel.model' | ||
2 | 3 | ||
3 | export interface User { | 4 | export interface User { |
4 | id: number | 5 | id: number |
@@ -7,5 +8,10 @@ export interface User { | |||
7 | displayNSFW: boolean | 8 | displayNSFW: boolean |
8 | role: UserRole | 9 | role: UserRole |
9 | videoQuota: number | 10 | videoQuota: number |
10 | createdAt: Date | 11 | createdAt: Date, |
12 | author: { | ||
13 | id: number | ||
14 | uuid: string | ||
15 | } | ||
16 | videoChannels?: VideoChannel[] | ||
11 | } | 17 | } |
diff --git a/shared/models/videos/index.ts b/shared/models/videos/index.ts index 35144dbad..2a3912f06 100644 --- a/shared/models/videos/index.ts +++ b/shared/models/videos/index.ts | |||
@@ -4,6 +4,9 @@ export * from './user-video-rate.type' | |||
4 | export * from './video-abuse-create.model' | 4 | export * from './video-abuse-create.model' |
5 | export * from './video-abuse.model' | 5 | export * from './video-abuse.model' |
6 | export * from './video-blacklist.model' | 6 | export * from './video-blacklist.model' |
7 | export * from './video-channel-create.model' | ||
8 | export * from './video-channel-update.model' | ||
9 | export * from './video-channel.model' | ||
7 | export * from './video-create.model' | 10 | export * from './video-create.model' |
8 | export * from './video-rate.type' | 11 | export * from './video-rate.type' |
9 | export * from './video-resolution.enum' | 12 | export * from './video-resolution.enum' |
diff --git a/shared/models/videos/video-channel-create.model.ts b/shared/models/videos/video-channel-create.model.ts new file mode 100644 index 000000000..f309c8f45 --- /dev/null +++ b/shared/models/videos/video-channel-create.model.ts | |||
@@ -0,0 +1,4 @@ | |||
1 | export interface VideoChannelCreate { | ||
2 | name: string | ||
3 | description?: string | ||
4 | } | ||
diff --git a/shared/models/videos/video-channel-update.model.ts b/shared/models/videos/video-channel-update.model.ts new file mode 100644 index 000000000..4e98e39a8 --- /dev/null +++ b/shared/models/videos/video-channel-update.model.ts | |||
@@ -0,0 +1,4 @@ | |||
1 | export interface VideoChannelUpdate { | ||
2 | name: string | ||
3 | description: string | ||
4 | } | ||
diff --git a/shared/models/videos/video-channel.model.ts b/shared/models/videos/video-channel.model.ts new file mode 100644 index 000000000..ee56c54b6 --- /dev/null +++ b/shared/models/videos/video-channel.model.ts | |||
@@ -0,0 +1,15 @@ | |||
1 | import { Video } from './video.model' | ||
2 | |||
3 | export interface VideoChannel { | ||
4 | id: number | ||
5 | name: string | ||
6 | description: string | ||
7 | isLocal: boolean | ||
8 | createdAt: Date | string | ||
9 | updatedAt: Date | string | ||
10 | owner?: { | ||
11 | name: string | ||
12 | uuid: string | ||
13 | } | ||
14 | videos?: Video[] | ||
15 | } | ||
diff --git a/shared/models/videos/video-create.model.ts b/shared/models/videos/video-create.model.ts index 5c0b498ce..4d0e83520 100644 --- a/shared/models/videos/video-create.model.ts +++ b/shared/models/videos/video-create.model.ts | |||
@@ -3,6 +3,7 @@ export interface VideoCreate { | |||
3 | licence: number | 3 | licence: number |
4 | language: number | 4 | language: number |
5 | description: string | 5 | description: string |
6 | channelId: number | ||
6 | nsfw: boolean | 7 | nsfw: boolean |
7 | name: string | 8 | name: string |
8 | tags: string[] | 9 | tags: string[] |
diff --git a/shared/models/videos/video.model.ts b/shared/models/videos/video.model.ts index 8e47ac069..32463933d 100644 --- a/shared/models/videos/video.model.ts +++ b/shared/models/videos/video.model.ts | |||
@@ -1,3 +1,5 @@ | |||
1 | import { VideoChannel } from './video-channel.model' | ||
2 | |||
1 | export interface VideoFile { | 3 | export interface VideoFile { |
2 | magnetUri: string | 4 | magnetUri: string |
3 | resolution: number | 5 | resolution: number |
@@ -32,5 +34,9 @@ export interface Video { | |||
32 | likes: number | 34 | likes: number |
33 | dislikes: number | 35 | dislikes: number |
34 | nsfw: boolean | 36 | nsfw: boolean |
37 | } | ||
38 | |||
39 | export interface VideoDetails extends Video { | ||
40 | channel: VideoChannel | ||
35 | files: VideoFile[] | 41 | files: VideoFile[] |
36 | } | 42 | } |