+
+
+
+
+
+
diff --git a/client/src/app/videos/video-watch/video-watch.component.scss b/client/src/app/videos/video-watch/video-watch.component.scss
index 0b8af52ce..5f322a194 100644
--- a/client/src/app/videos/video-watch/video-watch.component.scss
+++ b/client/src/app/videos/video-watch/video-watch.component.scss
@@ -47,6 +47,34 @@
top: 2px;
}
+ #rates {
+ display: inline-block;
+ margin-right: 20px;
+
+ // Remove focus style
+ .btn:focus {
+ outline: 0;
+ }
+
+ .activated-btn {
+ color: #333;
+ background-color: #e6e6e6;
+ border-color: #8c8c8c;
+ }
+
+ .not-interactive-btn {
+ cursor: default;
+
+ &:hover, &:focus, &:active {
+ color: #333;
+ background-color: #fff;
+ border-color: #ccc;
+ box-shadow: none;
+ outline: 0;
+ }
+ }
+ }
+
#share, #more {
font-weight: bold;
opacity: 0.85;
diff --git a/client/src/app/videos/video-watch/video-watch.component.ts b/client/src/app/videos/video-watch/video-watch.component.ts
index d1abc81bc..ed6b30102 100644
--- a/client/src/app/videos/video-watch/video-watch.component.ts
+++ b/client/src/app/videos/video-watch/video-watch.component.ts
@@ -10,7 +10,7 @@ import { AuthService } from '../../core';
import { VideoMagnetComponent } from './video-magnet.component';
import { VideoShareComponent } from './video-share.component';
import { VideoReportComponent } from './video-report.component';
-import { Video, VideoService } from '../shared';
+import { RateType, Video, VideoService } from '../shared';
import { WebTorrentService } from './webtorrent.service';
@Component({
@@ -33,6 +33,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
player: VideoJSPlayer;
playerElement: Element;
uploadSpeed: number;
+ userRating: RateType = null;
video: Video = null;
videoNotFound = false;
@@ -61,6 +62,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
this.video = video;
this.setOpenGraphTags();
this.loadVideo();
+ this.checkUserRating();
},
error => {
this.videoNotFound = true;
@@ -136,6 +138,40 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
});
}
+ setLike() {
+ if (this.isUserLoggedIn() === false) return;
+ // Already liked this video
+ if (this.userRating === 'like') return;
+
+ this.videoService.setVideoLike(this.video.id)
+ .subscribe(
+ () => {
+ // Update the video like attribute
+ this.updateVideoRating(this.userRating, 'like');
+ this.userRating = 'like';
+ },
+
+ err => this.notificationsService.error('Error', err.text)
+ );
+ }
+
+ setDislike() {
+ if (this.isUserLoggedIn() === false) return;
+ // Already disliked this video
+ if (this.userRating === 'dislike') return;
+
+ this.videoService.setVideoDislike(this.video.id)
+ .subscribe(
+ () => {
+ // Update the video dislike attribute
+ this.updateVideoRating(this.userRating, 'dislike');
+ this.userRating = 'dislike';
+ },
+
+ err => this.notificationsService.error('Error', err.text)
+ );
+ }
+
showReportModal(event: Event) {
event.preventDefault();
this.videoReportModal.show();
@@ -154,6 +190,38 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
return this.authService.isLoggedIn();
}
+ private checkUserRating() {
+ // Unlogged users do not have ratings
+ if (this.isUserLoggedIn() === false) return;
+
+ this.videoService.getUserVideoRating(this.video.id)
+ .subscribe(
+ ratingObject => {
+ if (ratingObject) {
+ this.userRating = ratingObject.rating;
+ }
+ },
+
+ err => this.notificationsService.error('Error', err.text)
+ );
+ }
+
+ private updateVideoRating(oldRating: RateType, newRating: RateType) {
+ let likesToIncrement = 0;
+ let dislikesToIncrement = 0;
+
+ if (oldRating) {
+ if (oldRating === 'like') likesToIncrement--;
+ if (oldRating === 'dislike') dislikesToIncrement--;
+ }
+
+ if (newRating === 'like') likesToIncrement++;
+ if (newRating === 'dislike') dislikesToIncrement++;
+
+ this.video.likes += likesToIncrement;
+ this.video.dislikes += dislikesToIncrement;
+ }
+
private loadTooLong() {
this.error = true;
console.error('The video load seems to be abnormally long.');
diff --git a/server/controllers/api/remote/videos.js b/server/controllers/api/remote/videos.js
index 39c9579c1..98891c99e 100644
--- a/server/controllers/api/remote/videos.js
+++ b/server/controllers/api/remote/videos.js
@@ -11,6 +11,7 @@ const secureMiddleware = middlewares.secure
const videosValidators = middlewares.validators.remote.videos
const signatureValidators = middlewares.validators.remote.signature
const logger = require('../../../helpers/logger')
+const friends = require('../../../lib/friends')
const databaseUtils = require('../../../helpers/database-utils')
const ENDPOINT_ACTIONS = constants.REQUEST_ENDPOINT_ACTIONS[constants.REQUEST_ENDPOINTS.VIDEOS]
@@ -129,18 +130,22 @@ function processVideosEvents (eventData, fromPod, finalCallback) {
const options = { transaction: t }
let columnToUpdate
+ let qaduType
switch (eventData.eventType) {
case constants.REQUEST_VIDEO_EVENT_TYPES.VIEWS:
columnToUpdate = 'views'
+ qaduType = constants.REQUEST_VIDEO_QADU_TYPES.VIEWS
break
case constants.REQUEST_VIDEO_EVENT_TYPES.LIKES:
columnToUpdate = 'likes'
+ qaduType = constants.REQUEST_VIDEO_QADU_TYPES.LIKES
break
case constants.REQUEST_VIDEO_EVENT_TYPES.DISLIKES:
columnToUpdate = 'dislikes'
+ qaduType = constants.REQUEST_VIDEO_QADU_TYPES.DISLIKES
break
default:
@@ -151,6 +156,19 @@ function processVideosEvents (eventData, fromPod, finalCallback) {
query[columnToUpdate] = eventData.count
videoInstance.increment(query, options).asCallback(function (err) {
+ return callback(err, t, videoInstance, qaduType)
+ })
+ },
+
+ function sendQaduToFriends (t, videoInstance, qaduType, callback) {
+ const qadusParams = [
+ {
+ videoId: videoInstance.id,
+ type: qaduType
+ }
+ ]
+
+ friends.quickAndDirtyUpdatesVideoToFriends(qadusParams, t, function (err) {
return callback(err, t)
})
},
@@ -159,7 +177,6 @@ function processVideosEvents (eventData, fromPod, finalCallback) {
], function (err, t) {
if (err) {
- console.log(err)
logger.debug('Cannot process a video event.', { error: err })
return databaseUtils.rollbackTransaction(err, t, finalCallback)
}
@@ -278,7 +295,10 @@ function addRemoteVideo (videoToCreateData, fromPod, finalCallback) {
duration: videoToCreateData.duration,
createdAt: videoToCreateData.createdAt,
// FIXME: updatedAt does not seems to be considered by Sequelize
- updatedAt: videoToCreateData.updatedAt
+ updatedAt: videoToCreateData.updatedAt,
+ views: videoToCreateData.views,
+ likes: videoToCreateData.likes,
+ dislikes: videoToCreateData.dislikes
}
const video = db.Video.build(videoData)
@@ -372,6 +392,9 @@ function updateRemoteVideo (videoAttributesToUpdate, fromPod, finalCallback) {
videoInstance.set('createdAt', videoAttributesToUpdate.createdAt)
videoInstance.set('updatedAt', videoAttributesToUpdate.updatedAt)
videoInstance.set('extname', videoAttributesToUpdate.extname)
+ videoInstance.set('views', videoAttributesToUpdate.views)
+ videoInstance.set('likes', videoAttributesToUpdate.likes)
+ videoInstance.set('dislikes', videoAttributesToUpdate.dislikes)
videoInstance.save(options).asCallback(function (err) {
return callback(err, t, videoInstance, tagInstances)
diff --git a/server/controllers/api/users.js b/server/controllers/api/users.js
index 324c99b4c..f854b3082 100644
--- a/server/controllers/api/users.js
+++ b/server/controllers/api/users.js
@@ -18,7 +18,16 @@ const validatorsUsers = middlewares.validators.users
const router = express.Router()
-router.get('/me', oAuth.authenticate, getUserInformation)
+router.get('/me',
+ oAuth.authenticate,
+ getUserInformation
+)
+
+router.get('/me/videos/:videoId/rating',
+ oAuth.authenticate,
+ validatorsUsers.usersVideoRating,
+ getUserVideoRating
+)
router.get('/',
validatorsPagination.pagination,
@@ -80,6 +89,22 @@ function getUserInformation (req, res, next) {
})
}
+function getUserVideoRating (req, res, next) {
+ const videoId = req.params.videoId
+ const userId = res.locals.oauth.token.User.id
+
+ db.UserVideoRate.load(userId, videoId, function (err, ratingObj) {
+ if (err) return next(err)
+
+ const rating = ratingObj ? ratingObj.type : 'none'
+
+ res.json({
+ videoId,
+ rating
+ })
+ })
+}
+
function listUsers (req, res, next) {
db.User.listForApi(req.query.start, req.query.count, req.query.sort, function (err, usersList, usersTotal) {
if (err) return next(err)
diff --git a/server/controllers/api/videos.js b/server/controllers/api/videos.js
index 5a67d1121..9acdb8fd2 100644
--- a/server/controllers/api/videos.js
+++ b/server/controllers/api/videos.js
@@ -60,6 +60,12 @@ router.post('/:id/abuse',
reportVideoAbuseRetryWrapper
)
+router.put('/:id/rate',
+ oAuth.authenticate,
+ validatorsVideos.videoRate,
+ rateVideoRetryWrapper
+)
+
router.get('/',
validatorsPagination.pagination,
validatorsSort.videosSort,
@@ -104,6 +110,147 @@ module.exports = router
// ---------------------------------------------------------------------------
+function rateVideoRetryWrapper (req, res, next) {
+ const options = {
+ arguments: [ req, res ],
+ errorMessage: 'Cannot update the user video rate.'
+ }
+
+ databaseUtils.retryTransactionWrapper(rateVideo, options, function (err) {
+ if (err) return next(err)
+
+ return res.type('json').status(204).end()
+ })
+}
+
+function rateVideo (req, res, finalCallback) {
+ const rateType = req.body.rating
+ const videoInstance = res.locals.video
+ const userInstance = res.locals.oauth.token.User
+
+ waterfall([
+ databaseUtils.startSerializableTransaction,
+
+ function findPreviousRate (t, callback) {
+ db.UserVideoRate.load(userInstance.id, videoInstance.id, t, function (err, previousRate) {
+ return callback(err, t, previousRate)
+ })
+ },
+
+ function insertUserRateIntoDB (t, previousRate, callback) {
+ const options = { transaction: t }
+
+ let likesToIncrement = 0
+ let dislikesToIncrement = 0
+
+ if (rateType === constants.VIDEO_RATE_TYPES.LIKE) likesToIncrement++
+ else if (rateType === constants.VIDEO_RATE_TYPES.DISLIKE) dislikesToIncrement++
+
+ // There was a previous rate, update it
+ if (previousRate) {
+ // We will remove the previous rate, so we will need to remove it from the video attribute
+ if (previousRate.type === constants.VIDEO_RATE_TYPES.LIKE) likesToIncrement--
+ else if (previousRate.type === constants.VIDEO_RATE_TYPES.DISLIKE) dislikesToIncrement--
+
+ previousRate.type = rateType
+
+ previousRate.save(options).asCallback(function (err) {
+ return callback(err, t, likesToIncrement, dislikesToIncrement)
+ })
+ } else { // There was not a previous rate, insert a new one
+ const query = {
+ userId: userInstance.id,
+ videoId: videoInstance.id,
+ type: rateType
+ }
+
+ db.UserVideoRate.create(query, options).asCallback(function (err) {
+ return callback(err, t, likesToIncrement, dislikesToIncrement)
+ })
+ }
+ },
+
+ function updateVideoAttributeDB (t, likesToIncrement, dislikesToIncrement, callback) {
+ const options = { transaction: t }
+ const incrementQuery = {
+ likes: likesToIncrement,
+ dislikes: dislikesToIncrement
+ }
+
+ // Even if we do not own the video we increment the attributes
+ // It is usefull for the user to have a feedback
+ videoInstance.increment(incrementQuery, options).asCallback(function (err) {
+ return callback(err, t, likesToIncrement, dislikesToIncrement)
+ })
+ },
+
+ function sendEventsToFriendsIfNeeded (t, likesToIncrement, dislikesToIncrement, callback) {
+ // No need for an event type, we own the video
+ if (videoInstance.isOwned()) return callback(null, t, likesToIncrement, dislikesToIncrement)
+
+ const eventsParams = []
+
+ if (likesToIncrement !== 0) {
+ eventsParams.push({
+ videoId: videoInstance.id,
+ type: constants.REQUEST_VIDEO_EVENT_TYPES.LIKES,
+ count: likesToIncrement
+ })
+ }
+
+ if (dislikesToIncrement !== 0) {
+ eventsParams.push({
+ videoId: videoInstance.id,
+ type: constants.REQUEST_VIDEO_EVENT_TYPES.DISLIKES,
+ count: dislikesToIncrement
+ })
+ }
+
+ friends.addEventsToRemoteVideo(eventsParams, t, function (err) {
+ return callback(err, t, likesToIncrement, dislikesToIncrement)
+ })
+ },
+
+ function sendQaduToFriendsIfNeeded (t, likesToIncrement, dislikesToIncrement, callback) {
+ // We do not own the video, there is no need to send a quick and dirty update to friends
+ // Our rate was already sent by the addEvent function
+ if (videoInstance.isOwned() === false) return callback(null, t)
+
+ const qadusParams = []
+
+ if (likesToIncrement !== 0) {
+ qadusParams.push({
+ videoId: videoInstance.id,
+ type: constants.REQUEST_VIDEO_QADU_TYPES.LIKES
+ })
+ }
+
+ if (dislikesToIncrement !== 0) {
+ qadusParams.push({
+ videoId: videoInstance.id,
+ type: constants.REQUEST_VIDEO_QADU_TYPES.DISLIKES
+ })
+ }
+
+ friends.quickAndDirtyUpdatesVideoToFriends(qadusParams, t, function (err) {
+ return callback(err, t)
+ })
+ },
+
+ databaseUtils.commitTransaction
+
+ ], function (err, t) {
+ if (err) {
+ // This is just a debug because we will retry the insert
+ logger.debug('Cannot add the user video rate.', { error: err })
+ return databaseUtils.rollbackTransaction(err, t, finalCallback)
+ }
+
+ logger.info('User video rate for video %s of user %s updated.', videoInstance.name, userInstance.username)
+ return finalCallback(null)
+ })
+}
+
// Wrapper to video 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 addVideoRetryWrapper (req, res, next) {
@@ -155,8 +302,7 @@ function addVideo (req, res, videoFile, finalCallback) {
extname: path.extname(videoFile.filename),
description: videoInfos.description,
duration: videoFile.duration,
- authorId: author.id,
- views: videoInfos.views
+ authorId: author.id
}
const video = db.Video.build(videoData)
@@ -332,11 +478,19 @@ function getVideo (req, res, next) {
// FIXME: make a real view system
// For example, only add a view when a user watch a video during 30s etc
- friends.quickAndDirtyUpdateVideoToFriends(videoInstance.id, constants.REQUEST_VIDEO_QADU_TYPES.VIEWS)
+ const qaduParams = {
+ videoId: videoInstance.id,
+ type: constants.REQUEST_VIDEO_QADU_TYPES.VIEWS
+ }
+ friends.quickAndDirtyUpdateVideoToFriends(qaduParams)
})
} else {
// Just send the event to our friends
- friends.addEventToRemoteVideo(videoInstance.id, constants.REQUEST_VIDEO_EVENT_TYPES.VIEWS)
+ const eventParams = {
+ videoId: videoInstance.id,
+ type: constants.REQUEST_VIDEO_EVENT_TYPES.VIEWS
+ }
+ friends.addEventToRemoteVideo(eventParams)
}
// Do not wait the view system
diff --git a/server/helpers/custom-validators/remote/videos.js b/server/helpers/custom-validators/remote/videos.js
index ba2d0bb93..e1636e0e6 100644
--- a/server/helpers/custom-validators/remote/videos.js
+++ b/server/helpers/custom-validators/remote/videos.js
@@ -92,7 +92,9 @@ function isCommonVideoAttributesValid (video) {
videosValidators.isVideoTagsValid(video.tags) &&
videosValidators.isVideoRemoteIdValid(video.remoteId) &&
videosValidators.isVideoExtnameValid(video.extname) &&
- videosValidators.isVideoViewsValid(video.views)
+ videosValidators.isVideoViewsValid(video.views) &&
+ videosValidators.isVideoLikesValid(video.likes) &&
+ videosValidators.isVideoDislikesValid(video.dislikes)
}
function isRequestTypeAddValid (value) {
diff --git a/server/helpers/custom-validators/videos.js b/server/helpers/custom-validators/videos.js
index c5a1f3cb5..648c7540b 100644
--- a/server/helpers/custom-validators/videos.js
+++ b/server/helpers/custom-validators/videos.js
@@ -1,6 +1,7 @@
'use strict'
const validator = require('express-validator').validator
+const values = require('lodash/values')
const constants = require('../../initializers/constants')
const usersValidators = require('./users')
@@ -26,6 +27,7 @@ const videosValidators = {
isVideoFile,
isVideoViewsValid,
isVideoLikesValid,
+ isVideoRatingTypeValid,
isVideoDislikesValid,
isVideoEventCountValid
}
@@ -103,6 +105,10 @@ function isVideoEventCountValid (value) {
return validator.isInt(value + '', VIDEO_EVENTS_CONSTRAINTS_FIELDS.COUNT)
}
+function isVideoRatingTypeValid (value) {
+ return values(constants.VIDEO_RATE_TYPES).indexOf(value) !== -1
+}
+
function isVideoFile (value, files) {
// Should have files
if (!files) return false
diff --git a/server/initializers/constants.js b/server/initializers/constants.js
index 2d5bb84cc..16a2dd320 100644
--- a/server/initializers/constants.js
+++ b/server/initializers/constants.js
@@ -5,7 +5,7 @@ const path = require('path')
// ---------------------------------------------------------------------------
-const LAST_MIGRATION_VERSION = 15
+const LAST_MIGRATION_VERSION = 25
// ---------------------------------------------------------------------------
@@ -95,6 +95,11 @@ const CONSTRAINTS_FIELDS = {
}
}
+const VIDEO_RATE_TYPES = {
+ LIKE: 'like',
+ DISLIKE: 'dislike'
+}
+
// ---------------------------------------------------------------------------
// Score a pod has when we create it as a friend
@@ -249,7 +254,8 @@ module.exports = {
STATIC_MAX_AGE,
STATIC_PATHS,
THUMBNAILS_SIZE,
- USER_ROLES
+ USER_ROLES,
+ VIDEO_RATE_TYPES
}
// ---------------------------------------------------------------------------
diff --git a/server/initializers/migrations/0020-video-likes.js b/server/initializers/migrations/0020-video-likes.js
new file mode 100644
index 000000000..6db62cb90
--- /dev/null
+++ b/server/initializers/migrations/0020-video-likes.js
@@ -0,0 +1,19 @@
+'use strict'
+
+// utils = { transaction, queryInterface, sequelize, Sequelize }
+exports.up = function (utils, finalCallback) {
+ const q = utils.queryInterface
+ const Sequelize = utils.Sequelize
+
+ const data = {
+ type: Sequelize.INTEGER,
+ allowNull: false,
+ defaultValue: 0
+ }
+
+ q.addColumn('Videos', 'likes', data, { transaction: utils.transaction }).asCallback(finalCallback)
+}
+
+exports.down = function (options, callback) {
+ throw new Error('Not implemented.')
+}
diff --git a/server/initializers/migrations/0025-video-dislikes.js b/server/initializers/migrations/0025-video-dislikes.js
new file mode 100644
index 000000000..40d2e7351
--- /dev/null
+++ b/server/initializers/migrations/0025-video-dislikes.js
@@ -0,0 +1,19 @@
+'use strict'
+
+// utils = { transaction, queryInterface, sequelize, Sequelize }
+exports.up = function (utils, finalCallback) {
+ const q = utils.queryInterface
+ const Sequelize = utils.Sequelize
+
+ const data = {
+ type: Sequelize.INTEGER,
+ allowNull: false,
+ defaultValue: 0
+ }
+
+ q.addColumn('Videos', 'dislikes', data, { transaction: utils.transaction }).asCallback(finalCallback)
+}
+
+exports.down = function (options, callback) {
+ throw new Error('Not implemented.')
+}
diff --git a/server/lib/friends.js b/server/lib/friends.js
index 7bd087d8c..23accfa45 100644
--- a/server/lib/friends.js
+++ b/server/lib/friends.js
@@ -3,6 +3,7 @@
const each = require('async/each')
const eachLimit = require('async/eachLimit')
const eachSeries = require('async/eachSeries')
+const series = require('async/series')
const request = require('request')
const waterfall = require('async/waterfall')
@@ -28,7 +29,9 @@ const friends = {
updateVideoToFriends,
reportAbuseVideoToFriend,
quickAndDirtyUpdateVideoToFriends,
+ quickAndDirtyUpdatesVideoToFriends,
addEventToRemoteVideo,
+ addEventsToRemoteVideo,
hasFriends,
makeFriends,
quitFriends,
@@ -84,24 +87,52 @@ function reportAbuseVideoToFriend (reportData, video) {
createRequest(options)
}
-function quickAndDirtyUpdateVideoToFriends (videoId, type, transaction, callback) {
+function quickAndDirtyUpdateVideoToFriends (qaduParams, transaction, callback) {
const options = {
- videoId,
- type,
+ videoId: qaduParams.videoId,
+ type: qaduParams.type,
transaction
}
return createVideoQaduRequest(options, callback)
}
-function addEventToRemoteVideo (videoId, type, transaction, callback) {
+function quickAndDirtyUpdatesVideoToFriends (qadusParams, transaction, finalCallback) {
+ const tasks = []
+
+ qadusParams.forEach(function (qaduParams) {
+ const fun = function (callback) {
+ quickAndDirtyUpdateVideoToFriends(qaduParams, transaction, callback)
+ }
+
+ tasks.push(fun)
+ })
+
+ series(tasks, finalCallback)
+}
+
+function addEventToRemoteVideo (eventParams, transaction, callback) {
const options = {
- videoId,
- type,
+ videoId: eventParams.videoId,
+ type: eventParams.type,
transaction
}
createVideoEventRequest(options, callback)
}
+function addEventsToRemoteVideo (eventsParams, transaction, finalCallback) {
+ const tasks = []
+
+ eventsParams.forEach(function (eventParams) {
+ const fun = function (callback) {
+ addEventToRemoteVideo(eventParams, transaction, callback)
+ }
+
+ tasks.push(fun)
+ })
+
+ series(tasks, finalCallback)
+}
+
function hasFriends (callback) {
db.Pod.countAll(function (err, count) {
if (err) return callback(err)
diff --git a/server/lib/request-video-qadu-scheduler.js b/server/lib/request-video-qadu-scheduler.js
index ac50cfc11..a85d35160 100644
--- a/server/lib/request-video-qadu-scheduler.js
+++ b/server/lib/request-video-qadu-scheduler.js
@@ -44,14 +44,17 @@ module.exports = class RequestVideoQaduScheduler extends BaseRequestScheduler {
}
}
- const videoData = {}
+ // Maybe another attribute was filled for this video
+ let videoData = requestsToMakeGrouped[hashKey].videos[video.id]
+ if (!videoData) videoData = {}
+
switch (request.type) {
case constants.REQUEST_VIDEO_QADU_TYPES.LIKES:
videoData.likes = video.likes
break
case constants.REQUEST_VIDEO_QADU_TYPES.DISLIKES:
- videoData.likes = video.dislikes
+ videoData.dislikes = video.dislikes
break
case constants.REQUEST_VIDEO_QADU_TYPES.VIEWS:
diff --git a/server/middlewares/validators/users.js b/server/middlewares/validators/users.js
index 3089370ff..ce83fc074 100644
--- a/server/middlewares/validators/users.js
+++ b/server/middlewares/validators/users.js
@@ -7,7 +7,8 @@ const logger = require('../../helpers/logger')
const validatorsUsers = {
usersAdd,
usersRemove,
- usersUpdate
+ usersUpdate,
+ usersVideoRating
}
function usersAdd (req, res, next) {
@@ -62,6 +63,25 @@ function usersUpdate (req, res, next) {
checkErrors(req, res, next)
}
+function usersVideoRating (req, res, next) {
+ req.checkParams('videoId', 'Should have a valid video id').notEmpty().isUUID(4)
+
+ logger.debug('Checking usersVideoRating parameters', { parameters: req.params })
+
+ checkErrors(req, res, function () {
+ db.Video.load(req.params.videoId, function (err, video) {
+ if (err) {
+ logger.error('Error in user request validator.', { error: err })
+ return res.sendStatus(500)
+ }
+
+ if (!video) return res.status(404).send('Video not found')
+
+ next()
+ })
+ })
+}
+
// ---------------------------------------------------------------------------
module.exports = validatorsUsers
diff --git a/server/middlewares/validators/videos.js b/server/middlewares/validators/videos.js
index 5c3f3ecf3..7dc79c56f 100644
--- a/server/middlewares/validators/videos.js
+++ b/server/middlewares/validators/videos.js
@@ -13,7 +13,9 @@ const validatorsVideos = {
videosRemove,
videosSearch,
- videoAbuseReport
+ videoAbuseReport,
+
+ videoRate
}
function videosAdd (req, res, next) {
@@ -119,6 +121,17 @@ function videoAbuseReport (req, res, next) {
})
}
+function videoRate (req, res, next) {
+ req.checkParams('id', 'Should have a valid id').notEmpty().isUUID(4)
+ req.checkBody('rating', 'Should have a valid rate type').isVideoRatingTypeValid()
+
+ logger.debug('Checking videoRate parameters', { parameters: req.body })
+
+ checkErrors(req, res, function () {
+ checkVideoExists(req.params.id, res, next)
+ })
+}
+
// ---------------------------------------------------------------------------
module.exports = validatorsVideos
diff --git a/server/models/request-video-event.js b/server/models/request-video-event.js
index ef3ebcb3a..9ebeaec90 100644
--- a/server/models/request-video-event.js
+++ b/server/models/request-video-event.js
@@ -83,6 +83,9 @@ function listWithLimitAndRandom (limitPods, limitRequestsPerPod, callback) {
if (podIds.length === 0) return callback(null, [])
const query = {
+ order: [
+ [ 'id', 'ASC' ]
+ ],
include: [
{
model: self.sequelize.models.Video,
diff --git a/server/models/user-video-rate.js b/server/models/user-video-rate.js
new file mode 100644
index 000000000..84007d70c
--- /dev/null
+++ b/server/models/user-video-rate.js
@@ -0,0 +1,77 @@
+'use strict'
+
+/*
+ User rates per video.
+
+*/
+
+const values = require('lodash/values')
+
+const constants = require('../initializers/constants')
+
+// ---------------------------------------------------------------------------
+
+module.exports = function (sequelize, DataTypes) {
+ const UserVideoRate = sequelize.define('UserVideoRate',
+ {
+ type: {
+ type: DataTypes.ENUM(values(constants.VIDEO_RATE_TYPES)),
+ allowNull: false
+ }
+ },
+ {
+ indexes: [
+ {
+ fields: [ 'videoId', 'userId', 'type' ],
+ unique: true
+ }
+ ],
+ classMethods: {
+ associate,
+
+ load
+ }
+ }
+ )
+
+ return UserVideoRate
+}
+
+// ------------------------------ STATICS ------------------------------
+
+function associate (models) {
+ this.belongsTo(models.Video, {
+ foreignKey: {
+ name: 'videoId',
+ allowNull: false
+ },
+ onDelete: 'CASCADE'
+ })
+
+ this.belongsTo(models.User, {
+ foreignKey: {
+ name: 'userId',
+ allowNull: false
+ },
+ onDelete: 'CASCADE'
+ })
+}
+
+function load (userId, videoId, transaction, callback) {
+ if (!callback) {
+ callback = transaction
+ transaction = null
+ }
+
+ const query = {
+ where: {
+ userId,
+ videoId
+ }
+ }
+
+ const options = {}
+ if (transaction) options.transaction = transaction
+
+ return this.findOne(query, options).asCallback(callback)
+}
diff --git a/server/models/video.js b/server/models/video.js
index fb46aca86..182555c85 100644
--- a/server/models/video.js
+++ b/server/models/video.js
@@ -89,6 +89,24 @@ module.exports = function (sequelize, DataTypes) {
min: 0,
isInt: true
}
+ },
+ likes: {
+ type: DataTypes.INTEGER,
+ allowNull: false,
+ defaultValue: 0,
+ validate: {
+ min: 0,
+ isInt: true
+ }
+ },
+ dislikes: {
+ type: DataTypes.INTEGER,
+ allowNull: false,
+ defaultValue: 0,
+ validate: {
+ min: 0,
+ isInt: true
+ }
}
},
{
@@ -113,6 +131,9 @@ module.exports = function (sequelize, DataTypes) {
},
{
fields: [ 'views' ]
+ },
+ {
+ fields: [ 'likes' ]
}
],
classMethods: {
@@ -349,6 +370,8 @@ function toFormatedJSON () {
author: this.Author.name,
duration: this.duration,
views: this.views,
+ likes: this.likes,
+ dislikes: this.dislikes,
tags: map(this.Tags, 'name'),
thumbnailPath: pathUtils.join(constants.STATIC_PATHS.THUMBNAILS, this.getThumbnailName()),
createdAt: this.createdAt,
@@ -381,7 +404,9 @@ function toAddRemoteJSON (callback) {
createdAt: self.createdAt,
updatedAt: self.updatedAt,
extname: self.extname,
- views: self.views
+ views: self.views,
+ likes: self.likes,
+ dislikes: self.dislikes
}
return callback(null, remoteVideo)
@@ -400,7 +425,9 @@ function toUpdateRemoteJSON (callback) {
createdAt: this.createdAt,
updatedAt: this.updatedAt,
extname: this.extname,
- views: this.views
+ views: this.views,
+ likes: this.likes,
+ dislikes: this.dislikes
}
return json
diff --git a/server/tests/api/check-params/users.js b/server/tests/api/check-params/users.js
index 6edb54660..11e2bada4 100644
--- a/server/tests/api/check-params/users.js
+++ b/server/tests/api/check-params/users.js
@@ -9,11 +9,13 @@ const loginUtils = require('../../utils/login')
const requestsUtils = require('../../utils/requests')
const serversUtils = require('../../utils/servers')
const usersUtils = require('../../utils/users')
+const videosUtils = require('../../utils/videos')
describe('Test users API validators', function () {
const path = '/api/v1/users/'
let userId = null
let rootId = null
+ let videoId = null
let server = null
let userAccessToken = null
@@ -47,6 +49,23 @@ describe('Test users API validators', function () {
usersUtils.createUser(server.url, server.accessToken, username, password, next)
},
+ function (next) {
+ const name = 'my super name for pod'
+ const description = 'my super description for pod'
+ const tags = [ 'tag' ]
+ const file = 'video_short2.webm'
+ videosUtils.uploadVideo(server.url, server.accessToken, name, description, tags, file, next)
+ },
+ function (next) {
+ videosUtils.getVideosList(server.url, function (err, res) {
+ if (err) throw err
+
+ const videos = res.body.data
+ videoId = videos[0].id
+
+ next()
+ })
+ },
function (next) {
const user = {
username: 'user1',
@@ -289,6 +308,63 @@ describe('Test users API validators', function () {
})
})
+ describe('When getting my video rating', function () {
+ it('Should fail with a non authenticated user', function (done) {
+ request(server.url)
+ .get(path + 'me/videos/' + videoId + '/rating')
+ .set('Authorization', 'Bearer faketoken')
+ .set('Accept', 'application/json')
+ .expect(401, done)
+ })
+
+ it('Should fail with an incorrect video uuid', function (done) {
+ request(server.url)
+ .get(path + 'me/videos/blabla/rating')
+ .set('Authorization', 'Bearer ' + userAccessToken)
+ .set('Accept', 'application/json')
+ .expect(400, done)
+ })
+
+ it('Should fail with an unknown video', function (done) {
+ request(server.url)
+ .get(path + 'me/videos/4da6fde3-88f7-4d16-b119-108df5630b06/rating')
+ .set('Authorization', 'Bearer ' + userAccessToken)
+ .set('Accept', 'application/json')
+ .expect(404, done)
+ })
+
+ it('Should success with the correct parameters', function (done) {
+ request(server.url)
+ .get(path + 'me/videos/' + videoId + '/rating')
+ .set('Authorization', 'Bearer ' + userAccessToken)
+ .set('Accept', 'application/json')
+ .expect(200, done)
+ })
+ })
+
+ describe('When removing an user', function () {
+ it('Should fail with an incorrect id', function (done) {
+ request(server.url)
+ .delete(path + 'bla-bla')
+ .set('Authorization', 'Bearer ' + server.accessToken)
+ .expect(400, done)
+ })
+
+ it('Should fail with the root user', function (done) {
+ request(server.url)
+ .delete(path + rootId)
+ .set('Authorization', 'Bearer ' + server.accessToken)
+ .expect(400, done)
+ })
+
+ it('Should return 404 with a non existing id', function (done) {
+ request(server.url)
+ .delete(path + '45')
+ .set('Authorization', 'Bearer ' + server.accessToken)
+ .expect(404, done)
+ })
+ })
+
describe('When removing an user', function () {
it('Should fail with an incorrect id', function (done) {
request(server.url)
diff --git a/server/tests/api/check-params/videos.js b/server/tests/api/check-params/videos.js
index f8549a95b..0f5f40b8e 100644
--- a/server/tests/api/check-params/videos.js
+++ b/server/tests/api/check-params/videos.js
@@ -420,6 +420,48 @@ describe('Test videos API validator', function () {
it('Should succeed with the correct parameters')
})
+ describe('When rating a video', function () {
+ let videoId
+
+ before(function (done) {
+ videosUtils.getVideosList(server.url, function (err, res) {
+ if (err) throw err
+
+ videoId = res.body.data[0].id
+
+ return done()
+ })
+ })
+
+ it('Should fail without a valid uuid', function (done) {
+ const data = {
+ rating: 'like'
+ }
+ requestsUtils.makePutBodyRequest(server.url, path + 'blabla/rate', server.accessToken, data, done)
+ })
+
+ it('Should fail with an unknown id', function (done) {
+ const data = {
+ rating: 'like'
+ }
+ requestsUtils.makePutBodyRequest(server.url, path + '4da6fde3-88f7-4d16-b119-108df5630b06/rate', server.accessToken, data, done, 404)
+ })
+
+ it('Should fail with a wrong rating', function (done) {
+ const data = {
+ rating: 'likes'
+ }
+ requestsUtils.makePutBodyRequest(server.url, path + videoId + '/rate', server.accessToken, data, done)
+ })
+
+ it('Should succeed with the correct parameters', function (done) {
+ const data = {
+ rating: 'like'
+ }
+ requestsUtils.makePutBodyRequest(server.url, path + videoId + '/rate', server.accessToken, data, done, 204)
+ })
+ })
+
describe('When removing a video', function () {
it('Should have 404 with nothing', function (done) {
request(server.url)
diff --git a/server/tests/api/multiple-pods.js b/server/tests/api/multiple-pods.js
index e02b6180b..552f10c6f 100644
--- a/server/tests/api/multiple-pods.js
+++ b/server/tests/api/multiple-pods.js
@@ -4,6 +4,7 @@
const chai = require('chai')
const each = require('async/each')
+const eachSeries = require('async/eachSeries')
const expect = chai.expect
const parallel = require('async/parallel')
const series = require('async/series')
@@ -378,7 +379,7 @@ describe('Test multiple pods', function () {
})
})
- describe('Should update video views', function () {
+ describe('Should update video views, likes and dislikes', function () {
let localVideosPod3 = []
let remoteVideosPod1 = []
let remoteVideosPod2 = []
@@ -419,7 +420,7 @@ describe('Test multiple pods', function () {
], done)
})
- it('Should views multiple videos on owned servers', function (done) {
+ it('Should view multiple videos on owned servers', function (done) {
this.timeout(30000)
parallel([
@@ -440,18 +441,18 @@ describe('Test multiple pods', function () {
},
function (callback) {
- setTimeout(done, 22000)
+ setTimeout(callback, 22000)
}
], function (err) {
if (err) throw err
- each(servers, function (server, callback) {
+ eachSeries(servers, function (server, callback) {
videosUtils.getVideosList(server.url, function (err, res) {
if (err) throw err
const videos = res.body.data
- expect(videos.find(video => video.views === 3)).to.be.exist
- expect(videos.find(video => video.views === 1)).to.be.exist
+ expect(videos.find(video => video.views === 3)).to.exist
+ expect(videos.find(video => video.views === 1)).to.exist
callback()
})
@@ -459,7 +460,7 @@ describe('Test multiple pods', function () {
})
})
- it('Should views multiple videos on each servers', function (done) {
+ it('Should view multiple videos on each servers', function (done) {
this.timeout(30000)
parallel([
@@ -504,17 +505,17 @@ describe('Test multiple pods', function () {
},
function (callback) {
- setTimeout(done, 22000)
+ setTimeout(callback, 22000)
}
], function (err) {
if (err) throw err
let baseVideos = null
- each(servers, function (server, callback) {
+ eachSeries(servers, function (server, callback) {
videosUtils.getVideosList(server.url, function (err, res) {
if (err) throw err
- const videos = res.body
+ const videos = res.body.data
// Initialize base videos for future comparisons
if (baseVideos === null) {
@@ -522,10 +523,74 @@ describe('Test multiple pods', function () {
return callback()
}
- for (let i = 0; i < baseVideos.length; i++) {
- expect(baseVideos[i].views).to.equal(videos[i].views)
+ baseVideos.forEach(baseVideo => {
+ const sameVideo = videos.find(video => video.name === baseVideo.name)
+ expect(baseVideo.views).to.equal(sameVideo.views)
+ })
+
+ callback()
+ })
+ }, done)
+ })
+ })
+
+ it('Should like and dislikes videos on different services', function (done) {
+ this.timeout(30000)
+
+ parallel([
+ function (callback) {
+ videosUtils.rateVideo(servers[0].url, servers[0].accessToken, remoteVideosPod1[0], 'like', callback)
+ },
+
+ function (callback) {
+ videosUtils.rateVideo(servers[0].url, servers[0].accessToken, remoteVideosPod1[0], 'dislike', callback)
+ },
+
+ function (callback) {
+ videosUtils.rateVideo(servers[0].url, servers[0].accessToken, remoteVideosPod1[0], 'like', callback)
+ },
+
+ function (callback) {
+ videosUtils.rateVideo(servers[2].url, servers[2].accessToken, localVideosPod3[1], 'like', callback)
+ },
+
+ function (callback) {
+ videosUtils.rateVideo(servers[2].url, servers[2].accessToken, localVideosPod3[1], 'dislike', callback)
+ },
+
+ function (callback) {
+ videosUtils.rateVideo(servers[2].url, servers[2].accessToken, remoteVideosPod3[1], 'dislike', callback)
+ },
+
+ function (callback) {
+ videosUtils.rateVideo(servers[2].url, servers[2].accessToken, remoteVideosPod3[0], 'like', callback)
+ },
+
+ function (callback) {
+ setTimeout(callback, 22000)
+ }
+ ], function (err) {
+ if (err) throw err
+
+ let baseVideos = null
+ eachSeries(servers, function (server, callback) {
+ videosUtils.getVideosList(server.url, function (err, res) {
+ if (err) throw err
+
+ const videos = res.body.data
+
+ // Initialize base videos for future comparisons
+ if (baseVideos === null) {
+ baseVideos = videos
+ return callback()
}
+ baseVideos.forEach(baseVideo => {
+ const sameVideo = videos.find(video => video.name === baseVideo.name)
+ expect(baseVideo.likes).to.equal(sameVideo.likes)
+ expect(baseVideo.dislikes).to.equal(sameVideo.dislikes)
+ })
+
callback()
})
}, done)
diff --git a/server/tests/api/single-pod.js b/server/tests/api/single-pod.js
index 87d0e9a71..96e4aff9e 100644
--- a/server/tests/api/single-pod.js
+++ b/server/tests/api/single-pod.js
@@ -609,6 +609,40 @@ describe('Test a single pod', function () {
})
})
+ it('Should like a video', function (done) {
+ videosUtils.rateVideo(server.url, server.accessToken, videoId, 'like', function (err) {
+ if (err) throw err
+
+ videosUtils.getVideo(server.url, videoId, function (err, res) {
+ if (err) throw err
+
+ const video = res.body
+
+ expect(video.likes).to.equal(1)
+ expect(video.dislikes).to.equal(0)
+
+ done()
+ })
+ })
+ })
+
+ it('Should dislike the same video', function (done) {
+ videosUtils.rateVideo(server.url, server.accessToken, videoId, 'dislike', function (err) {
+ if (err) throw err
+
+ videosUtils.getVideo(server.url, videoId, function (err, res) {
+ if (err) throw err
+
+ const video = res.body
+
+ expect(video.likes).to.equal(0)
+ expect(video.dislikes).to.equal(1)
+
+ done()
+ })
+ })
+ })
+
after(function (done) {
process.kill(-server.app.pid)
diff --git a/server/tests/api/users.js b/server/tests/api/users.js
index bd95e78c2..f9568b874 100644
--- a/server/tests/api/users.js
+++ b/server/tests/api/users.js
@@ -10,6 +10,7 @@ const loginUtils = require('../utils/login')
const podsUtils = require('../utils/pods')
const serversUtils = require('../utils/servers')
const usersUtils = require('../utils/users')
+const requestsUtils = require('../utils/requests')
const videosUtils = require('../utils/videos')
describe('Test users', function () {
@@ -138,6 +139,23 @@ describe('Test users', function () {
videosUtils.uploadVideo(server.url, accessToken, name, description, tags, video, 204, done)
})
+ it('Should retrieve a video rating', function (done) {
+ videosUtils.rateVideo(server.url, accessToken, videoId, 'like', function (err) {
+ if (err) throw err
+
+ usersUtils.getUserVideoRating(server.url, accessToken, videoId, function (err, res) {
+ if (err) throw err
+
+ const rating = res.body
+
+ expect(rating.videoId).to.equal(videoId)
+ expect(rating.rating).to.equal('like')
+
+ done()
+ })
+ })
+ })
+
it('Should not be able to remove the video with an incorrect token', function (done) {
videosUtils.removeVideo(server.url, 'bad_token', videoId, 401, done)
})
@@ -150,10 +168,21 @@ describe('Test users', function () {
it('Should logout (revoke token)')
+ it('Should not be able to get the user informations')
+
it('Should not be able to upload a video')
it('Should not be able to remove a video')
+ it('Should not be able to rate a video', function (done) {
+ const path = '/api/v1/videos/'
+ const data = {
+ rating: 'likes'
+ }
+
+ requestsUtils.makePutBodyRequest(server.url, path + videoId, 'wrong token', data, done, 401)
+ })
+
it('Should be able to login again')
it('Should have an expired access token')
diff --git a/server/tests/utils/users.js b/server/tests/utils/users.js
index a2c010f64..7817160b9 100644
--- a/server/tests/utils/users.js
+++ b/server/tests/utils/users.js
@@ -5,6 +5,7 @@ const request = require('supertest')
const usersUtils = {
createUser,
getUserInformation,
+ getUserVideoRating,
getUsersList,
getUsersListPaginationAndSort,
removeUser,
@@ -47,6 +48,18 @@ function getUserInformation (url, accessToken, end) {
.end(end)
}
+function getUserVideoRating (url, accessToken, videoId, end) {
+ const path = '/api/v1/users/me/videos/' + videoId + '/rating'
+
+ request(url)
+ .get(path)
+ .set('Accept', 'application/json')
+ .set('Authorization', 'Bearer ' + accessToken)
+ .expect(200)
+ .expect('Content-Type', /json/)
+ .end(end)
+}
+
function getUsersList (url, end) {
const path = '/api/v1/users'
diff --git a/server/tests/utils/videos.js b/server/tests/utils/videos.js
index f94368437..177426076 100644
--- a/server/tests/utils/videos.js
+++ b/server/tests/utils/videos.js
@@ -16,7 +16,8 @@ const videosUtils = {
searchVideoWithSort,
testVideoImage,
uploadVideo,
- updateVideo
+ updateVideo,
+ rateVideo
}
// ---------------------- Export functions --------------------
@@ -236,6 +237,23 @@ function updateVideo (url, accessToken, id, name, description, tags, specialStat
req.expect(specialStatus).end(end)
}
+function rateVideo (url, accessToken, id, rating, specialStatus, end) {
+ if (!end) {
+ end = specialStatus
+ specialStatus = 204
+ }
+
+ const path = '/api/v1/videos/' + id + '/rate'
+
+ request(url)
+ .put(path)
+ .set('Accept', 'application/json')
+ .set('Authorization', 'Bearer ' + accessToken)
+ .send({ rating })
+ .expect(specialStatus)
+ .end(end)
+}
+
// ---------------------------------------------------------------------------
module.exports = videosUtils