Add video channels
authorChocobozzz <florian.bigard@gmail.com>
Tue, 24 Oct 2017 17:41:09 +0000 (19:41 +0200)
committerChocobozzz <florian.bigard@gmail.com>
Thu, 26 Oct 2017 07:11:38 +0000 (09:11 +0200)
56 files changed:
server/controllers/api/remote/pods.ts
server/controllers/api/remote/videos.ts
server/controllers/api/users.ts
server/controllers/api/videos/channel.ts [new file with mode: 0644]
server/controllers/api/videos/index.ts
server/controllers/services.ts
server/helpers/custom-validators/index.ts
server/helpers/custom-validators/misc.ts
server/helpers/custom-validators/remote/videos.ts
server/helpers/custom-validators/video-authors.ts [new file with mode: 0644]
server/helpers/custom-validators/video-channels.ts [new file with mode: 0644]
server/helpers/custom-validators/videos.ts
server/initializers/constants.ts
server/initializers/database.ts
server/initializers/installer.ts
server/lib/cache/videos-preview-cache.ts
server/lib/friends.ts
server/lib/index.ts
server/lib/user.ts [new file with mode: 0644]
server/lib/video-channel.ts [new file with mode: 0644]
server/middlewares/sort.ts
server/middlewares/validators/index.ts
server/middlewares/validators/oembed.ts
server/middlewares/validators/sort.ts
server/middlewares/validators/users.ts
server/middlewares/validators/video-blacklist.ts
server/middlewares/validators/video-channels.ts [new file with mode: 0644]
server/middlewares/validators/videos.ts
server/models/oauth/oauth-token.ts
server/models/request/request-video-event.ts
server/models/user/user-interface.ts
server/models/user/user.ts
server/models/video/author-interface.ts
server/models/video/author.ts
server/models/video/index.ts
server/models/video/video-channel-interface.ts [new file with mode: 0644]
server/models/video/video-channel.ts [new file with mode: 0644]
server/models/video/video-interface.ts
server/models/video/video.ts
shared/models/pods/remote-video/index.ts
shared/models/pods/remote-video/remote-video-author-create-request.model.ts [new file with mode: 0644]
shared/models/pods/remote-video/remote-video-author-remove-request.model.ts [new file with mode: 0644]
shared/models/pods/remote-video/remote-video-channel-create-request.model.ts [new file with mode: 0644]
shared/models/pods/remote-video/remote-video-channel-remove-request.model.ts [new file with mode: 0644]
shared/models/pods/remote-video/remote-video-channel-update-request.model.ts [new file with mode: 0644]
shared/models/pods/remote-video/remote-video-create-request.model.ts
shared/models/pods/remote-video/remote-video-remove-request.model.ts
shared/models/pods/remote-video/remote-video-request.model.ts
shared/models/pods/remote-video/remote-video-update-request.model.ts
shared/models/users/user.model.ts
shared/models/videos/index.ts
shared/models/videos/video-channel-create.model.ts [new file with mode: 0644]
shared/models/videos/video-channel-update.model.ts [new file with mode: 0644]
shared/models/videos/video-channel.model.ts [new file with mode: 0644]
shared/models/videos/video-create.model.ts
shared/models/videos/video.model.ts

index 6f7b5f6511a2aa0d5b297102d294570254ace4a8..a62b9c6846a6abb7ec58b80987ee03c00079e411 100644 (file)
@@ -7,7 +7,7 @@ import {
   setBodyHostPort,
   remotePodsAddValidator
 } from '../../../middlewares'
-import { sendOwnedVideosToPod } from '../../../lib'
+import { sendOwnedDataToPod } from '../../../lib'
 import { getMyPublicCert, getFormattedObjects } from '../../../helpers'
 import { CONFIG } from '../../../initializers'
 import { PodInstance } from '../../../models'
