@Injectable()
export class UserService {
- private static BASE_USERS_URL = '/api/v1/users/';
+ static BASE_USERS_URL = '/api/v1/users/';
constructor(
private authHttp: AuthHttp,
export * from './loader';
export * from './sort-field.type';
+export * from './rate-type.type';
export * from './video.model';
export * from './video.service';
--- /dev/null
+export type RateType = 'like' | 'dislike';
-export type SortField = "name" | "-name"
- | "duration" | "-duration"
- | "createdAt" | "-createdAt";
+export type SortField = 'name' | '-name'
+ | 'duration' | '-duration'
+ | 'createdAt' | '-createdAt';
tags: string[];
thumbnailPath: string;
views: number;
+ likes: number;
+ dislikes: number;
private static createByString(author: string, podHost: string) {
return author + '@' + podHost;
podHost: string,
tags: string[],
thumbnailPath: string,
- views: number
+ views: number,
+ likes: number,
+ dislikes: number,
}) {
this.author = hash.author;
this.createdAt = new Date(hash.createdAt);
this.tags = hash.tags;
this.thumbnailPath = hash.thumbnailPath;
this.views = hash.views;
+ this.likes = hash.likes;
+ this.dislikes = hash.dislikes;
this.by = Video.createByString(hash.author, hash.podHost);
}
import { Search } from '../../shared';
import { SortField } from './sort-field.type';
+import { RateType } from './rate-type.type';
import { AuthService } from '../../core';
-import { AuthHttp, RestExtractor, RestPagination, RestService, ResultList } from '../../shared';
+import {
+ AuthHttp,
+ RestExtractor,
+ RestPagination,
+ RestService,
+ ResultList,
+ UserService
+} from '../../shared';
import { Video } from './video.model';
@Injectable()
}
reportVideo(id: string, reason: string) {
+ const url = VideoService.BASE_VIDEO_URL + id + '/abuse';
const body = {
reason
};
- const url = VideoService.BASE_VIDEO_URL + id + '/abuse';
return this.authHttp.post(url, body)
- .map(this.restExtractor.extractDataBool)
- .catch((res) => this.restExtractor.handleError(res));
+ .map(this.restExtractor.extractDataBool)
+ .catch((res) => this.restExtractor.handleError(res));
+ }
+
+ setVideoLike(id: string) {
+ return this.setVideoRate(id, 'like');
+ }
+
+ setVideoDislike(id: string) {
+ return this.setVideoRate(id, 'dislike');
+ }
+
+ getUserVideoRating(id: string) {
+ const url = UserService.BASE_USERS_URL + '/me/videos/' + id + '/rating';
+
+ return this.authHttp.get(url)
+ .map(this.restExtractor.extractDataGet)
+ .catch((res) => this.restExtractor.handleError(res));
+ }
+
+ private setVideoRate(id: string, rateType: RateType) {
+ const url = VideoService.BASE_VIDEO_URL + id + '/rate';
+ const body = {
+ rating: rateType
+ };
+
+ return this.authHttp.put(url, body)
+ .map(this.restExtractor.extractDataBool)
+ .catch((res) => this.restExtractor.handleError(res));
}
private extractVideos(result: ResultList) {
<div *ngIf="video !== null" id="video-info">
<div class="row" id="video-name-actions">
- <div class="col-md-8">
+ <div class="col-md-6">
<div class="row">
<div id="video-name" class="col-md-12">
{{ video.name }}
</div>
</div>
- <div id="video-actions" class="col-md-4 text-right">
+ <div id="video-actions" class="col-md-6 text-right">
+ <div id="rates">
+ <button
+ id="likes" class="btn btn-default"
+ [ngClass]="{ 'not-interactive-btn': !isUserLoggedIn(), 'activated-btn': userRating === 'like' }" (click)="setLike()"
+ >
+ <span class="glyphicon glyphicon-thumbs-up"></span> {{ video.likes }}
+ </button>
+
+ <button
+ id="dislikes" class="btn btn-default"
+ [ngClass]="{ 'not-interactive-btn': !isUserLoggedIn(), 'activated-btn': userRating === 'dislike' }" (click)="setDislike()"
+ >
+ <span class=" glyphicon glyphicon-thumbs-down"></span> {{ video.dislikes }}
+ </button>
+ </div>
+
<button id="share" class="btn btn-default" (click)="showShareModal()">
<span class="glyphicon glyphicon-share"></span> Share
</button>
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;
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({
player: VideoJSPlayer;
playerElement: Element;
uploadSpeed: number;
+ userRating: RateType = null;
video: Video = null;
videoNotFound = false;
this.video = video;
this.setOpenGraphTags();
this.loadVideo();
+ this.checkUserRating();
},
error => {
this.videoNotFound = true;
});
}
+ 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();
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.');
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]
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:
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)
})
},
], function (err, t) {
if (err) {
- console.log(err)
logger.debug('Cannot process a video event.', { error: err })
return databaseUtils.rollbackTransaction(err, t, 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)
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)
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,
})
}
+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)
reportVideoAbuseRetryWrapper
)
+router.put('/:id/rate',
+ oAuth.authenticate,
+ validatorsVideos.videoRate,
+ rateVideoRetryWrapper
+)
+
router.get('/',
validatorsPagination.pagination,
validatorsSort.videosSort,
// ---------------------------------------------------------------------------
+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) {
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)
// 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
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) {
'use strict'
const validator = require('express-validator').validator
+const values = require('lodash/values')
const constants = require('../../initializers/constants')
const usersValidators = require('./users')
isVideoFile,
isVideoViewsValid,
isVideoLikesValid,
+ isVideoRatingTypeValid,
isVideoDislikesValid,
isVideoEventCountValid
}
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
// ---------------------------------------------------------------------------
-const LAST_MIGRATION_VERSION = 15
+const LAST_MIGRATION_VERSION = 25
// ---------------------------------------------------------------------------
}
}
+const VIDEO_RATE_TYPES = {
+ LIKE: 'like',
+ DISLIKE: 'dislike'
+}
+
// ---------------------------------------------------------------------------
// Score a pod has when we create it as a friend
STATIC_MAX_AGE,
STATIC_PATHS,
THUMBNAILS_SIZE,
- USER_ROLES
+ USER_ROLES,
+ VIDEO_RATE_TYPES
}
// ---------------------------------------------------------------------------
--- /dev/null
+'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.')
+}
--- /dev/null
+'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.')
+}
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')
updateVideoToFriends,
reportAbuseVideoToFriend,
quickAndDirtyUpdateVideoToFriends,
+ quickAndDirtyUpdatesVideoToFriends,
addEventToRemoteVideo,
+ addEventsToRemoteVideo,
hasFriends,
makeFriends,
quitFriends,
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)
}
}
- 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:
const validatorsUsers = {
usersAdd,
usersRemove,
- usersUpdate
+ usersUpdate,
+ usersVideoRating
}
function usersAdd (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
videosRemove,
videosSearch,
- videoAbuseReport
+ videoAbuseReport,
+
+ videoRate
}
function videosAdd (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
if (podIds.length === 0) return callback(null, [])
const query = {
+ order: [
+ [ 'id', 'ASC' ]
+ ],
include: [
{
model: self.sequelize.models.Video,
--- /dev/null
+'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)
+}
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
+ }
}
},
{
},
{
fields: [ 'views' ]
+ },
+ {
+ fields: [ 'likes' ]
}
],
classMethods: {
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,
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)
createdAt: this.createdAt,
updatedAt: this.updatedAt,
extname: this.extname,
- views: this.views
+ views: this.views,
+ likes: this.likes,
+ dislikes: this.dislikes
}
return json
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
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',
})
})
+ 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)
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)
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')
})
})
- describe('Should update video views', function () {
+ describe('Should update video views, likes and dislikes', function () {
let localVideosPod3 = []
let remoteVideosPod1 = []
let remoteVideosPod2 = []
], 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([
},
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()
})
})
})
- it('Should views multiple videos on each servers', function (done) {
+ it('Should view multiple videos on each servers', function (done) {
this.timeout(30000)
parallel([
},
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) {
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)
})
})
+ 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)
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 () {
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)
})
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')
const usersUtils = {
createUser,
getUserInformation,
+ getUserVideoRating,
getUsersList,
getUsersListPaginationAndSort,
removeUser,
.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'
searchVideoWithSort,
testVideoImage,
uploadVideo,
- updateVideo
+ updateVideo,
+ rateVideo
}
// ---------------------- Export functions --------------------
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