Add video channels

This commit is contained in:
Chocobozzz 2017-10-24 19:41:09 +02:00
parent 8113a93a0d
commit 72c7248b6f
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
56 changed files with 2011 additions and 280 deletions

View 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()

View 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
})
}

View 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))
}

View File

@ -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))
}

View 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) {

View 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
}

View 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'

View 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
}

View 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 isRequestTypeUpdateValid (value: string) {
return value === ENDPOINT_ACTIONS.UPDATE
function checkUpdateVideo (video: any) {
return isCommonVideoAttributesValid(video)
}
function isRequestTypeRemoveValid (value: string) {
return value === ENDPOINT_ACTIONS.REMOVE
function checkRemoveVideo (video: any) {
return isUUIDValid(video.uuid)
}
function isRequestTypeReportAbuseValid (value: string) {
return value === ENDPOINT_ACTIONS.REPORT_ABUSE
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 checkRemoveVideoChannel (videoChannel: any) {
return isUUIDValid(videoChannel.uuid)
}
function checkAddAuthor (author: any) {
return isUUIDValid(author.uuid) &&
isVideoAuthorNameValid(author.name)
}
function checkRemoveAuthor (author: any) {
return isUUIDValid(author.uuid)
}

View File

@ -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
}

View File

@ -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
}

View 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,

View 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'
}

View 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,

View 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 })
})
})
}

View 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())

View 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[]

View 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'

46
server/lib/user.ts Normal file
View File

@ -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
}

View File

@ -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
}

View 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
}

View File

@ -6,3 +6,4 @@ export * from './sort'
export * from './users'
export * from './videos'
export * from './video-blacklist'
export * from './video-channels'

View 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()

View 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
}

View 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 })

View 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 })

View File

@ -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()
})
}

View 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) => {

View 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 => {

View 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] = []

View 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> {

View 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'

View 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> {}

View 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)
}
const query: Sequelize.FindOrInitializeOptions<AuthorAttributes> = {
where: author,
defaults: author,
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
}
}
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)
}

View 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'

View File

@ -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> {}

View File

@ -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)
}

View 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

View 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 }

View 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'

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View 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
}

View File

@ -5,6 +5,6 @@ export interface RemoteVideoRemoveData {
}
export interface RemoteVideoRemoveRequest extends RemoteVideoRequest {
type: 'remove'
type: 'remove-video'
data: RemoteVideoRemoveData
}

View 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'

View 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
}

View 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[]
}

View 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'

View File

@ -0,0 +1,4 @@
export interface VideoChannelCreate {
name: string
description?: string
}

View File

@ -0,0 +1,4 @@
export interface VideoChannelUpdate {
name: string
description: string
}

View File

@ -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[]
}

View File

@ -3,6 +3,7 @@ export interface VideoCreate {
licence: number
language: number
description: string
channelId: number
nsfw: boolean
name: string
tags: string[]

View 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[]
}