@@ -43,7 +43,7 @@ function addPods (req: express.Request, res: express.Response, next: express.Nex
   const pod = db.Pod.build(information)
   pod.save()
      .then(podCreated => {
-       return sendOwnedVideosToPod(podCreated.id)
+       return sendOwnedDataToPod(podCreated.id)
      })
      .then(() => {
        return getMyPublicCert()
index 23023211f8a877d1cb7495a82632964617419e01..c8f531490c371e6d2b34067068c76da058e0f40c 100644 (file)
@@ -1,5 +1,6 @@
 import * as express from 'express'
 import * as Promise from 'bluebird'
+import * as Sequelize from 'sequelize'
 
 import { database as db } from '../../../initializers/database'
 import {
@@ -27,17 +28,28 @@ import {
   RemoteQaduVideoRequest,
   RemoteQaduVideoData,
   RemoteVideoEventRequest,
-  RemoteVideoEventData
+  RemoteVideoEventData,
+  RemoteVideoChannelCreateData,
+  RemoteVideoChannelUpdateData,
+  RemoteVideoChannelRemoveData,
+  RemoteVideoAuthorRemoveData,
+  RemoteVideoAuthorCreateData
 } from '../../../../shared'
 
 const ENDPOINT_ACTIONS = REQUEST_ENDPOINT_ACTIONS[REQUEST_ENDPOINTS.VIDEOS]
 
 // Functions to call when processing a remote request
+// FIXME: use RemoteVideoRequestType as id type
 const functionsHash: { [ id: string ]: (...args) => Promise<any> } = {}
-functionsHash[ENDPOINT_ACTIONS.ADD] = addRemoteVideoRetryWrapper
-functionsHash[ENDPOINT_ACTIONS.UPDATE] = updateRemoteVideoRetryWrapper
-functionsHash[ENDPOINT_ACTIONS.REMOVE] = removeRemoteVideo
-functionsHash[ENDPOINT_ACTIONS.REPORT_ABUSE] = reportAbuseRemoteVideo
+functionsHash[ENDPOINT_ACTIONS.ADD_VIDEO] = addRemoteVideoRetryWrapper
+functionsHash[ENDPOINT_ACTIONS.UPDATE_VIDEO] = updateRemoteVideoRetryWrapper
+functionsHash[ENDPOINT_ACTIONS.REMOVE_VIDEO] = removeRemoteVideoRetryWrapper
+functionsHash[ENDPOINT_ACTIONS.ADD_CHANNEL] = addRemoteVideoChannelRetryWrapper
+functionsHash[ENDPOINT_ACTIONS.UPDATE_CHANNEL] = updateRemoteVideoChannelRetryWrapper
+functionsHash[ENDPOINT_ACTIONS.REMOVE_CHANNEL] = removeRemoteVideoChannelRetryWrapper
+functionsHash[ENDPOINT_ACTIONS.REPORT_ABUSE] = reportAbuseRemoteVideoRetryWrapper
+functionsHash[ENDPOINT_ACTIONS.ADD_AUTHOR] = addRemoteVideoAuthorRetryWrapper
+functionsHash[ENDPOINT_ACTIONS.REMOVE_AUTHOR] = removeRemoteVideoAuthorRetryWrapper
 
 const remoteVideosRouter = express.Router()
 
@@ -133,7 +145,7 @@ function processVideosEventsRetryWrapper (eventData: RemoteVideoEventData, fromP
 function processVideosEvents (eventData: RemoteVideoEventData, fromPod: PodInstance) {
 
   return db.sequelize.transaction(t => {
-    return fetchVideoByUUID(eventData.uuid)
+    return fetchVideoByUUID(eventData.uuid, t)
       .then(videoInstance => {
         const options = { transaction: t }
 
@@ -196,7 +208,7 @@ function quickAndDirtyUpdateVideo (videoData: RemoteQaduVideoData, fromPod: PodI
   let videoUUID = ''
 
   return db.sequelize.transaction(t => {
-    return fetchVideoByHostAndUUID(fromPod.host, videoData.uuid)
+    return fetchVideoByHostAndUUID(fromPod.host, videoData.uuid, t)
       .then(videoInstance => {
         const options = { transaction: t }
 
@@ -239,22 +251,16 @@ function addRemoteVideo (videoToCreateData: RemoteVideoCreateData, fromPod: PodI
       .then(video => {
         if (video) throw new Error('UUID already exists.')
 
-        return undefined
+        return db.VideoChannel.loadByHostAndUUID(fromPod.host, videoToCreateData.channelUUID, t)
       })
-      .then(() => {
-        const name = videoToCreateData.author
-        const podId = fromPod.id
-        // This author is from another pod so we do not associate a user
-        const userId = null
+      .then(videoChannel => {
+        if (!videoChannel) throw new Error('Video channel ' + videoToCreateData.channelUUID + ' not found.')
 
-        return db.Author.findOrCreateAuthor(name, podId, userId, t)
-      })
-      .then(author => {
         const tags = videoToCreateData.tags
 
-        return db.Tag.findOrCreateTags(tags, t).then(tagInstances => ({ author, tagInstances }))
+        return db.Tag.findOrCreateTags(tags, t).then(tagInstances => ({ videoChannel, tagInstances }))
       })
-      .then(({ author, tagInstances }) => {
+      .then(({ videoChannel, tagInstances }) => {
         const videoData = {
           name: videoToCreateData.name,
           uuid: videoToCreateData.uuid,
@@ -263,7 +269,7 @@ function addRemoteVideo (videoToCreateData: RemoteVideoCreateData, fromPod: PodI
           language: videoToCreateData.language,
           nsfw: videoToCreateData.nsfw,
           description: videoToCreateData.description,
-          authorId: author.id,
+          channelId: videoChannel.id,
           duration: videoToCreateData.duration,
           createdAt: videoToCreateData.createdAt,
           // FIXME: updatedAt does not seems to be considered by Sequelize
@@ -336,7 +342,7 @@ function updateRemoteVideo (videoAttributesToUpdate: RemoteVideoUpdateData, from
   logger.debug('Updating remote video "%s".', videoAttributesToUpdate.uuid)
 
   return db.sequelize.transaction(t => {
-    return fetchVideoByHostAndUUID(fromPod.host, videoAttributesToUpdate.uuid)
+    return fetchVideoByHostAndUUID(fromPod.host, videoAttributesToUpdate.uuid, t)
       .then(videoInstance => {
         const tags = videoAttributesToUpdate.tags
 
@@ -365,7 +371,7 @@ function updateRemoteVideo (videoAttributesToUpdate: RemoteVideoUpdateData, from
 
         // Remove old video files
         videoInstance.VideoFiles.forEach(videoFile => {
-          tasks.push(videoFile.destroy())
+          tasks.push(videoFile.destroy({ transaction: t }))
         })
 
         return Promise.all(tasks).then(() => ({ tagInstances, videoInstance }))
@@ -404,37 +410,231 @@ function updateRemoteVideo (videoAttributesToUpdate: RemoteVideoUpdateData, from
   })
 }
 
+function removeRemoteVideoRetryWrapper (videoToRemoveData: RemoteVideoRemoveData, fromPod: PodInstance) {
+  const options = {
+    arguments: [ videoToRemoveData, fromPod ],
+    errorMessage: 'Cannot remove the remote video channel with many retries.'
+  }
+
+  return retryTransactionWrapper(removeRemoteVideo, options)
+}
+
 function removeRemoteVideo (videoToRemoveData: RemoteVideoRemoveData, fromPod: PodInstance) {
-  // We need the instance because we have to remove some other stuffs (thumbnail etc)
-  return fetchVideoByHostAndUUID(fromPod.host, videoToRemoveData.uuid)
-    .then(video => {
-      logger.debug('Removing remote video with uuid %s.', video.uuid)
-      return video.destroy()
-    })
+  logger.debug('Removing remote video "%s".', videoToRemoveData.uuid)
+
+  return db.sequelize.transaction(t => {
+    // We need the instance because we have to remove some other stuffs (thumbnail etc)
+    return fetchVideoByHostAndUUID(fromPod.host, videoToRemoveData.uuid, t)
+      .then(video => video.destroy({ transaction: t }))
+  })
+  .then(() => logger.info('Remote video with uuid %s removed.', videoToRemoveData.uuid))
+  .catch(err => {
+    logger.debug('Cannot remove the remote video.', err)
+    throw err
+  })
+}
+
+function addRemoteVideoAuthorRetryWrapper (authorToCreateData: RemoteVideoAuthorCreateData, fromPod: PodInstance) {
+  const options = {
+    arguments: [ authorToCreateData, fromPod ],
+    errorMessage: 'Cannot insert the remote video author with many retries.'
+  }
+
+  return retryTransactionWrapper(addRemoteVideoAuthor, options)
+}
+
+function addRemoteVideoAuthor (authorToCreateData: RemoteVideoAuthorCreateData, fromPod: PodInstance) {
+  logger.debug('Adding remote video author "%s".', authorToCreateData.uuid)
+
+  return db.sequelize.transaction(t => {
+    return db.Author.loadAuthorByPodAndUUID(authorToCreateData.uuid, fromPod.id, t)
+      .then(author => {
+        if (author) throw new Error('UUID already exists.')
+
+        return undefined
+      })
+      .then(() => {
+        const videoAuthorData = {
+          name: authorToCreateData.name,
+          uuid: authorToCreateData.uuid,
+          userId: null, // Not on our pod
+          podId: fromPod.id
+        }
+
+        const author = db.Author.build(videoAuthorData)
+        return author.save({ transaction: t })
+      })
+  })
+    .then(() => logger.info('Remote video author with uuid %s inserted.', authorToCreateData.uuid))
     .catch(err => {
-      logger.debug('Could not fetch remote video.', { host: fromPod.host, uuid: videoToRemoveData.uuid, error: err.stack })
+      logger.debug('Cannot insert the remote video author.', err)
+      throw err
     })
 }
 
+function removeRemoteVideoAuthorRetryWrapper (authorAttributesToRemove: RemoteVideoAuthorRemoveData, fromPod: PodInstance) {
+  const options = {
+    arguments: [ authorAttributesToRemove, fromPod ],
+    errorMessage: 'Cannot remove the remote video author with many retries.'
+  }
+
+  return retryTransactionWrapper(removeRemoteVideoAuthor, options)
+}
+
+function removeRemoteVideoAuthor (authorAttributesToRemove: RemoteVideoAuthorRemoveData, fromPod: PodInstance) {
+  logger.debug('Removing remote video author "%s".', authorAttributesToRemove.uuid)
+
+  return db.sequelize.transaction(t => {
+    return db.Author.loadAuthorByPodAndUUID(authorAttributesToRemove.uuid, fromPod.id, t)
+      .then(videoAuthor => videoAuthor.destroy({ transaction: t }))
+  })
+  .then(() => logger.info('Remote video author with uuid %s removed.', authorAttributesToRemove.uuid))
+  .catch(err => {
+    logger.debug('Cannot remove the remote video author.', err)
+    throw err
+  })
+}
+
+function addRemoteVideoChannelRetryWrapper (videoChannelToCreateData: RemoteVideoChannelCreateData, fromPod: PodInstance) {
+  const options = {
+    arguments: [ videoChannelToCreateData, fromPod ],
+    errorMessage: 'Cannot insert the remote video channel with many retries.'
+  }
+
+  return retryTransactionWrapper(addRemoteVideoChannel, options)
+}
+
+function addRemoteVideoChannel (videoChannelToCreateData: RemoteVideoChannelCreateData, fromPod: PodInstance) {
+  logger.debug('Adding remote video channel "%s".', videoChannelToCreateData.uuid)
+
+  return db.sequelize.transaction(t => {
+    return db.VideoChannel.loadByUUID(videoChannelToCreateData.uuid)
+      .then(videoChannel => {
+        if (videoChannel) throw new Error('UUID already exists.')
+
+        return undefined
+      })
+      .then(() => {
+        const authorUUID = videoChannelToCreateData.ownerUUID
+        const podId = fromPod.id
+
+        return db.Author.loadAuthorByPodAndUUID(authorUUID, podId, t)
+      })
+      .then(author => {
+        if (!author) throw new Error('Unknown author UUID.')
+
+        const videoChannelData = {
+          name: videoChannelToCreateData.name,
+          description: videoChannelToCreateData.description,
+          uuid: videoChannelToCreateData.uuid,
+          createdAt: videoChannelToCreateData.createdAt,
+          updatedAt: videoChannelToCreateData.updatedAt,
+          remote: true,
+          authorId: author.id
+        }
+
+        const videoChannel = db.VideoChannel.build(videoChannelData)
+        return videoChannel.save({ transaction: t })
+      })
+  })
+  .then(() => logger.info('Remote video channel with uuid %s inserted.', videoChannelToCreateData.uuid))
+  .catch(err => {
+    logger.debug('Cannot insert the remote video channel.', err)
+    throw err
+  })
+}
+
+function updateRemoteVideoChannelRetryWrapper (videoChannelAttributesToUpdate: RemoteVideoChannelUpdateData, fromPod: PodInstance) {
+  const options = {
+    arguments: [ videoChannelAttributesToUpdate, fromPod ],
+    errorMessage: 'Cannot update the remote video channel with many retries.'
+  }
+
+  return retryTransactionWrapper(updateRemoteVideoChannel, options)
+}
+
+function updateRemoteVideoChannel (videoChannelAttributesToUpdate: RemoteVideoChannelUpdateData, fromPod: PodInstance) {
+  logger.debug('Updating remote video channel "%s".', videoChannelAttributesToUpdate.uuid)
+
+  return db.sequelize.transaction(t => {
+    return fetchVideoChannelByHostAndUUID(fromPod.host, videoChannelAttributesToUpdate.uuid, t)
+      .then(videoChannelInstance => {
+        const options = { transaction: t }
+
+        videoChannelInstance.set('name', videoChannelAttributesToUpdate.name)
+        videoChannelInstance.set('description', videoChannelAttributesToUpdate.description)
+        videoChannelInstance.set('createdAt', videoChannelAttributesToUpdate.createdAt)
+        videoChannelInstance.set('updatedAt', videoChannelAttributesToUpdate.updatedAt)
+
+        return videoChannelInstance.save(options)
+      })
+  })
+  .then(() => logger.info('Remote video channel with uuid %s updated', videoChannelAttributesToUpdate.uuid))
+  .catch(err => {
+    // This is just a debug because we will retry the insert
+    logger.debug('Cannot update the remote video channel.', err)
+    throw err
+  })
+}
+
+function removeRemoteVideoChannelRetryWrapper (videoChannelAttributesToRemove: RemoteVideoChannelRemoveData, fromPod: PodInstance) {
+  const options = {
+    arguments: [ videoChannelAttributesToRemove, fromPod ],
+    errorMessage: 'Cannot remove the remote video channel with many retries.'
+  }
+
+  return retryTransactionWrapper(removeRemoteVideoChannel, options)
+}
+
+function removeRemoteVideoChannel (videoChannelAttributesToRemove: RemoteVideoChannelRemoveData, fromPod: PodInstance) {
+  logger.debug('Removing remote video channel "%s".', videoChannelAttributesToRemove.uuid)
+
+  return db.sequelize.transaction(t => {
+    return fetchVideoChannelByHostAndUUID(fromPod.host, videoChannelAttributesToRemove.uuid, t)
+      .then(videoChannel => videoChannel.destroy({ transaction: t }))
+  })
+  .then(() => logger.info('Remote video channel with uuid %s removed.', videoChannelAttributesToRemove.uuid))
+  .catch(err => {
+    logger.debug('Cannot remove the remote video channel.', err)
+    throw err
+  })
+}
+
+function reportAbuseRemoteVideoRetryWrapper (reportData: RemoteVideoReportAbuseData, fromPod: PodInstance) {
+  const options = {
+    arguments: [ reportData, fromPod ],
+    errorMessage: 'Cannot create remote abuse video with many retries.'
+  }
+
+  return retryTransactionWrapper(reportAbuseRemoteVideo, options)
+}
+
 function reportAbuseRemoteVideo (reportData: RemoteVideoReportAbuseData, fromPod: PodInstance) {
-  return fetchVideoByUUID(reportData.videoUUID)
-    .then(video => {
-      logger.debug('Reporting remote abuse for video %s.', video.id)
+  logger.debug('Reporting remote abuse for video %s.', reportData.videoUUID)
 
-      const videoAbuseData = {
-        reporterUsername: reportData.reporterUsername,
-        reason: reportData.reportReason,
-        reporterPodId: fromPod.id,
-        videoId: video.id
-      }
+  return db.sequelize.transaction(t => {
+    return fetchVideoByUUID(reportData.videoUUID, t)
+      .then(video => {
+        const videoAbuseData = {
+          reporterUsername: reportData.reporterUsername,
+          reason: reportData.reportReason,
+          reporterPodId: fromPod.id,
+          videoId: video.id
+        }
 
-      return db.VideoAbuse.create(videoAbuseData)
-    })
-    .catch(err => logger.error('Cannot create remote abuse video.', err))
+        return db.VideoAbuse.create(videoAbuseData)
+      })
+  })
+  .then(() => logger.info('Remote abuse for video uuid %s created', reportData.videoUUID))
+  .catch(err => {
+    // This is just a debug because we will retry the insert
+    logger.debug('Cannot create remote abuse video', err)
+    throw err
+  })
 }
 
-function fetchVideoByUUID (id: string) {
-  return db.Video.loadByUUID(id)
+function fetchVideoByUUID (id: string, t: Sequelize.Transaction) {
+  return db.Video.loadByUUID(id, t)
     .then(video => {
       if (!video) throw new Error('Video not found')
 
@@ -446,8 +646,8 @@ function fetchVideoByUUID (id: string) {
     })
 }
 
-function fetchVideoByHostAndUUID (podHost: string, uuid: string) {
-  return db.Video.loadByHostAndUUID(podHost, uuid)
+function fetchVideoByHostAndUUID (podHost: string, uuid: string, t: Sequelize.Transaction) {
+  return db.Video.loadByHostAndUUID(podHost, uuid, t)
     .then(video => {
       if (!video) throw new Error('Video not found')
 
@@ -458,3 +658,16 @@ function fetchVideoByHostAndUUID (podHost: string, uuid: string) {
       throw err
     })
 }
+
+function fetchVideoChannelByHostAndUUID (podHost: string, uuid: string, t: Sequelize.Transaction) {
+  return db.VideoChannel.loadByHostAndUUID(podHost, uuid, t)
+    .then(videoChannel => {
+      if (!videoChannel) throw new Error('Video channel not found')
+
+      return videoChannel
+    })
+    .catch(err => {
+      logger.error('Cannot load video channel from host and uuid.', { error: err.stack, podHost, uuid })
+      throw err
+    })
+}
index 1ecaaf93f7a04c60dd0180b46be92933dab0f891..6576e4333f19d75688183f6aa7f28209c8107b35 100644 (file)
@@ -2,7 +2,7 @@ import * as express from 'express'
 
 import { database as db } from '../../initializers/database'
 import { USER_ROLES, CONFIG } from '../../initializers'
-import { logger, getFormattedObjects } from '../../helpers'
+import { logger, getFormattedObjects, retryTransactionWrapper } from '../../helpers'
 import {
   authenticate,
   ensureIsAdmin,
@@ -26,6 +26,7 @@ import {
   UserUpdate,
   UserUpdateMe
 } from '../../../shared'
+import { createUserAuthorAndChannel } from '../../lib'
 import { UserInstance } from '../../models'
 
 const usersRouter = express.Router()
@@ -58,7 +59,7 @@ usersRouter.post('/',
   authenticate,
   ensureIsAdmin,
   usersAddValidator,
-  createUser
+  createUserRetryWrapper
 )
 
 usersRouter.post('/register',
@@ -98,9 +99,22 @@ export {
 
 // ---------------------------------------------------------------------------
 
+function createUserRetryWrapper (req: express.Request, res: express.Response, next: express.NextFunction) {
+  const options = {
+    arguments: [ req, res ],
+    errorMessage: 'Cannot insert the user with many retries.'
+  }
+
+  retryTransactionWrapper(createUser, options)
+    .then(() => {
+      // TODO : include Location of the new user -> 201
+      res.type('json').status(204).end()
+    })
+    .catch(err => next(err))
+}
+
 function createUser (req: express.Request, res: express.Response, next: express.NextFunction) {
   const body: UserCreate = req.body
-
   const user = db.User.build({
     username: body.username,
     password: body.password,
@@ -110,9 +124,12 @@ function createUser (req: express.Request, res: express.Response, next: express.
     videoQuota: body.videoQuota
   })
 
-  user.save()
-    .then(() => res.type('json').status(204).end())
-    .catch(err => next(err))
+  return createUserAuthorAndChannel(user)
+    .then(() => logger.info('User %s with its channel and author created.', body.username))
+    .catch((err: Error) => {
+      logger.debug('Cannot insert the user.', err)
+      throw err
+    })
 }
 
 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
     videoQuota: CONFIG.USER.VIDEO_QUOTA
   })
 
-  user.save()
+  return createUserAuthorAndChannel(user)
     .then(() => res.type('json').status(204).end())
     .catch(err => next(err))
 }
 
 function getUserInformation (req: express.Request, res: express.Response, next: express.NextFunction) {
-  db.User.loadByUsername(res.locals.oauth.token.user.username)
+  db.User.loadByUsernameAndPopulateChannels(res.locals.oauth.token.user.username)
     .then(user => res.json(user.toFormattedJSON()))
     .catch(err => next(err))
 }
diff --git a/server/controllers/api/videos/channel.ts b/server/controllers/api/videos/channel.ts
new file mode 100644 (file)
index 0000000..630fc4f
--- /dev/null
@@ -0,0 +1,196 @@
+import * as express from 'express'
+
+import { database as db } from '../../../initializers'
+import {
+  logger,
+  getFormattedObjects,
+  retryTransactionWrapper
+} from '../../../helpers'
+import {
+  authenticate,
+  paginationValidator,
+  videoChannelsSortValidator,
+  videoChannelsAddValidator,
+  setVideoChannelsSort,
+  setPagination,
+  videoChannelsRemoveValidator,
+  videoChannelGetValidator,
+  videoChannelsUpdateValidator,
+  listVideoAuthorChannelsValidator
+} from '../../../middlewares'
+import {
+  createVideoChannel,
+  updateVideoChannelToFriends
+} from '../../../lib'
+import { VideoChannelInstance, AuthorInstance } from '../../../models'
+import { VideoChannelCreate, VideoChannelUpdate } from '../../../../shared'
+
+const videoChannelRouter = express.Router()
+
+videoChannelRouter.get('/channels',
+  paginationValidator,
+  videoChannelsSortValidator,
+  setVideoChannelsSort,
+  setPagination,
+  listVideoChannels
+)
+
+videoChannelRouter.get('/authors/:authorId/channels',
+  listVideoAuthorChannelsValidator,
+  listVideoAuthorChannels
+)
+
+videoChannelRouter.post('/channels',
+  authenticate,
+  videoChannelsAddValidator,
+  addVideoChannelRetryWrapper
+)
+
+videoChannelRouter.put('/channels/:id',
+  authenticate,
+  videoChannelsUpdateValidator,
+  updateVideoChannelRetryWrapper
+)
+
+videoChannelRouter.delete('/channels/:id',
+  authenticate,
+  videoChannelsRemoveValidator,
+  removeVideoChannelRetryWrapper
+)
+
+videoChannelRouter.get('/channels/:id',
+  videoChannelGetValidator,
+  getVideoChannel
+)
+
+// ---------------------------------------------------------------------------
+
+export {
+  videoChannelRouter
+}
+
+// ---------------------------------------------------------------------------
+
+function listVideoChannels (req: express.Request, res: express.Response, next: express.NextFunction) {
+  db.VideoChannel.listForApi(req.query.start, req.query.count, req.query.sort)
+    .then(result => res.json(getFormattedObjects(result.data, result.total)))
+    .catch(err => next(err))
+}
+
+function listVideoAuthorChannels (req: express.Request, res: express.Response, next: express.NextFunction) {
+  db.VideoChannel.listByAuthor(res.locals.author.id)
+    .then(result => res.json(getFormattedObjects(result.data, result.total)))
+    .catch(err => next(err))
+}
+
+// Wrapper to video channel add that retry the function if there is a database error
+// We need this because we run the transaction in SERIALIZABLE isolation that can fail
+function addVideoChannelRetryWrapper (req: express.Request, res: express.Response, next: express.NextFunction) {
+  const options = {
+    arguments: [ req, res ],
+    errorMessage: 'Cannot insert the video video channel with many retries.'
+  }
+
+  retryTransactionWrapper(addVideoChannel, options)
+    .then(() => {
+      // TODO : include Location of the new video channel -> 201
+      res.type('json').status(204).end()
+    })
+    .catch(err => next(err))
+}
+
+function addVideoChannel (req: express.Request, res: express.Response) {
+  const videoChannelInfo: VideoChannelCreate = req.body
+  const author: AuthorInstance = res.locals.oauth.token.User.Author
+
+  return db.sequelize.transaction(t => {
+    return createVideoChannel(videoChannelInfo, author, t)
+  })
+  .then(videoChannelUUID => logger.info('Video channel with uuid %s created.', videoChannelUUID))
+  .catch((err: Error) => {
+    logger.debug('Cannot insert the video channel.', err)
+    throw err
+  })
+}
+
+function updateVideoChannelRetryWrapper (req: express.Request, res: express.Response, next: express.NextFunction) {
+  const options = {
+    arguments: [ req, res ],
+    errorMessage: 'Cannot update the video with many retries.'
+  }
+
+  retryTransactionWrapper(updateVideoChannel, options)
+    .then(() => res.type('json').status(204).end())
+    .catch(err => next(err))
+}
+
+function updateVideoChannel (req: express.Request, res: express.Response) {
+  const videoChannelInstance: VideoChannelInstance = res.locals.videoChannel
+  const videoChannelFieldsSave = videoChannelInstance.toJSON()
+  const videoChannelInfoToUpdate: VideoChannelUpdate = req.body
+
+  return db.sequelize.transaction(t => {
+    const options = {
+      transaction: t
+    }
+
+    if (videoChannelInfoToUpdate.name !== undefined) videoChannelInstance.set('name', videoChannelInfoToUpdate.name)
+    if (videoChannelInfoToUpdate.description !== undefined) videoChannelInstance.set('description', videoChannelInfoToUpdate.description)
+
+    return videoChannelInstance.save(options)
+      .then(() => {
+        const json = videoChannelInstance.toUpdateRemoteJSON()
+
+        // Now we'll update the video channel's meta data to our friends
+        return updateVideoChannelToFriends(json, t)
+      })
+  })
+    .then(() => {
+      logger.info('Video channel with name %s and uuid %s updated.', videoChannelInstance.name, videoChannelInstance.uuid)
+    })
+    .catch(err => {
+      logger.debug('Cannot update the video channel.', err)
+
+      // Force fields we want to update
+      // If the transaction is retried, sequelize will think the object has not changed
+      // So it will skip the SQL request, even if the last one was ROLLBACKed!
+      Object.keys(videoChannelFieldsSave).forEach(key => {
+        const value = videoChannelFieldsSave[key]
+        videoChannelInstance.set(key, value)
+      })
+
+      throw err
+    })
+}
+
+function removeVideoChannelRetryWrapper (req: express.Request, res: express.Response, next: express.NextFunction) {
+  const options = {
+    arguments: [ req, res ],
+    errorMessage: 'Cannot remove the video channel with many retries.'
+  }
+
+  retryTransactionWrapper(removeVideoChannel, options)
+    .then(() => res.type('json').status(204).end())
+    .catch(err => next(err))
+}
+
+function removeVideoChannel (req: express.Request, res: express.Response) {
+  const videoChannelInstance: VideoChannelInstance = res.locals.videoChannel
+
+  return db.sequelize.transaction(t => {
+    return videoChannelInstance.destroy({ transaction: t })
+  })
+  .then(() => {
+    logger.info('Video channel with name %s and uuid %s deleted.', videoChannelInstance.name, videoChannelInstance.uuid)
+  })
+  .catch(err => {
+    logger.error('Errors when removed the video channel.', err)
+    throw err
+  })
+}
+
+function getVideoChannel (req: express.Request, res: express.Response, next: express.NextFunction) {
+  db.VideoChannel.loadAndPopulateAuthorAndVideos(res.locals.videoChannel.id)
+    .then(videoChannelWithVideos => res.json(videoChannelWithVideos.toFormattedJSON()))
+    .catch(err => next(err))
+}
index 2b7ead95438ed37842c8eddf178249c0fa4e9597..ec855ee8e12597a27707f508f9531d600d0cb24d 100644 (file)
@@ -46,6 +46,7 @@ import { VideoCreate, VideoUpdate } from '../../../../shared'
 import { abuseVideoRouter } from './abuse'
 import { blacklistRouter } from './blacklist'
 import { rateVideoRouter } from './rate'
+import { videoChannelRouter } from './channel'
 
 const videosRouter = express.Router()
 
@@ -76,6 +77,7 @@ const reqFiles = multer({ storage: storage }).fields([{ name: 'videofile', maxCo
 videosRouter.use('/', abuseVideoRouter)
 videosRouter.use('/', blacklistRouter)
 videosRouter.use('/', rateVideoRouter)
+videosRouter.use('/', videoChannelRouter)
 
 videosRouter.get('/categories', listVideoCategories)
 videosRouter.get('/licences', listVideoLicences)
@@ -161,21 +163,13 @@ function addVideo (req: express.Request, res: express.Response, videoPhysicalFil
   let videoUUID = ''
 
   return db.sequelize.transaction(t => {
-    const user = res.locals.oauth.token.User
+    let p: Promise<TagInstance[]>
 
-    const name = user.username
-    // null because it is OUR pod
-    const podId = null
-    const userId = user.id
+    if (!videoInfo.tags) p = Promise.resolve(undefined)
+    else p = db.Tag.findOrCreateTags(videoInfo.tags, t)
 
-    return db.Author.findOrCreateAuthor(name, podId, userId, t)
-      .then(author => {
-        const tags = videoInfo.tags
-        if (!tags) return { author, tagInstances: undefined }
-
-        return db.Tag.findOrCreateTags(tags, t).then(tagInstances => ({ author, tagInstances }))
-      })
-      .then(({ author, tagInstances }) => {
+    return p
+      .then(tagInstances => {
         const videoData = {
           name: videoInfo.name,
           remote: false,
@@ -186,18 +180,18 @@ function addVideo (req: express.Request, res: express.Response, videoPhysicalFil
           nsfw: videoInfo.nsfw,
           description: videoInfo.description,
           duration: videoPhysicalFile['duration'], // duration was added by a previous middleware
-          authorId: author.id
+          channelId: res.locals.videoChannel.id
         }
 
         const video = db.Video.build(videoData)
-        return { author, tagInstances, video }
+        return { tagInstances, video }
       })
-      .then(({ author, tagInstances, video }) => {
+      .then(({ tagInstances, video }) => {
         const videoFilePath = join(CONFIG.STORAGE.VIDEOS_DIR, videoPhysicalFile.filename)
         return getVideoFileHeight(videoFilePath)
-          .then(height => ({ author, tagInstances, video, videoFileHeight: height }))
+          .then(height => ({ tagInstances, video, videoFileHeight: height }))
       })
-      .then(({ author, tagInstances, video, videoFileHeight }) => {
+      .then(({ tagInstances, video, videoFileHeight }) => {
         const videoFileData = {
           extname: extname(videoPhysicalFile.filename),
           resolution: videoFileHeight,
@@ -205,9 +199,9 @@ function addVideo (req: express.Request, res: express.Response, videoPhysicalFil
         }
 
         const videoFile = db.VideoFile.build(videoFileData)
-        return { author, tagInstances, video, videoFile }
+        return { tagInstances, video, videoFile }
       })
-      .then(({ author, tagInstances, video, videoFile }) => {
+      .then(({ tagInstances, video, videoFile }) => {
         const videoDir = CONFIG.STORAGE.VIDEOS_DIR
         const source = join(videoDir, videoPhysicalFile.filename)
         const destination = join(videoDir, video.getVideoFilename(videoFile))
@@ -216,10 +210,10 @@ function addVideo (req: express.Request, res: express.Response, videoPhysicalFil
           .then(() => {
             // This is important in case if there is another attempt in the retry process
             videoPhysicalFile.filename = video.getVideoFilename(videoFile)
-            return { author, tagInstances, video, videoFile }
+            return { tagInstances, video, videoFile }
           })
       })
-      .then(({ author, tagInstances, video, videoFile }) => {
+      .then(({ tagInstances, video, videoFile }) => {
         const tasks = []
 
         tasks.push(
@@ -239,15 +233,15 @@ function addVideo (req: express.Request, res: express.Response, videoPhysicalFil
           )
         }
 
-        return Promise.all(tasks).then(() => ({ author, tagInstances, video, videoFile }))
+        return Promise.all(tasks).then(() => ({ tagInstances, video, videoFile }))
       })
-      .then(({ author, tagInstances, video, videoFile }) => {
+      .then(({ tagInstances, video, videoFile }) => {
         const options = { transaction: t }
 
         return video.save(options)
           .then(videoCreated => {
-            // Do not forget to add Author information to the created video
-            videoCreated.Author = author
+            // Do not forget to add video channel information to the created video
+            videoCreated.VideoChannel = res.locals.videoChannel
             videoUUID = videoCreated.uuid
 
             return { tagInstances, video: videoCreated, videoFile }
@@ -392,7 +386,7 @@ function getVideo (req: express.Request, res: express.Response) {
   }
 
   // Do not wait the view system
-  res.json(videoInstance.toFormattedJSON())
+  res.json(videoInstance.toFormattedDetailsJSON())
 }
 
 function listVideos (req: express.Request, res: express.Response, next: express.NextFunction) {
index 4bbe56a8a6b1c16e47dbba265037da46cec87483..99a33a716a158cb70060dfada8c76e4bdc96a1b0 100644 (file)
@@ -47,7 +47,7 @@ function generateOEmbed (req: express.Request, res: express.Response, next: expr
     width: embedWidth,
     height: embedHeight,
     title: video.name,
-    author_name: video.Author.name,
+    author_name: video.VideoChannel.Author.name,
     provider_name: 'PeerTube',
     provider_url: webserverUrl
   }
index 1dcab624afc1c0b35a0fd3c20ebb9e37337718e6..c79982660858095181cd6a940bda28d83f4f1668 100644 (file)
@@ -3,4 +3,6 @@ export * from './misc'
 export * from './pods'
 export * from './pods'
 export * from './users'
+export * from './video-authors'
+export * from './video-channels'
 export * from './videos'
index 60fcdd5bb4f2c712d402795d75939e3a84cd8a54..160ec91f3912211c4ea9b787d489ea9aa981a4d7 100644 (file)
@@ -1,4 +1,4 @@
-import 'express-validator'
+import * as validator from 'validator'
 
 function exists (value: any) {
   return value !== undefined && value !== null
@@ -8,9 +8,29 @@ function isArray (value: any) {
   return Array.isArray(value)
 }
 
+function isDateValid (value: string) {
+  return exists(value) && validator.isISO8601(value)
+}
+
+function isIdValid (value: string) {
+  return exists(value) && validator.isInt('' + value)
+}
+
+function isUUIDValid (value: string) {
+  return exists(value) && validator.isUUID('' + value, 4)
+}
+
+function isIdOrUUIDValid (value: string) {
+  return isIdValid(value) || isUUIDValid(value)
+}
+
 // ---------------------------------------------------------------------------
 
 export {
   exists,
-  isArray
+  isArray,
+  isIdValid,
+  isUUIDValid,
+  isIdOrUUIDValid,
+  isDateValid
 }
index e261e05a8f6d23983a6b18bbe8a9148a1e7c7852..057996f1cbaf3926945b2a4db08a900a634f6cec 100644 (file)
@@ -6,18 +6,15 @@ import {
   REQUEST_ENDPOINT_ACTIONS,
   REQUEST_VIDEO_EVENT_TYPES
 } from '../../../initializers'
-import { isArray } from '../misc'
+import { isArray, isDateValid, isUUIDValid } from '../misc'
 import {
-  isVideoAuthorValid,
   isVideoThumbnailDataValid,
-  isVideoUUIDValid,
   isVideoAbuseReasonValid,
   isVideoAbuseReporterUsernameValid,
   isVideoViewsValid,
   isVideoLikesValid,
   isVideoDislikesValid,
   isVideoEventCountValid,
-  isVideoDateValid,
   isVideoCategoryValid,
   isVideoLicenceValid,
   isVideoLanguageValid,
@@ -30,9 +27,22 @@ import {
   isVideoFileExtnameValid,
   isVideoFileResolutionValid
 } from '../videos'
+import { isVideoChannelDescriptionValid, isVideoChannelNameValid } from '../video-channels'
+import { isVideoAuthorNameValid } from '../video-authors'
 
 const ENDPOINT_ACTIONS = REQUEST_ENDPOINT_ACTIONS[REQUEST_ENDPOINTS.VIDEOS]
 
+const checkers: { [ id: string ]: (obj: any) => boolean } = {}
+checkers[ENDPOINT_ACTIONS.ADD_VIDEO] = checkAddVideo
+checkers[ENDPOINT_ACTIONS.UPDATE_VIDEO] = checkUpdateVideo
+checkers[ENDPOINT_ACTIONS.REMOVE_VIDEO] = checkRemoveVideo
+checkers[ENDPOINT_ACTIONS.REPORT_ABUSE] = checkReportVideo
+checkers[ENDPOINT_ACTIONS.ADD_CHANNEL] = checkAddVideoChannel
+checkers[ENDPOINT_ACTIONS.UPDATE_CHANNEL] = checkUpdateVideoChannel
+checkers[ENDPOINT_ACTIONS.REMOVE_CHANNEL] = checkRemoveVideoChannel
+checkers[ENDPOINT_ACTIONS.ADD_AUTHOR] = checkAddAuthor
+checkers[ENDPOINT_ACTIONS.REMOVE_AUTHOR] = checkRemoveAuthor
+
 function isEachRemoteRequestVideosValid (requests: any[]) {
   return isArray(requests) &&
     requests.every(request => {
@@ -40,26 +50,11 @@ function isEachRemoteRequestVideosValid (requests: any[]) {
 
       if (!video) return false
 
-      return (
-        isRequestTypeAddValid(request.type) &&
-        isCommonVideoAttributesValid(video) &&
-        isVideoAuthorValid(video.author) &&
-        isVideoThumbnailDataValid(video.thumbnailData)
-      ) ||
-      (
-        isRequestTypeUpdateValid(request.type) &&
-        isCommonVideoAttributesValid(video)
-      ) ||
-      (
-        isRequestTypeRemoveValid(request.type) &&
-        isVideoUUIDValid(video.uuid)
-      ) ||
-      (
-        isRequestTypeReportAbuseValid(request.type) &&
-        isVideoUUIDValid(request.data.videoUUID) &&
-        isVideoAbuseReasonValid(request.data.reportReason) &&
-        isVideoAbuseReporterUsernameValid(request.data.reporterUsername)
-      )
+      const checker = checkers[request.type]
+      // We don't know the request type
+      if (checker === undefined) return false
+
+      return checker(video)
     })
 }
 
@@ -71,7 +66,7 @@ function isEachRemoteRequestVideosQaduValid (requests: any[]) {
       if (!video) return false
 
       return (
-        isVideoUUIDValid(video.uuid) &&
+        isUUIDValid(video.uuid) &&
         (has(video, 'views') === false || isVideoViewsValid(video.views)) &&
         (has(video, 'likes') === false || isVideoLikesValid(video.likes)) &&
         (has(video, 'dislikes') === false || isVideoDislikesValid(video.dislikes))
@@ -87,7 +82,7 @@ function isEachRemoteRequestVideosEventsValid (requests: any[]) {
       if (!eventData) return false
 
       return (
-        isVideoUUIDValid(eventData.uuid) &&
+        isUUIDValid(eventData.uuid) &&
         values(REQUEST_VIDEO_EVENT_TYPES).indexOf(eventData.eventType) !== -1 &&
         isVideoEventCountValid(eventData.count)
       )
@@ -105,8 +100,8 @@ export {
 // ---------------------------------------------------------------------------
 
 function isCommonVideoAttributesValid (video: any) {
-  return isVideoDateValid(video.createdAt) &&
-         isVideoDateValid(video.updatedAt) &&
+  return isDateValid(video.createdAt) &&
+         isDateValid(video.updatedAt) &&
          isVideoCategoryValid(video.category) &&
          isVideoLicenceValid(video.licence) &&
          isVideoLanguageValid(video.language) &&
@@ -115,7 +110,7 @@ function isCommonVideoAttributesValid (video: any) {
          isVideoDurationValid(video.duration) &&
          isVideoNameValid(video.name) &&
          isVideoTagsValid(video.tags) &&
-         isVideoUUIDValid(video.uuid) &&
+         isUUIDValid(video.uuid) &&
          isVideoViewsValid(video.views) &&
          isVideoLikesValid(video.likes) &&
          isVideoDislikesValid(video.dislikes) &&
@@ -131,18 +126,53 @@ function isCommonVideoAttributesValid (video: any) {
          })
 }
 
-function isRequestTypeAddValid (value: string) {
-  return value === ENDPOINT_ACTIONS.ADD
+function checkAddVideo (video: any) {
+  return isCommonVideoAttributesValid(video) &&
+         isUUIDValid(video.channelUUID) &&
+         isVideoThumbnailDataValid(video.thumbnailData)
+}
+
+function checkUpdateVideo (video: any) {
+  return isCommonVideoAttributesValid(video)
+}
+
+function checkRemoveVideo (video: any) {
+  return isUUIDValid(video.uuid)
+}
+
+function checkReportVideo (abuse: any) {
+  return isUUIDValid(abuse.videoUUID) &&
+         isVideoAbuseReasonValid(abuse.reportReason) &&
+         isVideoAbuseReporterUsernameValid(abuse.reporterUsername)
+}
+
+function checkAddVideoChannel (videoChannel: any) {
+  return isUUIDValid(videoChannel.uuid) &&
+         isVideoChannelNameValid(videoChannel.name) &&
+         isVideoChannelDescriptionValid(videoChannel.description) &&
+         isDateValid(videoChannel.createdAt) &&
+         isDateValid(videoChannel.updatedAt) &&
+         isUUIDValid(videoChannel.ownerUUID)
+}
+
+function checkUpdateVideoChannel (videoChannel: any) {
+  return isUUIDValid(videoChannel.uuid) &&
+         isVideoChannelNameValid(videoChannel.name) &&
+         isVideoChannelDescriptionValid(videoChannel.description) &&
+         isDateValid(videoChannel.createdAt) &&
+         isDateValid(videoChannel.updatedAt) &&
+         isUUIDValid(videoChannel.ownerUUID)
 }
 
-function isRequestTypeUpdateValid (value: string) {
-  return value === ENDPOINT_ACTIONS.UPDATE
+function checkRemoveVideoChannel (videoChannel: any) {
+  return isUUIDValid(videoChannel.uuid)
 }
 
-function isRequestTypeRemoveValid (value: string) {
-  return value === ENDPOINT_ACTIONS.REMOVE
+function checkAddAuthor (author: any) {
+  return isUUIDValid(author.uuid) &&
+         isVideoAuthorNameValid(author.name)
 }
 
-function isRequestTypeReportAbuseValid (value: string) {
-  return value === ENDPOINT_ACTIONS.REPORT_ABUSE
+function checkRemoveAuthor (author: any) {
+  return isUUIDValid(author.uuid)
 }
diff --git a/server/helpers/custom-validators/video-authors.ts b/server/helpers/custom-validators/video-authors.ts
new file mode 100644 (file)
index 0000000..48ca9b2
--- /dev/null
@@ -0,0 +1,45 @@
+import * as Promise from 'bluebird'
+import * as validator from 'validator'
+import * as express from 'express'
+import 'express-validator'
+
+import { database as db } from '../../initializers'
+import { AuthorInstance } from '../../models'
+import { logger } from '../logger'
+
+import { isUserUsernameValid } from './users'
+
+function isVideoAuthorNameValid (value: string) {
+  return isUserUsernameValid(value)
+}
+
+function checkVideoAuthorExists (id: string, res: express.Response, callback: () => void) {
+  let promise: Promise<AuthorInstance>
+  if (validator.isInt(id)) {
+    promise = db.Author.load(+id)
+  } else { // UUID
+    promise = db.Author.loadByUUID(id)
+  }
+
+  promise.then(author => {
+    if (!author) {
+      return res.status(404)
+        .json({ error: 'Video author not found' })
+        .end()
+    }
+
+    res.locals.author = author
+    callback()
+  })
+    .catch(err => {
+      logger.error('Error in video author request validator.', err)
+      return res.sendStatus(500)
+    })
+}
+
+// ---------------------------------------------------------------------------
+
+export {
+  checkVideoAuthorExists,
+  isVideoAuthorNameValid
+}
diff --git a/server/helpers/custom-validators/video-channels.ts b/server/helpers/custom-validators/video-channels.ts
new file mode 100644 (file)
index 0000000..b6be557
--- /dev/null
@@ -0,0 +1,57 @@
+import * as Promise from 'bluebird'
+import * as validator from 'validator'
+import * as express from 'express'
+import 'express-validator'
+import 'multer'
+
+import { database as db, CONSTRAINTS_FIELDS } from '../../initializers'
+import { VideoChannelInstance } from '../../models'
+import { logger } from '../logger'
+import { exists } from './misc'
+
+const VIDEO_CHANNELS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEO_CHANNELS
+
+function isVideoChannelDescriptionValid (value: string) {
+  return value === null || validator.isLength(value, VIDEO_CHANNELS_CONSTRAINTS_FIELDS.DESCRIPTION)
+}
+
+function isVideoChannelNameValid (value: string) {
+  return exists(value) && validator.isLength(value, VIDEO_CHANNELS_CONSTRAINTS_FIELDS.NAME)
+}
+
+function isVideoChannelUUIDValid (value: string) {
+  return exists(value) && validator.isUUID('' + value, 4)
+}
+
+function checkVideoChannelExists (id: string, res: express.Response, callback: () => void) {
+  let promise: Promise<VideoChannelInstance>
+  if (validator.isInt(id)) {
+    promise = db.VideoChannel.loadAndPopulateAuthor(+id)
+  } else { // UUID
+    promise = db.VideoChannel.loadByUUIDAndPopulateAuthor(id)
+  }
+
+  promise.then(videoChannel => {
+    if (!videoChannel) {
+      return res.status(404)
+        .json({ error: 'Video channel not found' })
+        .end()
+    }
+
+    res.locals.videoChannel = videoChannel
+    callback()
+  })
+    .catch(err => {
+      logger.error('Error in video channel request validator.', err)
+      return res.sendStatus(500)
+    })
+}
+
+// ---------------------------------------------------------------------------
+
+export {
+  isVideoChannelDescriptionValid,
+  isVideoChannelNameValid,
+  isVideoChannelUUIDValid,
+  checkVideoChannelExists
+}
index 05d1dc60744eac1457d496f2afa3383f7584e278..4e441fe5f9481954ab28748c17102478f458832b 100644 (file)
@@ -23,18 +23,6 @@ const VIDEOS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEOS
 const VIDEO_ABUSES_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEO_ABUSES
 const VIDEO_EVENTS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEO_EVENTS
 
-function isVideoIdOrUUIDValid (value: string) {
-  return validator.isInt(value) || isVideoUUIDValid(value)
-}
-
-function isVideoAuthorValid (value: string) {
-  return isUserUsernameValid(value)
-}
-
-function isVideoDateValid (value: string) {
-  return exists(value) && validator.isISO8601(value)
-}
-
 function isVideoCategoryValid (value: number) {
   return VIDEO_CATEGORIES[value] !== undefined
 }
@@ -79,10 +67,6 @@ function isVideoThumbnailDataValid (value: string) {
   return exists(value) && validator.isByteLength(value, VIDEOS_CONSTRAINTS_FIELDS.THUMBNAIL_DATA)
 }
 
-function isVideoUUIDValid (value: string) {
-  return exists(value) && validator.isUUID('' + value, 4)
-}
-
 function isVideoAbuseReasonValid (value: string) {
   return exists(value) && validator.isLength(value, VIDEO_ABUSES_CONSTRAINTS_FIELDS.REASON)
 }
@@ -170,9 +154,6 @@ function checkVideoExists (id: string, res: express.Response, callback: () => vo
 // ---------------------------------------------------------------------------
 
 export {
-  isVideoIdOrUUIDValid,
-  isVideoAuthorValid,
-  isVideoDateValid,
   isVideoCategoryValid,
   isVideoLicenceValid,
   isVideoLanguageValid,
@@ -185,7 +166,6 @@ export {
   isVideoThumbnailValid,
   isVideoThumbnailDataValid,
   isVideoFileExtnameValid,
-  isVideoUUIDValid,
   isVideoAbuseReasonValid,
   isVideoAbuseReporterUsernameValid,
   isVideoFile,
index 132164746efa8b6f7403696f139cd893e7e4f74b..54dce980f8e8ce52d553df5b978ef18290202d0e 100644 (file)
@@ -10,6 +10,7 @@ import {
   RequestEndpoint,
   RequestVideoEventType,
   RequestVideoQaduType,
+  RemoteVideoRequestType,
   JobState
 } from '../../shared/models'
 
@@ -35,6 +36,7 @@ const SORTABLE_COLUMNS = {
   PODS: [ 'id', 'host', 'score', 'createdAt' ],
   USERS: [ 'id', 'username', 'createdAt' ],
   VIDEO_ABUSES: [ 'id', 'createdAt' ],
+  VIDEO_CHANNELS: [ 'id', 'name', 'updatedAt', 'createdAt' ],
   VIDEOS: [ 'name', 'duration', 'createdAt', 'views', 'likes' ],
   BLACKLISTS: [ 'id', 'name', 'duration', 'views', 'likes', 'dislikes', 'uuid', 'createdAt' ]
 }
@@ -115,6 +117,10 @@ const CONSTRAINTS_FIELDS = {
   VIDEO_ABUSES: {
     REASON: { min: 2, max: 300 } // Length
   },
+  VIDEO_CHANNELS: {
+    NAME: { min: 3, max: 50 }, // Length
+    DESCRIPTION: { min: 3, max: 250 } // Length
+  },
   VIDEOS: {
     NAME: { min: 3, max: 50 }, // Length
     DESCRIPTION: { min: 3, max: 250 }, // Length
@@ -232,11 +238,20 @@ const REQUEST_ENDPOINTS: { [ id: string ]: RequestEndpoint } = {
   VIDEOS: 'videos'
 }
 
-const REQUEST_ENDPOINT_ACTIONS: { [ id: string ]: any } = {}
+const REQUEST_ENDPOINT_ACTIONS: {
+  [ id: string ]: {
+    [ id: string ]: RemoteVideoRequestType
+  }
+} = {}
 REQUEST_ENDPOINT_ACTIONS[REQUEST_ENDPOINTS.VIDEOS] = {
-  ADD: 'add',
-  UPDATE: 'update',
-  REMOVE: 'remove',
+  ADD_VIDEO: 'add-video',
+  UPDATE_VIDEO: 'update-video',
+  REMOVE_VIDEO: 'remove-video',
+  ADD_CHANNEL: 'add-channel',
+  UPDATE_CHANNEL: 'update-channel',
+  REMOVE_CHANNEL: 'remove-channel',
+  ADD_AUTHOR: 'add-author',
+  REMOVE_AUTHOR: 'remove-author',
   REPORT_ABUSE: 'report-abuse'
 }
 
index c5a3853613a4518ce875ca9f2801ea2bd797d534..d461cb440d9f26cbd816465c412a0108a702e280 100644 (file)
@@ -14,6 +14,7 @@ import { VideoTagModel } from './../models/video/video-tag-interface'
 import { BlacklistedVideoModel } from './../models/video/video-blacklist-interface'
 import { VideoFileModel } from './../models/video/video-file-interface'
 import { VideoAbuseModel } from './../models/video/video-abuse-interface'
+import { VideoChannelModel } from './../models/video/video-channel-interface'
 import { UserModel } from './../models/user/user-interface'
 import { UserVideoRateModel } from './../models/user/user-video-rate-interface'
 import { TagModel } from './../models/video/tag-interface'
@@ -50,6 +51,7 @@ const database: {
   UserVideoRate?: UserVideoRateModel,
   User?: UserModel,
   VideoAbuse?: VideoAbuseModel,
+  VideoChannel?: VideoChannelModel,
   VideoFile?: VideoFileModel,
   BlacklistedVideo?: BlacklistedVideoModel,
   VideoTag?: VideoTagModel,
index 10b74b85fd50cb8b2334f52716d4e4fc2522b21d..b997de07f16824c3fdefe56b13036892b7a40184 100644 (file)
@@ -5,6 +5,7 @@ import { database as db } from './database'
 import { USER_ROLES, CONFIG, LAST_MIGRATION_VERSION, CACHE } from './constants'
 import { clientsExist, usersExist } from './checker'
 import { logger, createCertsIfNotExist, mkdirpPromise, rimrafPromise } from '../helpers'
+import { createUserAuthorAndChannel } from '../lib'
 
 function installApplication () {
   return db.sequelize.sync()
@@ -91,7 +92,7 @@ function createOAuthAdminIfNotExist () {
     const username = 'root'
     const role = USER_ROLES.ADMIN
     const email = CONFIG.ADMIN.EMAIL
-    const createOptions: { validate?: boolean } = {}
+    let validatePassword = true
     let password = ''
 
     // Do not generate a random password for tests
@@ -103,7 +104,7 @@ function createOAuthAdminIfNotExist () {
       }
 
       // Our password is weak so do not validate it
-      createOptions.validate = false
+      validatePassword = false
     } else {
       password = passwordGenerator(8, true)
     }
@@ -115,13 +116,15 @@ function createOAuthAdminIfNotExist () {
       role,
       videoQuota: -1
     }
+    const user = db.User.build(userData)
 
-    return db.User.create(userData, createOptions).then(createdUser => {
-      logger.info('Username: ' + username)
-      logger.info('User password: ' + password)
+    return createUserAuthorAndChannel(user, validatePassword)
+      .then(({ user }) => {
+        logger.info('Username: ' + username)
+        logger.info('User password: ' + password)
 
-      logger.info('Creating Application table.')
-      return db.Application.create({ migrationVersion: LAST_MIGRATION_VERSION })
-    })
+        logger.info('Creating Application table.')
+        return db.Application.create({ migrationVersion: LAST_MIGRATION_VERSION })
+      })
   })
 }
index 09e2e9a0d62531adc8521c9e53905330c6b1f126..fecdca6eff41c921008f0656465dbd8eae8a75c8 100644 (file)
@@ -55,7 +55,7 @@ class VideosPreviewCache {
   }
 
   private saveRemotePreviewAndReturnPath (video: VideoInstance) {
-    const req = fetchRemotePreview(video.Author.Pod, video)
+    const req = fetchRemotePreview(video)
 
     return new Promise<string>((res, rej) => {
       const path = join(CACHE.DIRECTORIES.PREVIEWS, video.getPreviewName())
index 65349ef5f7eb674248ba27f6dc32d365b589e81a..f035b099ba68a7b44c9f1fa47e2e1e38694b5278 100644 (file)
@@ -42,7 +42,13 @@ import {
   RemoteVideoRemoveData,
   RemoteVideoReportAbuseData,
   ResultList,
-  Pod as FormattedPod
+  RemoteVideoRequestType,
+  Pod as FormattedPod,
+  RemoteVideoChannelCreateData,
+  RemoteVideoChannelUpdateData,
+  RemoteVideoChannelRemoveData,
+  RemoteVideoAuthorCreateData,
+  RemoteVideoAuthorRemoveData
 } from '../../shared'
 
 type QaduParam = { videoId: number, type: RequestVideoQaduType }
@@ -62,7 +68,7 @@ function activateSchedulers () {
 
 function addVideoToFriends (videoData: RemoteVideoCreateData, transaction: Sequelize.Transaction) {
   const options = {
-    type: ENDPOINT_ACTIONS.ADD,
+    type: ENDPOINT_ACTIONS.ADD_VIDEO,
     endpoint: REQUEST_ENDPOINTS.VIDEOS,
     data: videoData,
     transaction
@@ -72,7 +78,7 @@ function addVideoToFriends (videoData: RemoteVideoCreateData, transaction: Seque
 
 function updateVideoToFriends (videoData: RemoteVideoUpdateData, transaction: Sequelize.Transaction) {
   const options = {
-    type: ENDPOINT_ACTIONS.UPDATE,
+    type: ENDPOINT_ACTIONS.UPDATE_VIDEO,
     endpoint: REQUEST_ENDPOINTS.VIDEOS,
     data: videoData,
     transaction
@@ -82,7 +88,7 @@ function updateVideoToFriends (videoData: RemoteVideoUpdateData, transaction: Se
 
 function removeVideoToFriends (videoParams: RemoteVideoRemoveData, transaction: Sequelize.Transaction) {
   const options = {
-    type: ENDPOINT_ACTIONS.REMOVE,
+    type: ENDPOINT_ACTIONS.REMOVE_VIDEO,
     endpoint: REQUEST_ENDPOINTS.VIDEOS,
     data: videoParams,
     transaction
@@ -90,12 +96,62 @@ function removeVideoToFriends (videoParams: RemoteVideoRemoveData, transaction:
   return createRequest(options)
 }
 
+function addVideoAuthorToFriends (authorData: RemoteVideoAuthorCreateData, transaction: Sequelize.Transaction) {
+  const options = {
+    type: ENDPOINT_ACTIONS.ADD_AUTHOR,
+    endpoint: REQUEST_ENDPOINTS.VIDEOS,
+    data: authorData,
+    transaction
+  }
+  return createRequest(options)
+}
+
+function removeVideoAuthorToFriends (authorData: RemoteVideoAuthorRemoveData, transaction: Sequelize.Transaction) {
+  const options = {
+    type: ENDPOINT_ACTIONS.REMOVE_AUTHOR,
+    endpoint: REQUEST_ENDPOINTS.VIDEOS,
+    data: authorData,
+    transaction
+  }
+  return createRequest(options)
+}
+
+function addVideoChannelToFriends (videoChannelData: RemoteVideoChannelCreateData, transaction: Sequelize.Transaction) {
+  const options = {
+    type: ENDPOINT_ACTIONS.ADD_CHANNEL,
+    endpoint: REQUEST_ENDPOINTS.VIDEOS,
+    data: videoChannelData,
+    transaction
+  }
+  return createRequest(options)
+}
+
+function updateVideoChannelToFriends (videoChannelData: RemoteVideoChannelUpdateData, transaction: Sequelize.Transaction) {
+  const options = {
+    type: ENDPOINT_ACTIONS.UPDATE_CHANNEL,
+    endpoint: REQUEST_ENDPOINTS.VIDEOS,
+    data: videoChannelData,
+    transaction
+  }
+  return createRequest(options)
+}
+
+function removeVideoChannelToFriends (videoChannelParams: RemoteVideoChannelRemoveData, transaction: Sequelize.Transaction) {
+  const options = {
+    type: ENDPOINT_ACTIONS.REMOVE_CHANNEL,
+    endpoint: REQUEST_ENDPOINTS.VIDEOS,
+    data: videoChannelParams,
+    transaction
+  }
+  return createRequest(options)
+}
+
 function reportAbuseVideoToFriend (reportData: RemoteVideoReportAbuseData, video: VideoInstance, transaction: Sequelize.Transaction) {
   const options = {
     type: ENDPOINT_ACTIONS.REPORT_ABUSE,
     endpoint: REQUEST_ENDPOINTS.VIDEOS,
     data: reportData,
-    toIds: [ video.Author.podId ],
+    toIds: [ video.VideoChannel.Author.podId ],
     transaction
   }
   return createRequest(options)
@@ -207,15 +263,66 @@ function quitFriends () {
     .finally(() => requestScheduler.activate())
 }
 
+function sendOwnedDataToPod (podId: number) {
+  // First send authors
+  return sendOwnedAuthorsToPod(podId)
+    .then(() => sendOwnedChannelsToPod(podId))
+    .then(() => sendOwnedVideosToPod(podId))
+}
+
+function sendOwnedChannelsToPod (podId: number) {
+  return db.VideoChannel.listOwned()
+    .then(videoChannels => {
+      const tasks = []
+      videoChannels.forEach(videoChannel => {
+        const remoteVideoChannel = videoChannel.toAddRemoteJSON()
+        const options = {
+          type: 'add-channel' as 'add-channel',
+          endpoint: REQUEST_ENDPOINTS.VIDEOS,
+          data: remoteVideoChannel,
+          toIds: [ podId ],
+          transaction: null
+        }
+
+        const p = createRequest(options)
+        tasks.push(p)
+      })
+
+      return Promise.all(tasks)
+    })
+}
+
+function sendOwnedAuthorsToPod (podId: number) {
+  return db.Author.listOwned()
+    .then(authors => {
+      const tasks = []
+      authors.forEach(author => {
+        const remoteAuthor = author.toAddRemoteJSON()
+        const options = {
+          type: 'add-author' as 'add-author',
+          endpoint: REQUEST_ENDPOINTS.VIDEOS,
+          data: remoteAuthor,
+          toIds: [ podId ],
+          transaction: null
+        }
+
+        const p = createRequest(options)
+        tasks.push(p)
+      })
+
+      return Promise.all(tasks)
+    })
+}
+
 function sendOwnedVideosToPod (podId: number) {
-  db.Video.listOwnedAndPopulateAuthorAndTags()
+  return db.Video.listOwnedAndPopulateAuthorAndTags()
     .then(videosList => {
       const tasks = []
       videosList.forEach(video => {
         const promise = video.toAddRemoteJSON()
           .then(remoteVideo => {
             const options = {
-              type: 'add',
+              type: 'add-video' as 'add-video',
               endpoint: REQUEST_ENDPOINTS.VIDEOS,
               data: remoteVideo,
               toIds: [ podId ],
@@ -236,8 +343,8 @@ function sendOwnedVideosToPod (podId: number) {
     })
 }
 
-function fetchRemotePreview (pod: PodInstance, video: VideoInstance) {
-  const host = video.Author.Pod.host
+function fetchRemotePreview (video: VideoInstance) {
+  const host = video.VideoChannel.Author.Pod.host
   const path = join(STATIC_PATHS.PREVIEWS, video.getPreviewName())
 
   return request.get(REMOTE_SCHEME.HTTP + '://' + host + path)
@@ -274,7 +381,9 @@ function getRequestVideoEventScheduler () {
 export {
   activateSchedulers,
   addVideoToFriends,
+  removeVideoAuthorToFriends,
   updateVideoToFriends,
+  addVideoAuthorToFriends,
   reportAbuseVideoToFriend,
   quickAndDirtyUpdateVideoToFriends,
   quickAndDirtyUpdatesVideoToFriends,
@@ -285,11 +394,14 @@ export {
   quitFriends,
   removeFriend,
   removeVideoToFriends,
-  sendOwnedVideosToPod,
+  sendOwnedDataToPod,
   getRequestScheduler,
   getRequestVideoQaduScheduler,
   getRequestVideoEventScheduler,
-  fetchRemotePreview
+  fetchRemotePreview,
+  addVideoChannelToFriends,
+  updateVideoChannelToFriends,
+  removeVideoChannelToFriends
 }
 
 // ---------------------------------------------------------------------------
@@ -373,7 +485,7 @@ function makeRequestsToWinningPods (cert: string, podsList: PodInstance[]) {
             .then(podCreated => {
 
               // Add our videos to the request scheduler
-              sendOwnedVideosToPod(podCreated.id)
+              sendOwnedDataToPod(podCreated.id)
             })
             .catch(err => {
               logger.error('Cannot add friend %s pod.', pod.host, err)
@@ -397,7 +509,7 @@ function makeRequestsToWinningPods (cert: string, podsList: PodInstance[]) {
 
 // Wrapper that populate "toIds" argument with all our friends if it is not specified
 type CreateRequestOptions = {
-  type: string
+  type: RemoteVideoRequestType
   endpoint: RequestEndpoint
   data: Object
   toIds?: number[]
index 8628da4dded5c4e18d1fd05f189b8dbb6cfc064e..d1534b085555e609c5cfac096eebe8cae23d3b97 100644 (file)
@@ -3,3 +3,5 @@ export * from './jobs'
 export * from './request'
 export * from './friends'
 export * from './oauth-model'
+export * from './user'
+export * from './video-channel'
diff --git a/server/lib/user.ts b/server/lib/user.ts
new file mode 100644 (file)
index 0000000..8609e72
--- /dev/null
@@ -0,0 +1,46 @@
+import { database as db } from '../initializers'
+import { UserInstance } from '../models'
+import { addVideoAuthorToFriends } from './friends'
+import { createVideoChannel } from './video-channel'
+
+function createUserAuthorAndChannel (user: UserInstance, validateUser = true) {
+  return db.sequelize.transaction(t => {
+    const userOptions = {
+      transaction: t,
+      validate: validateUser
+    }
+
+    return user.save(userOptions)
+      .then(user => {
+        const author = db.Author.build({
+          name: user.username,
+          podId: null, // It is our pod
+          userId: user.id
+        })
+
+        return author.save({ transaction: t })
+          .then(author => ({ author, user }))
+      })
+      .then(({ author, user }) => {
+        const remoteVideoAuthor = author.toAddRemoteJSON()
+
+        // Now we'll add the video channel's meta data to our friends
+        return addVideoAuthorToFriends(remoteVideoAuthor, t)
+          .then(() => ({ author, user }))
+      })
+      .then(({ author, user }) => {
+        const videoChannelInfo = {
+          name: `Default ${user.username} channel`
+        }
+
+        return createVideoChannel(videoChannelInfo, author, t)
+          .then(videoChannel => ({ author, user, videoChannel }))
+      })
+  })
+}
+
+// ---------------------------------------------------------------------------
+
+export {
+  createUserAuthorAndChannel
+}
diff --git a/server/lib/video-channel.ts b/server/lib/video-channel.ts
new file mode 100644 (file)
index 0000000..2241799
--- /dev/null
@@ -0,0 +1,42 @@
+import * as Sequelize from 'sequelize'
+
+import { addVideoChannelToFriends } from './friends'
+import { database as db } from '../initializers'
+import { AuthorInstance } from '../models'
+import { VideoChannelCreate } from '../../shared/models'
+
+function createVideoChannel (videoChannelInfo: VideoChannelCreate, author: AuthorInstance, t: Sequelize.Transaction) {
+  let videoChannelUUID = ''
+
+  const videoChannelData = {
+    name: videoChannelInfo.name,
+    description: videoChannelInfo.description,
+    remote: false,
+    authorId: author.id
+  }
+
+  const videoChannel = db.VideoChannel.build(videoChannelData)
+  const options = { transaction: t }
+
+  return videoChannel.save(options)
+    .then(videoChannelCreated => {
+      // Do not forget to add Author information to the created video channel
+      videoChannelCreated.Author = author
+      videoChannelUUID = videoChannelCreated.uuid
+
+      return videoChannelCreated
+    })
+    .then(videoChannel => {
+      const remoteVideoChannel = videoChannel.toAddRemoteJSON()
+
+      // Now we'll add the video channel's meta data to our friends
+      return addVideoChannelToFriends(remoteVideoChannel, t)
+    })
+    .then(() => videoChannelUUID) // Return video channel UUID
+}
+
+// ---------------------------------------------------------------------------
+
+export {
+  createVideoChannel
+}
index 2c70ff5f0b2e5ecbb0eb940710ba13babc1c1a3e..91aa3e5b6b4c2ac38d7f1a330dd8ca9f36aac8f3 100644 (file)
@@ -22,6 +22,12 @@ function setVideoAbusesSort (req: express.Request, res: express.Response, next:
   return next()
 }
 
+function setVideoChannelsSort (req: express.Request, res: express.Response, next: express.NextFunction) {
+  if (!req.query.sort) req.query.sort = '-createdAt'
+
+  return next()
+}
+
 function setVideosSort (req: express.Request, res: express.Response, next: express.NextFunction) {
   if (!req.query.sort) req.query.sort = '-createdAt'
 
@@ -55,6 +61,7 @@ export {
   setPodsSort,
   setUsersSort,
   setVideoAbusesSort,
+  setVideoChannelsSort,
   setVideosSort,
   setBlacklistSort
 }
index 068c41b240e763e34c28ee69cea4309223665bbe..247f6039e9a88f1b56f2313031eeacc2a5f1fdda 100644 (file)
@@ -6,3 +6,4 @@ export * from './sort'
 export * from './users'
 export * from './videos'
 export * from './video-blacklist'
+export * from './video-channels'
index 4b8c03fafcacc4ca779faf051073699d9939bfc0..f8e34d2d429f846cf6bd44b3934620b028e9c208 100644 (file)
@@ -4,9 +4,12 @@ import { join } from 'path'
 
 import { checkErrors } from './utils'
 import { CONFIG } from '../../initializers'
-import { logger } from '../../helpers'
-import { checkVideoExists, isVideoIdOrUUIDValid } from '../../helpers/custom-validators/videos'
-import { isTestInstance } from '../../helpers/core-utils'
+import {
+  logger,
+  isTestInstance,
+  checkVideoExists,
+  isIdOrUUIDValid
+} from '../../helpers'
 
 const urlShouldStartWith = CONFIG.WEBSERVER.SCHEME + '://' + join(CONFIG.WEBSERVER.HOST, 'videos', 'watch') + '/'
 const videoWatchRegex = new RegExp('([^/]+)$')
@@ -45,7 +48,7 @@ const oembedValidator = [
       }
 
       const videoId = matches[1]
-      if (isVideoIdOrUUIDValid(videoId) === false) {
+      if (isIdOrUUIDValid(videoId) === false) {
         return res.status(400)
                   .json({ error: 'Invalid video id.' })
                   .end()
index 227f309ad567576005fbd7b2527d6a20886c5da0..d23a95537f9f32a6c12574695981fb84a56d2553 100644 (file)
@@ -11,12 +11,14 @@ const SORTABLE_USERS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.USERS)
 const SORTABLE_VIDEO_ABUSES_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_ABUSES)
 const SORTABLE_VIDEOS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEOS)
 const SORTABLE_BLACKLISTS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.BLACKLISTS)
+const SORTABLE_VIDEO_CHANNELS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_CHANNELS)
 
 const podsSortValidator = checkSort(SORTABLE_PODS_COLUMNS)
 const usersSortValidator = checkSort(SORTABLE_USERS_COLUMNS)
 const videoAbusesSortValidator = checkSort(SORTABLE_VIDEO_ABUSES_COLUMNS)
 const videosSortValidator = checkSort(SORTABLE_VIDEOS_COLUMNS)
 const blacklistSortValidator = checkSort(SORTABLE_BLACKLISTS_COLUMNS)
+const videoChannelsSortValidator = checkSort(SORTABLE_VIDEO_CHANNELS_COLUMNS)
 
 // ---------------------------------------------------------------------------
 
@@ -24,6 +26,7 @@ export {
   podsSortValidator,
   usersSortValidator,
   videoAbusesSortValidator,
+  videoChannelsSortValidator,
   videosSortValidator,
   blacklistSortValidator
 }
index ab9d0938cefd60a70897d17feaafc6d0d72671e2..1a33cfd8ceb6571aac43e20d324e5ef9c6b40b09 100644 (file)
@@ -13,7 +13,7 @@ import {
   isUserPasswordValid,
   isUserVideoQuotaValid,
   isUserDisplayNSFWValid,
-  isVideoIdOrUUIDValid
+  isIdOrUUIDValid
 } from '../../helpers'
 import { UserInstance, VideoInstance } from '../../models'
 
@@ -109,7 +109,7 @@ const usersGetValidator = [
 ]
 
 const usersVideoRatingValidator = [
-  param('videoId').custom(isVideoIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid video id'),
+  param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid video id'),
 
   (req: express.Request, res: express.Response, next: express.NextFunction) => {
     logger.debug('Checking usersVideoRating parameters', { parameters: req.params })
index 30c6d4bd96bf0db6e2e44abc21881c08473cbd7d..3c8c315195bbd24dc06ac4c0f527009f8e8129f7 100644 (file)
@@ -3,10 +3,10 @@ import * as express from 'express'
 
 import { database as db } from '../../initializers/database'
 import { checkErrors } from './utils'
-import { logger, isVideoIdOrUUIDValid, checkVideoExists } from '../../helpers'
+import { logger, isIdOrUUIDValid, checkVideoExists } from '../../helpers'
 
 const videosBlacklistRemoveValidator = [
-  param('videoId').custom(isVideoIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
+  param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
 
   (req: express.Request, res: express.Response, next: express.NextFunction) => {
     logger.debug('Checking blacklistRemove parameters.', { parameters: req.params })
@@ -20,7 +20,7 @@ const videosBlacklistRemoveValidator = [
 ]
 
 const videosBlacklistAddValidator = [
-  param('videoId').custom(isVideoIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
+  param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
 
   (req: express.Request, res: express.Response, next: express.NextFunction) => {
     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 (file)
index 0000000..979fbd3
--- /dev/null
@@ -0,0 +1,142 @@
+import { body, param } from 'express-validator/check'
+import * as express from 'express'
+
+import { checkErrors } from './utils'
+import { database as db } from '../../initializers'
+import {
+  logger,
+  isIdOrUUIDValid,
+  isVideoChannelDescriptionValid,
+  isVideoChannelNameValid,
+  checkVideoChannelExists,
+  checkVideoAuthorExists
+} from '../../helpers'
+
+const listVideoAuthorChannelsValidator = [
+  param('authorId').custom(isIdOrUUIDValid).withMessage('Should have a valid author id'),
+
+  (req: express.Request, res: express.Response, next: express.NextFunction) => {
+    logger.debug('Checking listVideoAuthorChannelsValidator parameters', { parameters: req.body })
+
+    checkErrors(req, res, () => {
+      checkVideoAuthorExists(req.params.authorId, res, next)
+    })
+  }
+]
+
+const videoChannelsAddValidator = [
+  body('name').custom(isVideoChannelNameValid).withMessage('Should have a valid name'),
+  body('description').custom(isVideoChannelDescriptionValid).withMessage('Should have a valid description'),
+
+  (req: express.Request, res: express.Response, next: express.NextFunction) => {
+    logger.debug('Checking videoChannelsAdd parameters', { parameters: req.body })
+
+    checkErrors(req, res, next)
+  }
+]
+
+const videoChannelsUpdateValidator = [
+  param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
+  body('name').optional().custom(isVideoChannelNameValid).withMessage('Should have a valid name'),
+  body('description').optional().custom(isVideoChannelDescriptionValid).withMessage('Should have a valid description'),
+
+  (req: express.Request, res: express.Response, next: express.NextFunction) => {
+    logger.debug('Checking videoChannelsUpdate parameters', { parameters: req.body })
+
+    checkErrors(req, res, () => {
+      checkVideoChannelExists(req.params.id, res, () => {
+        // We need to make additional checks
+        if (res.locals.videoChannel.isOwned() === false) {
+          return res.status(403)
+            .json({ error: 'Cannot update video channel of another pod' })
+            .end()
+        }
+
+        if (res.locals.videoChannel.Author.userId !== res.locals.oauth.token.User.id) {
+          return res.status(403)
+            .json({ error: 'Cannot update video channel of another user' })
+            .end()
+        }
+
+        next()
+      })
+    })
+  }
+]
+
+const videoChannelsRemoveValidator = [
+  param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
+
+  (req: express.Request, res: express.Response, next: express.NextFunction) => {
+    logger.debug('Checking videoChannelsRemove parameters', { parameters: req.params })
+
+    checkErrors(req, res, () => {
+      checkVideoChannelExists(req.params.id, res, () => {
+        // Check if the user who did the request is able to delete the video
+        checkUserCanDeleteVideoChannel(res, () => {
+          checkVideoChannelIsNotTheLastOne(res, next)
+        })
+      })
+    })
+  }
+]
+
+const videoChannelGetValidator = [
+  param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
+
+  (req: express.Request, res: express.Response, next: express.NextFunction) => {
+    logger.debug('Checking videoChannelsGet parameters', { parameters: req.params })
+
+    checkErrors(req, res, () => {
+      checkVideoChannelExists(req.params.id, res, next)
+    })
+  }
+]
+
+// ---------------------------------------------------------------------------
+
+export {
+  listVideoAuthorChannelsValidator,
+  videoChannelsAddValidator,
+  videoChannelsUpdateValidator,
+  videoChannelsRemoveValidator,
+  videoChannelGetValidator
+}
+
+// ---------------------------------------------------------------------------
+
+function checkUserCanDeleteVideoChannel (res: express.Response, callback: () => void) {
+  const user = res.locals.oauth.token.User
+
+  // Retrieve the user who did the request
+  if (res.locals.videoChannel.isOwned() === false) {
+    return res.status(403)
+              .json({ error: 'Cannot remove video channel of another pod.' })
+              .end()
+  }
+
+  // Check if the user can delete the video channel
+  // The user can delete it if s/he is an admin
+  // Or if s/he is the video channel's author
+  if (user.isAdmin() === false && res.locals.videoChannel.Author.userId !== user.id) {
+    return res.status(403)
+              .json({ error: 'Cannot remove video channel of another user' })
+              .end()
+  }
+
+  // If we reach this comment, we can delete the video
+  callback()
+}
+
+function checkVideoChannelIsNotTheLastOne (res: express.Response, callback: () => void) {
+  db.VideoChannel.countByAuthor(res.locals.oauth.token.User.Author.id)
+    .then(count => {
+      if (count <= 1) {
+        return res.status(409)
+          .json({ error: 'Cannot remove the last channel of this user' })
+          .end()
+      }
+
+      callback()
+    })
+}
index 3f881e1b553f366208d042120c5c36e61605a349..8a9b383b88c567abf1b0db3f1eb5d7673a2a24f4 100644 (file)
@@ -15,11 +15,12 @@ import {
   isVideoLanguageValid,
   isVideoTagsValid,
   isVideoNSFWValid,
-  isVideoIdOrUUIDValid,
+  isIdOrUUIDValid,
   isVideoAbuseReasonValid,
   isVideoRatingTypeValid,
   getDurationFromVideoFile,
-  checkVideoExists
+  checkVideoExists,
+  isIdValid
 } from '../../helpers'
 
 const videosAddValidator = [
@@ -33,6 +34,7 @@ const videosAddValidator = [
   body('language').optional().custom(isVideoLanguageValid).withMessage('Should have a valid language'),
   body('nsfw').custom(isVideoNSFWValid).withMessage('Should have a valid NSFW attribute'),
   body('description').custom(isVideoDescriptionValid).withMessage('Should have a valid description'),
+  body('channelId').custom(isIdValid).withMessage('Should have correct video channel id'),
   body('tags').optional().custom(isVideoTagsValid).withMessage('Should have correct tags'),
 
   (req: express.Request, res: express.Response, next: express.NextFunction) => {
@@ -42,7 +44,20 @@ const videosAddValidator = [
       const videoFile: Express.Multer.File = req.files['videofile'][0]
       const user = res.locals.oauth.token.User
 
-      user.isAbleToUploadVideo(videoFile)
+      return db.VideoChannel.loadByIdAndAuthor(req.body.channelId, user.Author.id)
+        .then(videoChannel => {
+          if (!videoChannel) {
+            res.status(400)
+              .json({ error: 'Unknown video video channel for this author.' })
+              .end()
+
+            return undefined
+          }
+
+          res.locals.videoChannel = videoChannel
+
+          return user.isAbleToUploadVideo(videoFile)
+        })
         .then(isAble => {
           if (isAble === false) {
             res.status(403)
@@ -88,7 +103,7 @@ const videosAddValidator = [
 ]
 
 const videosUpdateValidator = [
-  param('id').custom(isVideoIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
+  param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
   body('name').optional().custom(isVideoNameValid).withMessage('Should have a valid name'),
   body('category').optional().custom(isVideoCategoryValid).withMessage('Should have a valid category'),
   body('licence').optional().custom(isVideoLicenceValid).withMessage('Should have a valid licence'),
@@ -109,7 +124,7 @@ const videosUpdateValidator = [
                     .end()
         }
 
-        if (res.locals.video.Author.userId !== res.locals.oauth.token.User.id) {
+        if (res.locals.video.VideoChannel.Author.userId !== res.locals.oauth.token.User.id) {
           return res.status(403)
                     .json({ error: 'Cannot update video of another user' })
                     .end()
@@ -122,7 +137,7 @@ const videosUpdateValidator = [
 ]
 
 const videosGetValidator = [
-  param('id').custom(isVideoIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
+  param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
 
   (req: express.Request, res: express.Response, next: express.NextFunction) => {
     logger.debug('Checking videosGet parameters', { parameters: req.params })
@@ -134,7 +149,7 @@ const videosGetValidator = [
 ]
 
 const videosRemoveValidator = [
-  param('id').custom(isVideoIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
+  param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
 
   (req: express.Request, res: express.Response, next: express.NextFunction) => {
     logger.debug('Checking videosRemove parameters', { parameters: req.params })
@@ -162,7 +177,7 @@ const videosSearchValidator = [
 ]
 
 const videoAbuseReportValidator = [
-  param('id').custom(isVideoIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
+  param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
   body('reason').custom(isVideoAbuseReasonValid).withMessage('Should have a valid reason'),
 
   (req: express.Request, res: express.Response, next: express.NextFunction) => {
@@ -175,7 +190,7 @@ const videoAbuseReportValidator = [
 ]
 
 const videoRateValidator = [
-  param('id').custom(isVideoIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
+  param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
   body('rating').custom(isVideoRatingTypeValid).withMessage('Should have a valid rate type'),
 
   (req: express.Request, res: express.Response, next: express.NextFunction) => {
index e3de9468e1536738da90ecd827e638868350a7b4..dc8bcd872aa15b416cfd26c875d086acb93eb884 100644 (file)
@@ -126,7 +126,17 @@ getByTokenAndPopulateUser = function (bearerToken: string) {
     where: {
       accessToken: bearerToken
     },
-    include: [ OAuthToken['sequelize'].models.User ]
+    include: [
+      {
+        model: OAuthToken['sequelize'].models.User,
+        include: [
+          {
+            model: OAuthToken['sequelize'].models.Author,
+            required: true
+          }
+        ]
+      }
+    ]
   }
 
   return OAuthToken.findOne(query).then(token => {
@@ -141,7 +151,17 @@ getByRefreshTokenAndPopulateUser = function (refreshToken: string) {
     where: {
       refreshToken: refreshToken
     },
-    include: [ OAuthToken['sequelize'].models.User ]
+    include: [
+      {
+        model: OAuthToken['sequelize'].models.User,
+        include: [
+          {
+            model: OAuthToken['sequelize'].models.Author,
+            required: true
+          }
+        ]
+      }
+    ]
   }
 
   return OAuthToken.findOne(query).then(token => {
index 4862a57451632cd50f4fa0171527ede5b1b519df..34d5c71622149b2891b9557304cac17b6922ccc4 100644 (file)
@@ -85,7 +85,8 @@ listWithLimitAndRandom = function (limitPods: number, limitRequestsPerPod: numbe
   const Pod = db.Pod
 
   // We make a join between videos and authors to find the podId of our video event requests
-  const podJoins = 'INNER JOIN "Videos" ON "Videos"."authorId" = "Authors"."id" ' +
+  const podJoins = 'INNER JOIN "VideoChannels" ON "VideoChannels"."authorId" = "Authors"."id" ' +
+                   'INNER JOIN "Videos" ON "Videos"."channelId" = "VideoChannels"."id" ' +
                    'INNER JOIN "RequestVideoEvents" ON "RequestVideoEvents"."videoId" = "Videos"."id"'
 
   return Pod.listRandomPodIdsWithRequest(limitPods, 'Authors', podJoins).then(podIds => {
@@ -161,7 +162,7 @@ function groupAndTruncateRequests (events: RequestVideoEventInstance[], limitReq
   const eventsGrouped: RequestsVideoEventGrouped = {}
 
   events.forEach(event => {
-    const pod = event.Video.Author.Pod
+    const pod = event.Video.VideoChannel.Author.Pod
 
     if (!eventsGrouped[pod.id]) eventsGrouped[pod.id] = []
 
index 8974a9a97f32b6f82493aa72c1cc193cb227fdfe..1b5233eaff509b9e3463ae54fe8dae3b6a32ca6b 100644 (file)
@@ -5,6 +5,7 @@ import * as Promise from 'bluebird'
 import { User as FormattedUser } from '../../../shared/models/users/user.model'
 import { UserRole } from '../../../shared/models/users/user-role.type'
 import { ResultList } from '../../../shared/models/result-list.model'
+import { AuthorInstance } from '../video/author-interface'
 
 export namespace UserMethods {
   export type IsPasswordMatch = (this: UserInstance, password: string) => Promise<boolean>
@@ -17,13 +18,12 @@ export namespace UserMethods {
 
   export type GetByUsername = (username: string) => Promise<UserInstance>
 
-  export type List = () => Promise<UserInstance[]>
-
   export type ListForApi = (start: number, count: number, sort: string) => Promise< ResultList<UserInstance> >
 
   export type LoadById = (id: number) => Promise<UserInstance>
 
   export type LoadByUsername = (username: string) => Promise<UserInstance>
+  export type LoadByUsernameAndPopulateChannels = (username: string) => Promise<UserInstance>
 
   export type LoadByUsernameOrEmail = (username: string, email: string) => Promise<UserInstance>
 }
@@ -36,10 +36,10 @@ export interface UserClass {
 
   countTotal: UserMethods.CountTotal,
   getByUsername: UserMethods.GetByUsername,
-  list: UserMethods.List,
   listForApi: UserMethods.ListForApi,
   loadById: UserMethods.LoadById,
   loadByUsername: UserMethods.LoadByUsername,
+  loadByUsernameAndPopulateChannels: UserMethods.LoadByUsernameAndPopulateChannels,
   loadByUsernameOrEmail: UserMethods.LoadByUsernameOrEmail
 }
 
@@ -51,6 +51,8 @@ export interface UserAttributes {
   displayNSFW?: boolean
   role: UserRole
   videoQuota: number
+
+  Author?: AuthorInstance
 }
 
 export interface UserInstance extends UserClass, UserAttributes, Sequelize.Instance<UserAttributes> {
index 0dc52d3cfdaa3d19c2e0397b098111719e205a3c..f8598c40f4ecca27af4f046576135451faf96fd0 100644 (file)
@@ -27,10 +27,10 @@ let toFormattedJSON: UserMethods.ToFormattedJSON
 let isAdmin: UserMethods.IsAdmin
 let countTotal: UserMethods.CountTotal
 let getByUsername: UserMethods.GetByUsername
-let list: UserMethods.List
 let listForApi: UserMethods.ListForApi
 let loadById: UserMethods.LoadById
 let loadByUsername: UserMethods.LoadByUsername
+let loadByUsernameAndPopulateChannels: UserMethods.LoadByUsernameAndPopulateChannels
 let loadByUsernameOrEmail: UserMethods.LoadByUsernameOrEmail
 let isAbleToUploadVideo: UserMethods.IsAbleToUploadVideo
 
@@ -113,10 +113,10 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
 
     countTotal,
     getByUsername,
-    list,
     listForApi,
     loadById,
     loadByUsername,
+    loadByUsernameAndPopulateChannels,
     loadByUsernameOrEmail
   ]
   const instanceMethods = [
@@ -144,15 +144,34 @@ isPasswordMatch = function (this: UserInstance, password: string) {
 }
 
 toFormattedJSON = function (this: UserInstance) {
-  return {
+  const json = {
     id: this.id,
     username: this.username,
     email: this.email,
     displayNSFW: this.displayNSFW,
     role: this.role,
     videoQuota: this.videoQuota,
-    createdAt: this.createdAt
+    createdAt: this.createdAt,
+    author: {
+      id: this.Author.id,
+      uuid: this.Author.uuid
+    }
   }
+
+  if (Array.isArray(this.Author.VideoChannels) === true) {
+    const videoChannels = this.Author.VideoChannels
+      .map(c => c.toFormattedJSON())
+      .sort((v1, v2) => {
+        if (v1.createdAt < v2.createdAt) return -1
+        if (v1.createdAt === v2.createdAt) return 0
+
+        return 1
+      })
+
+    json['videoChannels'] = videoChannels
+  }
+
+  return json
 }
 
 isAdmin = function (this: UserInstance) {
@@ -189,21 +208,19 @@ getByUsername = function (username: string) {
   const query = {
     where: {
       username: username
-    }
+    },
+    include: [ { model: User['sequelize'].models.Author, required: true } ]
   }
 
   return User.findOne(query)
 }
 
-list = function () {
-  return User.findAll()
-}
-
 listForApi = function (start: number, count: number, sort: string) {
   const query = {
     offset: start,
     limit: count,
-    order: [ getSort(sort) ]
+    order: [ getSort(sort) ],
+    include: [ { model: User['sequelize'].models.Author, required: true } ]
   }
 
   return User.findAndCountAll(query).then(({ rows, count }) => {
@@ -215,14 +232,36 @@ listForApi = function (start: number, count: number, sort: string) {
 }
 
 loadById = function (id: number) {
-  return User.findById(id)
+  const options = {
+    include: [ { model: User['sequelize'].models.Author, required: true } ]
+  }
+
+  return User.findById(id, options)
 }
 
 loadByUsername = function (username: string) {
   const query = {
     where: {
       username
-    }
+    },
+    include: [ { model: User['sequelize'].models.Author, required: true } ]
+  }
+
+  return User.findOne(query)
+}
+
+loadByUsernameAndPopulateChannels = function (username: string) {
+  const query = {
+    where: {
+      username
+    },
+    include: [
+      {
+        model: User['sequelize'].models.Author,
+        required: true,
+        include: [ User['sequelize'].models.VideoChannel ]
+      }
+    ]
   }
 
   return User.findOne(query)
@@ -230,6 +269,7 @@ loadByUsername = function (username: string) {
 
 loadByUsernameOrEmail = function (username: string, email: string) {
   const query = {
+    include: [ { model: User['sequelize'].models.Author, required: true } ],
     where: {
       $or: [ { username }, { email } ]
     }
@@ -242,11 +282,12 @@ loadByUsernameOrEmail = function (username: string, email: string) {
 // ---------------------------------------------------------------------------
 
 function getOriginalVideoFileTotalFromUser (user: UserInstance) {
-  // Don't use sequelize because we need to use a subquery
+  // Don't use sequelize because we need to use a sub query
   const query = 'SELECT SUM("size") AS "total" FROM ' +
                 '(SELECT MAX("VideoFiles"."size") AS "size" FROM "VideoFiles" ' +
                 'INNER JOIN "Videos" ON "VideoFiles"."videoId" = "Videos"."id" ' +
-                'INNER JOIN "Authors" ON "Videos"."authorId" = "Authors"."id" ' +
+                'INNER JOIN "VideoChannels" ON "VideoChannels"."id" = "Videos"."channelId" ' +
+                'INNER JOIN "Authors" ON "VideoChannels"."authorId" = "Authors"."id" ' +
                 'INNER JOIN "Users" ON "Authors"."userId" = "Users"."id" ' +
                 'WHERE "Users"."id" = $userId GROUP BY "Videos"."id") t'
 
index 52a00a1d33c2e8d6b5c5aef94c958756a5c6cb58..fc69ff3c2b06b6d51c1979dca090b2bbfb87204c 100644 (file)
@@ -2,31 +2,44 @@ import * as Sequelize from 'sequelize'
 import * as Promise from 'bluebird'
 
 import { PodInstance } from '../pod/pod-interface'
+import { RemoteVideoAuthorCreateData } from '../../../shared/models/pods/remote-video/remote-video-author-create-request.model'
+import { VideoChannelInstance } from './video-channel-interface'
 
 export namespace AuthorMethods {
-  export type FindOrCreateAuthor = (
-    name: string,
-    podId: number,
-    userId: number,
-    transaction: Sequelize.Transaction
-  ) => Promise<AuthorInstance>
+  export type Load = (id: number) => Promise<AuthorInstance>
+  export type LoadByUUID = (uuid: string) => Promise<AuthorInstance>
+  export type LoadAuthorByPodAndUUID = (uuid: string, podId: number, transaction: Sequelize.Transaction) => Promise<AuthorInstance>
+  export type ListOwned = () => Promise<AuthorInstance[]>
+
+  export type ToAddRemoteJSON = (this: AuthorInstance) => RemoteVideoAuthorCreateData
+  export type IsOwned = (this: AuthorInstance) => boolean
 }
 
 export interface AuthorClass {
-  findOrCreateAuthor: AuthorMethods.FindOrCreateAuthor
+  loadAuthorByPodAndUUID: AuthorMethods.LoadAuthorByPodAndUUID
+  load: AuthorMethods.Load
+  loadByUUID: AuthorMethods.LoadByUUID
+  listOwned: AuthorMethods.ListOwned
 }
 
 export interface AuthorAttributes {
   name: string
+  uuid?: string
+
+  podId?: number
+  userId?: number
 }
 
 export interface AuthorInstance extends AuthorClass, AuthorAttributes, Sequelize.Instance<AuthorAttributes> {
+  isOwned: AuthorMethods.IsOwned
+  toAddRemoteJSON: AuthorMethods.ToAddRemoteJSON
+
   id: number
   createdAt: Date
   updatedAt: Date
 
-  podId: number
   Pod: PodInstance
+  VideoChannels: VideoChannelInstance[]
 }
 
 export interface AuthorModel extends AuthorClass, Sequelize.Model<AuthorInstance, AuthorAttributes> {}
index fd0f44f6b2e74a80699dd4fde49deffcd18524cf..6f27ea7bde223eaa389990b9e2bd5e7428c71934 100644 (file)
@@ -1,6 +1,7 @@
 import * as Sequelize from 'sequelize'
 
 import { isUserUsernameValid } from '../../helpers'
+import { removeVideoAuthorToFriends } from '../../lib'
 
 import { addMethodsToModel } from '../utils'
 import {
@@ -11,11 +12,24 @@ import {
 } from './author-interface'
 
 let Author: Sequelize.Model<AuthorInstance, AuthorAttributes>
-let findOrCreateAuthor: AuthorMethods.FindOrCreateAuthor
+let loadAuthorByPodAndUUID: AuthorMethods.LoadAuthorByPodAndUUID
+let load: AuthorMethods.Load
+let loadByUUID: AuthorMethods.LoadByUUID
+let listOwned: AuthorMethods.ListOwned
+let isOwned: AuthorMethods.IsOwned
+let toAddRemoteJSON: AuthorMethods.ToAddRemoteJSON
 
 export default function defineAuthor (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) {
   Author = sequelize.define<AuthorInstance, AuthorAttributes>('Author',
     {
+      uuid: {
+        type: DataTypes.UUID,
+        defaultValue: DataTypes.UUIDV4,
+        allowNull: false,
+        validate: {
+          isUUID: 4
+        }
+      },
       name: {
         type: DataTypes.STRING,
         allowNull: false,
@@ -43,12 +57,23 @@ export default function defineAuthor (sequelize: Sequelize.Sequelize, DataTypes:
           fields: [ 'name', 'podId' ],
           unique: true
         }
-      ]
+      ],
+      hooks: { afterDestroy }
     }
   )
 
-  const classMethods = [ associate, findOrCreateAuthor ]
-  addMethodsToModel(Author, classMethods)
+  const classMethods = [
+    associate,
+    loadAuthorByPodAndUUID,
+    load,
+    loadByUUID,
+    listOwned
+  ]
+  const instanceMethods = [
+    isOwned,
+    toAddRemoteJSON
+  ]
+  addMethodsToModel(Author, classMethods, instanceMethods)
 
   return Author
 }
@@ -72,27 +97,75 @@ function associate (models) {
     onDelete: 'cascade'
   })
 
-  Author.hasMany(models.Video, {
+  Author.hasMany(models.VideoChannel, {
     foreignKey: {
       name: 'authorId',
       allowNull: false
     },
-    onDelete: 'cascade'
+    onDelete: 'cascade',
+    hooks: true
   })
 }
 
-findOrCreateAuthor = function (name: string, podId: number, userId: number, transaction: Sequelize.Transaction) {
-  const author = {
-    name,
-    podId,
-    userId
+function afterDestroy (author: AuthorInstance, options: { transaction: Sequelize.Transaction }) {
+  if (author.isOwned()) {
+    const removeVideoAuthorToFriendsParams = {
+      uuid: author.uuid
+    }
+
+    return removeVideoAuthorToFriends(removeVideoAuthorToFriendsParams, options.transaction)
+  }
+
+  return undefined
+}
+
+toAddRemoteJSON = function (this: AuthorInstance) {
+  const json = {
+    uuid: this.uuid,
+    name: this.name
+  }
+
+  return json
+}
+
+isOwned = function (this: AuthorInstance) {
+  return this.podId === null
+}
+
+// ------------------------------ STATICS ------------------------------
+
+listOwned = function () {
+  const query: Sequelize.FindOptions<AuthorAttributes> = {
+    where: {
+      podId: null
+    }
+  }
+
+  return Author.findAll(query)
+}
+
+load = function (id: number) {
+  return Author.findById(id)
+}
+
+loadByUUID = function (uuid: string) {
+  const query: Sequelize.FindOptions<AuthorAttributes> = {
+    where: {
+      uuid
+    }
   }
 
-  const query: Sequelize.FindOrInitializeOptions<AuthorAttributes> = {
-    where: author,
-    defaults: author,
+  return Author.findOne(query)
+}
+
+loadAuthorByPodAndUUID = function (uuid: string, podId: number, transaction: Sequelize.Transaction) {
+  const query: Sequelize.FindOptions<AuthorAttributes> = {
+    where: {
+      podId,
+      uuid
+    },
     transaction
   }
 
-  return Author.findOrCreate(query).then(([ authorInstance ]) => authorInstance)
+  return Author.find(query)
 }
index 08b360376c6d832d4aff684c282ae86f56e2224c..dba6a559054a231b9e686ce54cf6fd1e2473bf4e 100644 (file)
@@ -2,6 +2,7 @@ export * from './author-interface'
 export * from './tag-interface'
 export * from './video-abuse-interface'
 export * from './video-blacklist-interface'
+export * from './video-channel-interface'
 export * from './video-tag-interface'
 export * from './video-file-interface'
 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 (file)
index 0000000..b8d3e0f
--- /dev/null
@@ -0,0 +1,64 @@
+import * as Sequelize from 'sequelize'
+import * as Promise from 'bluebird'
+
+import { ResultList, RemoteVideoChannelCreateData, RemoteVideoChannelUpdateData } from '../../../shared'
+
+// Don't use barrel, import just what we need
+import { VideoChannel as FormattedVideoChannel } from '../../../shared/models/videos/video-channel.model'
+import { AuthorInstance } from './author-interface'
+import { VideoInstance } from './video-interface'
+
+export namespace VideoChannelMethods {
+  export type ToFormattedJSON = (this: VideoChannelInstance) => FormattedVideoChannel
+  export type ToAddRemoteJSON = (this: VideoChannelInstance) => RemoteVideoChannelCreateData
+  export type ToUpdateRemoteJSON = (this: VideoChannelInstance) => RemoteVideoChannelUpdateData
+  export type IsOwned = (this: VideoChannelInstance) => boolean
+
+  export type CountByAuthor = (authorId: number) => Promise<number>
+  export type ListOwned = () => Promise<VideoChannelInstance[]>
+  export type ListForApi = (start: number, count: number, sort: string) => Promise< ResultList<VideoChannelInstance> >
+  export type LoadByIdAndAuthor = (id: number, authorId: number) => Promise<VideoChannelInstance>
+  export type ListByAuthor = (authorId: number) => Promise< ResultList<VideoChannelInstance> >
+  export type LoadAndPopulateAuthor = (id: number) => Promise<VideoChannelInstance>
+  export type LoadByUUIDAndPopulateAuthor = (uuid: string) => Promise<VideoChannelInstance>
+  export type LoadByUUID = (uuid: string, t?: Sequelize.Transaction) => Promise<VideoChannelInstance>
+  export type LoadByHostAndUUID = (uuid: string, podHost: string, t?: Sequelize.Transaction) => Promise<VideoChannelInstance>
+  export type LoadAndPopulateAuthorAndVideos = (id: number) => Promise<VideoChannelInstance>
+}
+
+export interface VideoChannelClass {
+  countByAuthor: VideoChannelMethods.CountByAuthor
+  listForApi: VideoChannelMethods.ListForApi
+  listByAuthor: VideoChannelMethods.ListByAuthor
+  listOwned: VideoChannelMethods.ListOwned
+  loadByIdAndAuthor: VideoChannelMethods.LoadByIdAndAuthor
+  loadByUUID: VideoChannelMethods.LoadByUUID
+  loadByHostAndUUID: VideoChannelMethods.LoadByHostAndUUID
+  loadAndPopulateAuthor: VideoChannelMethods.LoadAndPopulateAuthor
+  loadByUUIDAndPopulateAuthor: VideoChannelMethods.LoadByUUIDAndPopulateAuthor
+  loadAndPopulateAuthorAndVideos: VideoChannelMethods.LoadAndPopulateAuthorAndVideos
+}
+
+export interface VideoChannelAttributes {
+  id?: number
+  uuid?: string
+  name: string
+  description: string
+  remote: boolean
+
+  Author?: AuthorInstance
+  Videos?: VideoInstance[]
+}
+
+export interface VideoChannelInstance extends VideoChannelClass, VideoChannelAttributes, Sequelize.Instance<VideoChannelAttributes> {
+  id: number
+  createdAt: Date
+  updatedAt: Date
+
+  isOwned: VideoChannelMethods.IsOwned
+  toFormattedJSON: VideoChannelMethods.ToFormattedJSON
+  toAddRemoteJSON: VideoChannelMethods.ToAddRemoteJSON
+  toUpdateRemoteJSON: VideoChannelMethods.ToUpdateRemoteJSON
+}
+
+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 (file)
index 0000000..e469383
--- /dev/null
@@ -0,0 +1,349 @@
+import * as Sequelize from 'sequelize'
+
+import { isVideoChannelNameValid, isVideoChannelDescriptionValid } from '../../helpers'
+import { removeVideoChannelToFriends } from '../../lib'
+
+import { addMethodsToModel, getSort } from '../utils'
+import {
+  VideoChannelInstance,
+  VideoChannelAttributes,
+
+  VideoChannelMethods
+} from './video-channel-interface'
+
+let VideoChannel: Sequelize.Model<VideoChannelInstance, VideoChannelAttributes>
+let toFormattedJSON: VideoChannelMethods.ToFormattedJSON
+let toAddRemoteJSON: VideoChannelMethods.ToAddRemoteJSON
+let toUpdateRemoteJSON: VideoChannelMethods.ToUpdateRemoteJSON
+let isOwned: VideoChannelMethods.IsOwned
+let countByAuthor: VideoChannelMethods.CountByAuthor
+let listOwned: VideoChannelMethods.ListOwned
+let listForApi: VideoChannelMethods.ListForApi
+let listByAuthor: VideoChannelMethods.ListByAuthor
+let loadByIdAndAuthor: VideoChannelMethods.LoadByIdAndAuthor
+let loadByUUID: VideoChannelMethods.LoadByUUID
+let loadAndPopulateAuthor: VideoChannelMethods.LoadAndPopulateAuthor
+let loadByUUIDAndPopulateAuthor: VideoChannelMethods.LoadByUUIDAndPopulateAuthor
+let loadByHostAndUUID: VideoChannelMethods.LoadByHostAndUUID
+let loadAndPopulateAuthorAndVideos: VideoChannelMethods.LoadAndPopulateAuthorAndVideos
+
+export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) {
+  VideoChannel = sequelize.define<VideoChannelInstance, VideoChannelAttributes>('VideoChannel',
+    {
+      uuid: {
+        type: DataTypes.UUID,
+        defaultValue: DataTypes.UUIDV4,
+        allowNull: false,
+        validate: {
+          isUUID: 4
+        }
+      },
+      name: {
+        type: DataTypes.STRING,
+        allowNull: false,
+        validate: {
+          nameValid: value => {
+            const res = isVideoChannelNameValid(value)
+            if (res === false) throw new Error('Video channel name is not valid.')
+          }
+        }
+      },
+      description: {
+        type: DataTypes.STRING,
+        allowNull: true,
+        validate: {
+          descriptionValid: value => {
+            const res = isVideoChannelDescriptionValid(value)
+            if (res === false) throw new Error('Video channel description is not valid.')
+          }
+        }
+      },
+      remote: {
+        type: DataTypes.BOOLEAN,
+        allowNull: false,
+        defaultValue: false
+      }
+    },
+    {
+      indexes: [
+        {
+          fields: [ 'authorId' ]
+        }
+      ],
+      hooks: {
+        afterDestroy
+      }
+    }
+  )
+
+  const classMethods = [
+    associate,
+
+    listForApi,
+    listByAuthor,
+    listOwned,
+    loadByIdAndAuthor,
+    loadAndPopulateAuthor,
+    loadByUUIDAndPopulateAuthor,
+    loadByUUID,
+    loadByHostAndUUID,
+    loadAndPopulateAuthorAndVideos,
+    countByAuthor
+  ]
+  const instanceMethods = [
+    isOwned,
+    toFormattedJSON,
+    toAddRemoteJSON,
+    toUpdateRemoteJSON
+  ]
+  addMethodsToModel(VideoChannel, classMethods, instanceMethods)
+
+  return VideoChannel
+}
+
+// ------------------------------ METHODS ------------------------------
+
+isOwned = function (this: VideoChannelInstance) {
+  return this.remote === false
+}
+
+toFormattedJSON = function (this: VideoChannelInstance) {
+  const json = {
+    id: this.id,
+    uuid: this.uuid,
+    name: this.name,
+    description: this.description,
+    isLocal: this.isOwned(),
+    createdAt: this.createdAt,
+    updatedAt: this.updatedAt
+  }
+
+  if (this.Author !== undefined) {
+    json['owner'] = {
+      name: this.Author.name,
+      uuid: this.Author.uuid
+    }
+  }
+
+  if (Array.isArray(this.Videos)) {
+    json['videos'] = this.Videos.map(v => v.toFormattedJSON())
+  }
+
+  return json
+}
+
+toAddRemoteJSON = function (this: VideoChannelInstance) {
+  const json = {
+    uuid: this.uuid,
+    name: this.name,
+    description: this.description,
+    createdAt: this.createdAt,
+    updatedAt: this.updatedAt,
+    ownerUUID: this.Author.uuid
+  }
+
+  return json
+}
+
+toUpdateRemoteJSON = function (this: VideoChannelInstance) {
+  const json = {
+    uuid: this.uuid,
+    name: this.name,
+    description: this.description,
+    createdAt: this.createdAt,
+    updatedAt: this.updatedAt,
+    ownerUUID: this.Author.uuid
+  }
+
+  return json
+}
+
+// ------------------------------ STATICS ------------------------------
+
+function associate (models) {
+  VideoChannel.belongsTo(models.Author, {
+    foreignKey: {
+      name: 'authorId',
+      allowNull: false
+    },
+    onDelete: 'CASCADE'
+  })
+
+  VideoChannel.hasMany(models.Video, {
+    foreignKey: {
+      name: 'channelId',
+      allowNull: false
+    },
+    onDelete: 'CASCADE'
+  })
+}
+
+function afterDestroy (videoChannel: VideoChannelInstance, options: { transaction: Sequelize.Transaction }) {
+  if (videoChannel.isOwned()) {
+    const removeVideoChannelToFriendsParams = {
+      uuid: videoChannel.uuid
+    }
+
+    return removeVideoChannelToFriends(removeVideoChannelToFriendsParams, options.transaction)
+  }
+
+  return undefined
+}
+
+countByAuthor = function (authorId: number) {
+  const query = {
+    where: {
+      authorId
+    }
+  }
+
+  return VideoChannel.count(query)
+}
+
+listOwned = function () {
+  const query = {
+    where: {
+      remote: false
+    },
+    include: [ VideoChannel['sequelize'].models.Author ]
+  }
+
+  return VideoChannel.findAll(query)
+}
+
+listForApi = function (start: number, count: number, sort: string) {
+  const query = {
+    offset: start,
+    limit: count,
+    order: [ getSort(sort) ],
+    include: [
+      {
+        model: VideoChannel['sequelize'].models.Author,
+        required: true,
+        include: [ { model: VideoChannel['sequelize'].models.Pod, required: false } ]
+      }
+    ]
+  }
+
+  return VideoChannel.findAndCountAll(query).then(({ rows, count }) => {
+    return { total: count, data: rows }
+  })
+}
+
+listByAuthor = function (authorId: number) {
+  const query = {
+    order: [ getSort('createdAt') ],
+    include: [
+      {
+        model: VideoChannel['sequelize'].models.Author,
+        where: {
+          id: authorId
+        },
+        required: true,
+        include: [ { model: VideoChannel['sequelize'].models.Pod, required: false } ]
+      }
+    ]
+  }
+
+  return VideoChannel.findAndCountAll(query).then(({ rows, count }) => {
+    return { total: count, data: rows }
+  })
+}
+
+loadByUUID = function (uuid: string, t?: Sequelize.Transaction) {
+  const query: Sequelize.FindOptions<VideoChannelAttributes> = {
+    where: {
+      uuid
+    }
+  }
+
+  if (t !== undefined) query.transaction = t
+
+  return VideoChannel.findOne(query)
+}
+
+loadByHostAndUUID = function (fromHost: string, uuid: string, t?: Sequelize.Transaction) {
+  const query: Sequelize.FindOptions<VideoChannelAttributes> = {
+    where: {
+      uuid
+    },
+    include: [
+      {
+        model: VideoChannel['sequelize'].models.Author,
+        include: [
+          {
+            model: VideoChannel['sequelize'].models.Pod,
+            required: true,
+            where: {
+              host: fromHost
+            }
+          }
+        ]
+      }
+    ]
+  }
+
+  if (t !== undefined) query.transaction = t
+
+  return VideoChannel.findOne(query)
+}
+
+loadByIdAndAuthor = function (id: number, authorId: number) {
+  const options = {
+    where: {
+      id,
+      authorId
+    },
+    include: [
+      {
+        model: VideoChannel['sequelize'].models.Author,
+        include: [ { model: VideoChannel['sequelize'].models.Pod, required: false } ]
+      }
+    ]
+  }
+
+  return VideoChannel.findOne(options)
+}
+
+loadAndPopulateAuthor = function (id: number) {
+  const options = {
+    include: [
+      {
+        model: VideoChannel['sequelize'].models.Author,
+        include: [ { model: VideoChannel['sequelize'].models.Pod, required: false } ]
+      }
+    ]
+  }
+
+  return VideoChannel.findById(id, options)
+}
+
+loadByUUIDAndPopulateAuthor = function (uuid: string) {
+  const options = {
+    where: {
+      uuid
+    },
+    include: [
+      {
+        model: VideoChannel['sequelize'].models.Author,
+        include: [ { model: VideoChannel['sequelize'].models.Pod, required: false } ]
+      }
+    ]
+  }
+
+  return VideoChannel.findOne(options)
+}
+
+loadAndPopulateAuthorAndVideos = function (id: number) {
+  const options = {
+    include: [
+      {
+        model: VideoChannel['sequelize'].models.Author,
+        include: [ { model: VideoChannel['sequelize'].models.Pod, required: false } ]
+      },
+      VideoChannel['sequelize'].models.Video
+    ]
+  }
+
+  return VideoChannel.findById(id, options)
+}
index 86ce84dd99335bd8b9d5d0b50d1f3063c855d617..4b5ae08c20f2a6bcb77f0caa5b7cc05ceff403a9 100644 (file)
@@ -6,16 +6,21 @@ import { TagAttributes, TagInstance } from './tag-interface'
 import { VideoFileAttributes, VideoFileInstance } from './video-file-interface'
 
 // Don't use barrel, import just what we need
-import { Video as FormattedVideo } from '../../../shared/models/videos/video.model'
+import {
+  Video as FormattedVideo,
+  VideoDetails as FormattedDetailsVideo
+} from '../../../shared/models/videos/video.model'
 import { RemoteVideoUpdateData } from '../../../shared/models/pods/remote-video/remote-video-update-request.model'
 import { RemoteVideoCreateData } from '../../../shared/models/pods/remote-video/remote-video-create-request.model'
 import { ResultList } from '../../../shared/models/result-list.model'
+import { VideoChannelInstance } from './video-channel-interface'
 
 export namespace VideoMethods {
   export type GetThumbnailName = (this: VideoInstance) => string
   export type GetPreviewName = (this: VideoInstance) => string
   export type IsOwned = (this: VideoInstance) => boolean
   export type ToFormattedJSON = (this: VideoInstance) => FormattedVideo
+  export type ToFormattedDetailsJSON = (this: VideoInstance) => FormattedDetailsVideo
 
   export type GetOriginalFile = (this: VideoInstance) => VideoFileInstance
   export type GetTorrentFileName = (this: VideoInstance, videoFile: VideoFileInstance) => string
@@ -52,8 +57,8 @@ export namespace VideoMethods {
   ) => Promise< ResultList<VideoInstance> >
 
   export type Load = (id: number) => Promise<VideoInstance>
-  export type LoadByUUID = (uuid: string) => Promise<VideoInstance>
-  export type LoadByHostAndUUID = (fromHost: string, uuid: string) => Promise<VideoInstance>
+  export type LoadByUUID = (uuid: string, t?: Sequelize.Transaction) => Promise<VideoInstance>
+  export type LoadByHostAndUUID = (fromHost: string, uuid: string, t?: Sequelize.Transaction) => Promise<VideoInstance>
   export type LoadAndPopulateAuthor = (id: number) => Promise<VideoInstance>
   export type LoadAndPopulateAuthorAndPodAndTags = (id: number) => Promise<VideoInstance>
   export type LoadByUUIDAndPopulateAuthorAndPodAndTags = (uuid: string) => Promise<VideoInstance>
@@ -94,7 +99,9 @@ export interface VideoAttributes {
   dislikes?: number
   remote: boolean
 
-  Author?: AuthorInstance
+  channelId?: number
+
+  VideoChannel?: VideoChannelInstance
   Tags?: TagInstance[]
   VideoFiles?: VideoFileInstance[]
 }
@@ -121,6 +128,7 @@ export interface VideoInstance extends VideoClass, VideoAttributes, Sequelize.In
   removeTorrent: VideoMethods.RemoveTorrent
   toAddRemoteJSON: VideoMethods.ToAddRemoteJSON
   toFormattedJSON: VideoMethods.ToFormattedJSON
+  toFormattedDetailsJSON: VideoMethods.ToFormattedDetailsJSON
   toUpdateRemoteJSON: VideoMethods.ToUpdateRemoteJSON
   optimizeOriginalVideofile: VideoMethods.OptimizeOriginalVideofile
   transcodeOriginalVideofile: VideoMethods.TranscodeOriginalVideofile
index 0b1af4d21efb9963f2382a875986e39b47b955cd..d9b9764042b45bab0a3030eda67e7a7272049a6e 100644 (file)
@@ -60,6 +60,7 @@ let getPreviewPath: VideoMethods.GetPreviewPath
 let getTorrentFileName: VideoMethods.GetTorrentFileName
 let isOwned: VideoMethods.IsOwned
 let toFormattedJSON: VideoMethods.ToFormattedJSON
+let toFormattedDetailsJSON: VideoMethods.ToFormattedDetailsJSON
 let toAddRemoteJSON: VideoMethods.ToAddRemoteJSON
 let toUpdateRemoteJSON: VideoMethods.ToUpdateRemoteJSON
 let optimizeOriginalVideofile: VideoMethods.OptimizeOriginalVideofile
@@ -205,9 +206,6 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
     },
     {
       indexes: [
-        {
-          fields: [ 'authorId' ]
-        },
         {
           fields: [ 'name' ]
         },
@@ -225,6 +223,9 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
         },
         {
           fields: [ 'uuid' ]
+        },
+        {
+          fields: [ 'channelId' ]
         }
       ],
       hooks: {
@@ -268,6 +269,7 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
     removeTorrent,
     toAddRemoteJSON,
     toFormattedJSON,
+    toFormattedDetailsJSON,
     toUpdateRemoteJSON,
     optimizeOriginalVideofile,
     transcodeOriginalVideofile,
@@ -282,9 +284,9 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
 // ------------------------------ METHODS ------------------------------
 
 function associate (models) {
-  Video.belongsTo(models.Author, {
+  Video.belongsTo(models.VideoChannel, {
     foreignKey: {
-      name: 'authorId',
+      name: 'channelId',
       allowNull: false
     },
     onDelete: 'cascade'
@@ -439,8 +441,8 @@ getPreviewPath = function (this: VideoInstance) {
 toFormattedJSON = function (this: VideoInstance) {
   let podHost
 
-  if (this.Author.Pod) {
-    podHost = this.Author.Pod.host
+  if (this.VideoChannel.Author.Pod) {
+    podHost = this.VideoChannel.Author.Pod.host
   } else {
     // It means it's our video
     podHost = CONFIG.WEBSERVER.HOST
@@ -472,7 +474,59 @@ toFormattedJSON = function (this: VideoInstance) {
     description: this.description,
     podHost,
     isLocal: this.isOwned(),
-    author: this.Author.name,
+    author: this.VideoChannel.Author.name,
+    duration: this.duration,
+    views: this.views,
+    likes: this.likes,
+    dislikes: this.dislikes,
+    tags: map<TagInstance, string>(this.Tags, 'name'),
+    thumbnailPath: this.getThumbnailPath(),
+    previewPath: this.getPreviewPath(),
+    embedPath: this.getEmbedPath(),
+    createdAt: this.createdAt,
+    updatedAt: this.updatedAt
+  }
+
+  return json
+}
+
+toFormattedDetailsJSON = function (this: VideoInstance) {
+  let podHost
+
+  if (this.VideoChannel.Author.Pod) {
+    podHost = this.VideoChannel.Author.Pod.host
+  } else {
+    // It means it's our video
+    podHost = CONFIG.WEBSERVER.HOST
+  }
+
+  // Maybe our pod is not up to date and there are new categories since our version
+  let categoryLabel = VIDEO_CATEGORIES[this.category]
+  if (!categoryLabel) categoryLabel = 'Misc'
+
+  // Maybe our pod is not up to date and there are new licences since our version
+  let licenceLabel = VIDEO_LICENCES[this.licence]
+  if (!licenceLabel) licenceLabel = 'Unknown'
+
+  // Language is an optional attribute
+  let languageLabel = VIDEO_LANGUAGES[this.language]
+  if (!languageLabel) languageLabel = 'Unknown'
+
+  const json = {
+    id: this.id,
+    uuid: this.uuid,
+    name: this.name,
+    category: this.category,
+    categoryLabel,
+    licence: this.licence,
+    licenceLabel,
+    language: this.language,
+    languageLabel,
+    nsfw: this.nsfw,
+    description: this.description,
+    podHost,
+    isLocal: this.isOwned(),
+    author: this.VideoChannel.Author.name,
     duration: this.duration,
     views: this.views,
     likes: this.likes,
@@ -483,6 +537,7 @@ toFormattedJSON = function (this: VideoInstance) {
     embedPath: this.getEmbedPath(),
     createdAt: this.createdAt,
     updatedAt: this.updatedAt,
+    channel: this.VideoChannel.toFormattedJSON(),
     files: []
   }
 
@@ -525,7 +580,7 @@ toAddRemoteJSON = function (this: VideoInstance) {
       language: this.language,
       nsfw: this.nsfw,
       description: this.description,
-      author: this.Author.name,
+      channelUUID: this.VideoChannel.uuid,
       duration: this.duration,
       thumbnailData: thumbnailData.toString('binary'),
       tags: map<TagInstance, string>(this.Tags, 'name'),
@@ -559,7 +614,6 @@ toUpdateRemoteJSON = function (this: VideoInstance) {
     language: this.language,
     nsfw: this.nsfw,
     description: this.description,
-    author: this.Author.name,
     duration: this.duration,
     tags: map<TagInstance, string>(this.Tags, 'name'),
     createdAt: this.createdAt,
@@ -723,8 +777,18 @@ listForApi = function (start: number, count: number, sort: string) {
     order: [ getSort(sort), [ Video['sequelize'].models.Tag, 'name', 'ASC' ] ],
     include: [
       {
-        model: Video['sequelize'].models.Author,
-        include: [ { model: Video['sequelize'].models.Pod, required: false } ]
+        model: Video['sequelize'].models.VideoChannel,
+        include: [
+          {
+            model: Video['sequelize'].models.Author,
+            include: [
+              {
+                model: Video['sequelize'].models.Pod,
+                required: false
+              }
+            ]
+          }
+        ]
       },
       Video['sequelize'].models.Tag,
       Video['sequelize'].models.VideoFile
@@ -740,8 +804,8 @@ listForApi = function (start: number, count: number, sort: string) {
   })
 }
 
-loadByHostAndUUID = function (fromHost: string, uuid: string) {
-  const query = {
+loadByHostAndUUID = function (fromHost: string, uuid: string, t?: Sequelize.Transaction) {
+  const query: Sequelize.FindOptions<VideoAttributes> = {
     where: {
       uuid
     },
@@ -750,20 +814,27 @@ loadByHostAndUUID = function (fromHost: string, uuid: string) {
         model: Video['sequelize'].models.VideoFile
       },
       {
-        model: Video['sequelize'].models.Author,
+        model: Video['sequelize'].models.VideoChannel,
         include: [
           {
-            model: Video['sequelize'].models.Pod,
-            required: true,
-            where: {
-              host: fromHost
-            }
+            model: Video['sequelize'].models.Author,
+            include: [
+              {
+                model: Video['sequelize'].models.Pod,
+                required: true,
+                where: {
+                  host: fromHost
+                }
+              }
+            ]
           }
         ]
       }
     ]
   }
 
+  if (t !== undefined) query.transaction = t
+
   return Video.findOne(query)
 }
 
@@ -774,7 +845,10 @@ listOwnedAndPopulateAuthorAndTags = function () {
     },
     include: [
       Video['sequelize'].models.VideoFile,
-      Video['sequelize'].models.Author,
+      {
+        model: Video['sequelize'].models.VideoChannel,
+        include: [ Video['sequelize'].models.Author ]
+      },
       Video['sequelize'].models.Tag
     ]
   }
@@ -792,10 +866,15 @@ listOwnedByAuthor = function (author: string) {
         model: Video['sequelize'].models.VideoFile
       },
       {
-        model: Video['sequelize'].models.Author,
-        where: {
-          name: author
-        }
+        model: Video['sequelize'].models.VideoChannel,
+        include: [
+          {
+            model: Video['sequelize'].models.Author,
+            where: {
+              name: author
+            }
+          }
+        ]
       }
     ]
   }
@@ -807,19 +886,28 @@ load = function (id: number) {
   return Video.findById(id)
 }
 
-loadByUUID = function (uuid: string) {
-  const query = {
+loadByUUID = function (uuid: string, t?: Sequelize.Transaction) {
+  const query: Sequelize.FindOptions<VideoAttributes> = {
     where: {
       uuid
     },
     include: [ Video['sequelize'].models.VideoFile ]
   }
+
+  if (t !== undefined) query.transaction = t
+
   return Video.findOne(query)
 }
 
 loadAndPopulateAuthor = function (id: number) {
   const options = {
-    include: [ Video['sequelize'].models.VideoFile, Video['sequelize'].models.Author ]
+    include: [
+      Video['sequelize'].models.VideoFile,
+      {
+        model: Video['sequelize'].models.VideoChannel,
+        include: [ Video['sequelize'].models.Author ]
+      }
+    ]
   }
 
   return Video.findById(id, options)
@@ -829,8 +917,13 @@ loadAndPopulateAuthorAndPodAndTags = function (id: number) {
   const options = {
     include: [
       {
-        model: Video['sequelize'].models.Author,
-        include: [ { model: Video['sequelize'].models.Pod, required: false } ]
+        model: Video['sequelize'].models.VideoChannel,
+        include: [
+          {
+            model: Video['sequelize'].models.Author,
+            include: [ { model: Video['sequelize'].models.Pod, required: false } ]
+          }
+        ]
       },
       Video['sequelize'].models.Tag,
       Video['sequelize'].models.VideoFile
@@ -847,8 +940,13 @@ loadByUUIDAndPopulateAuthorAndPodAndTags = function (uuid: string) {
     },
     include: [
       {
-        model: Video['sequelize'].models.Author,
-        include: [ { model: Video['sequelize'].models.Pod, required: false } ]
+        model: Video['sequelize'].models.VideoChannel,
+        include: [
+          {
+            model: Video['sequelize'].models.Author,
+            include: [ { model: Video['sequelize'].models.Pod, required: false } ]
+          }
+        ]
       },
       Video['sequelize'].models.Tag,
       Video['sequelize'].models.VideoFile
@@ -866,9 +964,13 @@ searchAndPopulateAuthorAndPodAndTags = function (value: string, field: string, s
 
   const authorInclude: Sequelize.IncludeOptions = {
     model: Video['sequelize'].models.Author,
-    include: [
-      podInclude
-    ]
+    include: [ podInclude ]
+  }
+
+  const videoChannelInclude: Sequelize.IncludeOptions = {
+    model: Video['sequelize'].models.VideoChannel,
+    include: [ authorInclude ],
+    required: true
   }
 
   const tagInclude: Sequelize.IncludeOptions = {
@@ -915,8 +1017,6 @@ searchAndPopulateAuthorAndPodAndTags = function (value: string, field: string, s
         $iLike: '%' + value + '%'
       }
     }
-
-    // authorInclude.or = true
   } else {
     query.where[field] = {
       $iLike: '%' + value + '%'
@@ -924,7 +1024,7 @@ searchAndPopulateAuthorAndPodAndTags = function (value: string, field: string, s
   }
 
   query.include = [
-    authorInclude, tagInclude, videoFileInclude
+    videoChannelInclude, tagInclude, videoFileInclude
   ]
 
   return Video.findAndCountAll(query).then(({ rows, count }) => {
@@ -955,8 +1055,8 @@ function getBaseUrls (video: VideoInstance) {
     baseUrlHttp = CONFIG.WEBSERVER.URL
     baseUrlWs = CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT
   } else {
-    baseUrlHttp = REMOTE_SCHEME.HTTP + '://' + video.Author.Pod.host
-    baseUrlWs = REMOTE_SCHEME.WS + '://' + video.Author.Pod.host
+    baseUrlHttp = REMOTE_SCHEME.HTTP + '://' + video.VideoChannel.Author.Pod.host
+    baseUrlWs = REMOTE_SCHEME.WS + '://' + video.VideoChannel.Author.Pod.host
   }
 
   return { baseUrlHttp, baseUrlWs }
index c8811684991413896450c3ba4746faa6c4a93a37..918b9e1f69a221ff23f044a4766a1844d0db9795 100644 (file)
@@ -1,7 +1,12 @@
 export * from './remote-qadu-video-request.model'
+export * from './remote-video-author-create-request.model'
+export * from './remote-video-author-remove-request.model'
 export * from './remote-video-event-request.model'
 export * from './remote-video-request.model'
 export * from './remote-video-create-request.model'
 export * from './remote-video-update-request.model'
 export * from './remote-video-remove-request.model'
+export * from './remote-video-channel-create-request.model'
+export * from './remote-video-channel-update-request.model'
+export * from './remote-video-channel-remove-request.model'
 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 (file)
index 0000000..ae364d1
--- /dev/null
@@ -0,0 +1,11 @@
+import { RemoteVideoRequest } from './remote-video-request.model'
+
+export interface RemoteVideoAuthorCreateData {
+  uuid: string
+  name: string
+}
+
+export interface RemoteVideoAuthorCreateRequest extends RemoteVideoRequest {
+  type: 'add-author'
+  data: RemoteVideoAuthorCreateData
+}
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 (file)
index 0000000..8738e92
--- /dev/null
@@ -0,0 +1,10 @@
+import { RemoteVideoRequest } from './remote-video-request.model'
+
+export interface RemoteVideoAuthorRemoveData {
+  uuid: string
+}
+
+export interface RemoteVideoAuthorRemoveRequest extends RemoteVideoRequest {
+  type: 'remove-author'
+  data: RemoteVideoAuthorRemoveData
+}
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 (file)
index 0000000..54163a2
--- /dev/null
@@ -0,0 +1,15 @@
+import { RemoteVideoRequest } from './remote-video-request.model'
+
+export interface RemoteVideoChannelCreateData {
+  uuid: string
+  name: string
+  description: string
+  createdAt: Date
+  updatedAt: Date
+  ownerUUID: string
+}
+
+export interface RemoteVideoChannelCreateRequest extends RemoteVideoRequest {
+  type: 'add-channel'
+  data: RemoteVideoChannelCreateData
+}
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 (file)
index 0000000..7aa3fe1
--- /dev/null
@@ -0,0 +1,10 @@
+import { RemoteVideoRequest } from './remote-video-request.model'
+
+export interface RemoteVideoChannelRemoveData {
+  uuid: string
+}
+
+export interface RemoteVideoChannelRemoveRequest extends RemoteVideoRequest {
+  type: 'remove-channel'
+  data: RemoteVideoChannelRemoveData
+}
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 (file)
index 0000000..70250d1
--- /dev/null
@@ -0,0 +1,15 @@
+import { RemoteVideoRequest } from './remote-video-request.model'
+
+export interface RemoteVideoChannelUpdateData {
+  uuid: string
+  name: string
+  description: string
+  createdAt: Date
+  updatedAt: Date
+  ownerUUID: string
+}
+
+export interface RemoteVideoChannelUpdateRequest extends RemoteVideoRequest {
+  type: 'update-channel'
+  data: RemoteVideoChannelUpdateData
+}
index 98425e4d9d91c6b6acd504b67d3d76c32a31515c..e00e81214499d167fc2fd6fde5e56b7a5c509ac4 100644 (file)
@@ -2,7 +2,7 @@ import { RemoteVideoRequest } from './remote-video-request.model'
 
 export interface RemoteVideoCreateData {
   uuid: string
-  author: string
+  channelUUID: string
   tags: string[]
   name: string
   category: number
@@ -26,6 +26,6 @@ export interface RemoteVideoCreateData {
 }
 
 export interface RemoteVideoCreateRequest extends RemoteVideoRequest {
-  type: 'add'
+  type: 'add-video'
   data: RemoteVideoCreateData
 }
index 0686dc8ab963bbf34080923bab8c532e8dc5b0f8..79a5e0a5f4ba659ccf417307dca9e7ce0fec7821 100644 (file)
@@ -5,6 +5,6 @@ export interface RemoteVideoRemoveData {
 }
 
 export interface RemoteVideoRemoveRequest extends RemoteVideoRequest {
-  type: 'remove'
+  type: 'remove-video'
   data: RemoteVideoRemoveData
 }
index e5052a23df66b806a3cdfbc9c29dae410a3bc1da..56f8136c283e385c6d90bb720698f96df33857a4 100644 (file)
@@ -1,4 +1,9 @@
 export interface RemoteVideoRequest {
-  type: 'add' | 'update' | 'remove' | 'report-abuse'
+  type: RemoteVideoRequestType
   data: any
 }
+
+export type RemoteVideoRequestType = 'add-video' | 'update-video' | 'remove-video' |
+                                     'add-channel' | 'update-channel' | 'remove-channel' |
+                                     'report-abuse' |
+                                     'add-author' | 'remove-author'
index 7f34a30ae82ab3c2bfdc8eebacf73219e497d7e5..90c42fc28f968bf563434ed85f21ec7a0d0335c7 100644 (file)
@@ -1,3 +1,5 @@
+import { RemoteVideoRequest } from './remote-video-request.model'
+
 export interface RemoteVideoUpdateData {
   uuid: string
   tags: string[]
@@ -21,7 +23,7 @@ export interface RemoteVideoUpdateData {
   }[]
 }
 
-export interface RemoteVideoUpdateRequest {
-  type: 'update'
+export interface RemoteVideoUpdateRequest extends RemoteVideoRequest {
+  type: 'update-video'
   data: RemoteVideoUpdateData
 }
index 867a6dde5bad19bcdd96b65f8ab53889c0a4097d..175e72f28457ed3cb0862f5d164cf2743f57f761 100644 (file)
@@ -1,4 +1,5 @@
 import { UserRole } from './user-role.type'
+import { VideoChannel } from '../videos/video-channel.model'
 
 export interface User {
   id: number
@@ -7,5 +8,10 @@ export interface User {
   displayNSFW: boolean
   role: UserRole
   videoQuota: number
-  createdAt: Date
+  createdAt: Date,
+  author: {
+    id: number
+    uuid: string
+  }
+  videoChannels?: VideoChannel[]
 }
index 35144dbadcec7678d676b9e2114a420d1b2171bc..2a3912f06a8a72cd4549c9c85b2f230a7ca04976 100644 (file)
@@ -4,6 +4,9 @@ export * from './user-video-rate.type'
 export * from './video-abuse-create.model'
 export * from './video-abuse.model'
 export * from './video-blacklist.model'
+export * from './video-channel-create.model'
+export * from './video-channel-update.model'
+export * from './video-channel.model'
 export * from './video-create.model'
 export * from './video-rate.type'
 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 (file)
index 0000000..f309c8f
--- /dev/null
@@ -0,0 +1,4 @@
+export interface VideoChannelCreate {
+  name: string
+  description?: string
+}
diff --git a/shared/models/videos/video-channel-update.model.ts b/shared/models/videos/video-channel-update.model.ts
new file mode 100644 (file)
index 0000000..4e98e39
--- /dev/null
@@ -0,0 +1,4 @@
+export interface VideoChannelUpdate {
+  name: string
+  description: string
+}
diff --git a/shared/models/videos/video-channel.model.ts b/shared/models/videos/video-channel.model.ts
new file mode 100644 (file)
index 0000000..ee56c54
--- /dev/null
@@ -0,0 +1,15 @@
+import { Video } from './video.model'
+
+export interface VideoChannel {
+  id: number
+  name: string
+  description: string
+  isLocal: boolean
+  createdAt: Date | string
+  updatedAt: Date | string
+  owner?: {
+    name: string
+    uuid: string
+  }
+  videos?: Video[]
+}
index 5c0b498ce685fdf7605d1b7f3d79fca3c7d6ca74..4d0e83520fe3a8da392bd23e000bf0de3b640575 100644 (file)
@@ -3,6 +3,7 @@ export interface VideoCreate {
   licence: number
   language: number
   description: string
+  channelId: number
   nsfw: boolean
   name: string
   tags: string[]
index 8e47ac06969e28d65e7b31f0fc7a7d0c0dd9f19b..32463933d3e7ff0497eba46ef542191121e992ec 100644 (file)
@@ -1,3 +1,5 @@
+import { VideoChannel } from './video-channel.model'
+
 export interface VideoFile {
   magnetUri: string
   resolution: number
@@ -32,5 +34,9 @@ export interface Video {
   likes: number
   dislikes: number
   nsfw: boolean
+}
+
+export interface VideoDetails extends Video {
+  channel: VideoChannel
   files: VideoFile[]
 }