aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorChocobozzz <florian.bigard@gmail.com>2017-03-08 21:35:43 +0100
committerChocobozzz <florian.bigard@gmail.com>2017-03-08 21:35:43 +0100
commitd38b82810638b9f664c9016fac2684454c273a77 (patch)
tree9465c367e5033675309efca4d66790c6fdd5230d
parent8f9064432122cba0f518a24ac4378357dadec589 (diff)
downloadPeerTube-d38b82810638b9f664c9016fac2684454c273a77.tar.gz
PeerTube-d38b82810638b9f664c9016fac2684454c273a77.tar.zst
PeerTube-d38b82810638b9f664c9016fac2684454c273a77.zip
Add like/dislike system for videos
-rw-r--r--client/src/app/shared/users/user.service.ts2
-rw-r--r--client/src/app/videos/shared/index.ts1
-rw-r--r--client/src/app/videos/shared/rate-type.type.ts1
-rw-r--r--client/src/app/videos/shared/sort-field.type.ts6
-rw-r--r--client/src/app/videos/shared/video.model.ts8
-rw-r--r--client/src/app/videos/shared/video.service.ts43
-rw-r--r--client/src/app/videos/video-watch/video-watch.component.html20
-rw-r--r--client/src/app/videos/video-watch/video-watch.component.scss28
-rw-r--r--client/src/app/videos/video-watch/video-watch.component.ts70
-rw-r--r--server/controllers/api/remote/videos.js27
-rw-r--r--server/controllers/api/users.js27
-rw-r--r--server/controllers/api/videos.js162
-rw-r--r--server/helpers/custom-validators/remote/videos.js4
-rw-r--r--server/helpers/custom-validators/videos.js6
-rw-r--r--server/initializers/constants.js10
-rw-r--r--server/initializers/migrations/0020-video-likes.js19
-rw-r--r--server/initializers/migrations/0025-video-dislikes.js19
-rw-r--r--server/lib/friends.js43
-rw-r--r--server/lib/request-video-qadu-scheduler.js7
-rw-r--r--server/middlewares/validators/users.js22
-rw-r--r--server/middlewares/validators/videos.js15
-rw-r--r--server/models/request-video-event.js3
-rw-r--r--server/models/user-video-rate.js77
-rw-r--r--server/models/video.js31
-rw-r--r--server/tests/api/check-params/users.js76
-rw-r--r--server/tests/api/check-params/videos.js42
-rw-r--r--server/tests/api/multiple-pods.js89
-rw-r--r--server/tests/api/single-pod.js34
-rw-r--r--server/tests/api/users.js29
-rw-r--r--server/tests/utils/users.js13
-rw-r--r--server/tests/utils/videos.js20
31 files changed, 907 insertions, 47 deletions
diff --git a/client/src/app/shared/users/user.service.ts b/client/src/app/shared/users/user.service.ts
index 4cf100f0d..865e04d48 100644
--- a/client/src/app/shared/users/user.service.ts
+++ b/client/src/app/shared/users/user.service.ts
@@ -8,7 +8,7 @@ import { RestExtractor } from '../rest';
8 8
9@Injectable() 9@Injectable()
10export class UserService { 10export class UserService {
11 private static BASE_USERS_URL = '/api/v1/users/'; 11 static BASE_USERS_URL = '/api/v1/users/';
12 12
13 constructor( 13 constructor(
14 private authHttp: AuthHttp, 14 private authHttp: AuthHttp,
diff --git a/client/src/app/videos/shared/index.ts b/client/src/app/videos/shared/index.ts
index 67d16ead1..beaa528c0 100644
--- a/client/src/app/videos/shared/index.ts
+++ b/client/src/app/videos/shared/index.ts
@@ -1,4 +1,5 @@
1export * from './loader'; 1export * from './loader';
2export * from './sort-field.type'; 2export * from './sort-field.type';
3export * from './rate-type.type';
3export * from './video.model'; 4export * from './video.model';
4export * from './video.service'; 5export * from './video.service';
diff --git a/client/src/app/videos/shared/rate-type.type.ts b/client/src/app/videos/shared/rate-type.type.ts
new file mode 100644
index 000000000..88034d1ff
--- /dev/null
+++ b/client/src/app/videos/shared/rate-type.type.ts
@@ -0,0 +1 @@
export type RateType = 'like' | 'dislike';
diff --git a/client/src/app/videos/shared/sort-field.type.ts b/client/src/app/videos/shared/sort-field.type.ts
index 74908e344..7bda3112a 100644
--- a/client/src/app/videos/shared/sort-field.type.ts
+++ b/client/src/app/videos/shared/sort-field.type.ts
@@ -1,3 +1,3 @@
1export type SortField = "name" | "-name" 1export type SortField = 'name' | '-name'
2 | "duration" | "-duration" 2 | 'duration' | '-duration'
3 | "createdAt" | "-createdAt"; 3 | 'createdAt' | '-createdAt';
diff --git a/client/src/app/videos/shared/video.model.ts b/client/src/app/videos/shared/video.model.ts
index 8e676708b..3eef936eb 100644
--- a/client/src/app/videos/shared/video.model.ts
+++ b/client/src/app/videos/shared/video.model.ts
@@ -12,6 +12,8 @@ export class Video {
12 tags: string[]; 12 tags: string[];
13 thumbnailPath: string; 13 thumbnailPath: string;
14 views: number; 14 views: number;
15 likes: number;
16 dislikes: number;
15 17
16 private static createByString(author: string, podHost: string) { 18 private static createByString(author: string, podHost: string) {
17 return author + '@' + podHost; 19 return author + '@' + podHost;
@@ -38,7 +40,9 @@ export class Video {
38 podHost: string, 40 podHost: string,
39 tags: string[], 41 tags: string[],
40 thumbnailPath: string, 42 thumbnailPath: string,
41 views: number 43 views: number,
44 likes: number,
45 dislikes: number,
42 }) { 46 }) {
43 this.author = hash.author; 47 this.author = hash.author;
44 this.createdAt = new Date(hash.createdAt); 48 this.createdAt = new Date(hash.createdAt);
@@ -52,6 +56,8 @@ export class Video {
52 this.tags = hash.tags; 56 this.tags = hash.tags;
53 this.thumbnailPath = hash.thumbnailPath; 57 this.thumbnailPath = hash.thumbnailPath;
54 this.views = hash.views; 58 this.views = hash.views;
59 this.likes = hash.likes;
60 this.dislikes = hash.dislikes;
55 61
56 this.by = Video.createByString(hash.author, hash.podHost); 62 this.by = Video.createByString(hash.author, hash.podHost);
57 } 63 }
diff --git a/client/src/app/videos/shared/video.service.ts b/client/src/app/videos/shared/video.service.ts
index 7094d9a34..8bb5a2933 100644
--- a/client/src/app/videos/shared/video.service.ts
+++ b/client/src/app/videos/shared/video.service.ts
@@ -6,8 +6,16 @@ import 'rxjs/add/operator/map';
6 6
7import { Search } from '../../shared'; 7import { Search } from '../../shared';
8import { SortField } from './sort-field.type'; 8import { SortField } from './sort-field.type';
9import { RateType } from './rate-type.type';
9import { AuthService } from '../../core'; 10import { AuthService } from '../../core';
10import { AuthHttp, RestExtractor, RestPagination, RestService, ResultList } from '../../shared'; 11import {
12 AuthHttp,
13 RestExtractor,
14 RestPagination,
15 RestService,
16 ResultList,
17 UserService
18} from '../../shared';
11import { Video } from './video.model'; 19import { Video } from './video.model';
12 20
13@Injectable() 21@Injectable()
@@ -56,14 +64,41 @@ export class VideoService {
56 } 64 }
57 65
58 reportVideo(id: string, reason: string) { 66 reportVideo(id: string, reason: string) {
67 const url = VideoService.BASE_VIDEO_URL + id + '/abuse';
59 const body = { 68 const body = {
60 reason 69 reason
61 }; 70 };
62 const url = VideoService.BASE_VIDEO_URL + id + '/abuse';
63 71
64 return this.authHttp.post(url, body) 72 return this.authHttp.post(url, body)
65 .map(this.restExtractor.extractDataBool) 73 .map(this.restExtractor.extractDataBool)
66 .catch((res) => this.restExtractor.handleError(res)); 74 .catch((res) => this.restExtractor.handleError(res));
75 }
76
77 setVideoLike(id: string) {
78 return this.setVideoRate(id, 'like');
79 }
80
81 setVideoDislike(id: string) {
82 return this.setVideoRate(id, 'dislike');
83 }
84
85 getUserVideoRating(id: string) {
86 const url = UserService.BASE_USERS_URL + '/me/videos/' + id + '/rating';
87
88 return this.authHttp.get(url)
89 .map(this.restExtractor.extractDataGet)
90 .catch((res) => this.restExtractor.handleError(res));
91 }
92
93 private setVideoRate(id: string, rateType: RateType) {
94 const url = VideoService.BASE_VIDEO_URL + id + '/rate';
95 const body = {
96 rating: rateType
97 };
98
99 return this.authHttp.put(url, body)
100 .map(this.restExtractor.extractDataBool)
101 .catch((res) => this.restExtractor.handleError(res));
67 } 102 }
68 103
69 private extractVideos(result: ResultList) { 104 private extractVideos(result: ResultList) {
diff --git a/client/src/app/videos/video-watch/video-watch.component.html b/client/src/app/videos/video-watch/video-watch.component.html
index 24d741ff9..67094359e 100644
--- a/client/src/app/videos/video-watch/video-watch.component.html
+++ b/client/src/app/videos/video-watch/video-watch.component.html
@@ -32,7 +32,7 @@
32 32
33<div *ngIf="video !== null" id="video-info"> 33<div *ngIf="video !== null" id="video-info">
34 <div class="row" id="video-name-actions"> 34 <div class="row" id="video-name-actions">
35 <div class="col-md-8"> 35 <div class="col-md-6">
36 <div class="row"> 36 <div class="row">
37 <div id="video-name" class="col-md-12"> 37 <div id="video-name" class="col-md-12">
38 {{ video.name }} 38 {{ video.name }}
@@ -52,7 +52,23 @@
52 </div> 52 </div>
53 </div> 53 </div>
54 54
55 <div id="video-actions" class="col-md-4 text-right"> 55 <div id="video-actions" class="col-md-6 text-right">
56 <div id="rates">
57 <button
58 id="likes" class="btn btn-default"
59 [ngClass]="{ 'not-interactive-btn': !isUserLoggedIn(), 'activated-btn': userRating === 'like' }" (click)="setLike()"
60 >
61 <span class="glyphicon glyphicon-thumbs-up"></span> {{ video.likes }}
62 </button>
63
64 <button
65 id="dislikes" class="btn btn-default"
66 [ngClass]="{ 'not-interactive-btn': !isUserLoggedIn(), 'activated-btn': userRating === 'dislike' }" (click)="setDislike()"
67 >
68 <span class=" glyphicon glyphicon-thumbs-down"></span> {{ video.dislikes }}
69 </button>
70 </div>
71
56 <button id="share" class="btn btn-default" (click)="showShareModal()"> 72 <button id="share" class="btn btn-default" (click)="showShareModal()">
57 <span class="glyphicon glyphicon-share"></span> Share 73 <span class="glyphicon glyphicon-share"></span> Share
58 </button> 74 </button>
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 @@
47 top: 2px; 47 top: 2px;
48 } 48 }
49 49
50 #rates {
51 display: inline-block;
52 margin-right: 20px;
53
54 // Remove focus style
55 .btn:focus {
56 outline: 0;
57 }
58
59 .activated-btn {
60 color: #333;
61 background-color: #e6e6e6;
62 border-color: #8c8c8c;
63 }
64
65 .not-interactive-btn {
66 cursor: default;
67
68 &:hover, &:focus, &:active {
69 color: #333;
70 background-color: #fff;
71 border-color: #ccc;
72 box-shadow: none;
73 outline: 0;
74 }
75 }
76 }
77
50 #share, #more { 78 #share, #more {
51 font-weight: bold; 79 font-weight: bold;
52 opacity: 0.85; 80 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';
10import { VideoMagnetComponent } from './video-magnet.component'; 10import { VideoMagnetComponent } from './video-magnet.component';
11import { VideoShareComponent } from './video-share.component'; 11import { VideoShareComponent } from './video-share.component';
12import { VideoReportComponent } from './video-report.component'; 12import { VideoReportComponent } from './video-report.component';
13import { Video, VideoService } from '../shared'; 13import { RateType, Video, VideoService } from '../shared';
14import { WebTorrentService } from './webtorrent.service'; 14import { WebTorrentService } from './webtorrent.service';
15 15
16@Component({ 16@Component({
@@ -33,6 +33,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
33 player: VideoJSPlayer; 33 player: VideoJSPlayer;
34 playerElement: Element; 34 playerElement: Element;
35 uploadSpeed: number; 35 uploadSpeed: number;
36 userRating: RateType = null;
36 video: Video = null; 37 video: Video = null;
37 videoNotFound = false; 38 videoNotFound = false;
38 39
@@ -61,6 +62,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
61 this.video = video; 62 this.video = video;
62 this.setOpenGraphTags(); 63 this.setOpenGraphTags();
63 this.loadVideo(); 64 this.loadVideo();
65 this.checkUserRating();
64 }, 66 },
65 error => { 67 error => {
66 this.videoNotFound = true; 68 this.videoNotFound = true;
@@ -136,6 +138,40 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
136 }); 138 });
137 } 139 }
138 140
141 setLike() {
142 if (this.isUserLoggedIn() === false) return;
143 // Already liked this video
144 if (this.userRating === 'like') return;
145
146 this.videoService.setVideoLike(this.video.id)
147 .subscribe(
148 () => {
149 // Update the video like attribute
150 this.updateVideoRating(this.userRating, 'like');
151 this.userRating = 'like';
152 },
153
154 err => this.notificationsService.error('Error', err.text)
155 );
156 }
157
158 setDislike() {
159 if (this.isUserLoggedIn() === false) return;
160 // Already disliked this video
161 if (this.userRating === 'dislike') return;
162
163 this.videoService.setVideoDislike(this.video.id)
164 .subscribe(
165 () => {
166 // Update the video dislike attribute
167 this.updateVideoRating(this.userRating, 'dislike');
168 this.userRating = 'dislike';
169 },
170
171 err => this.notificationsService.error('Error', err.text)
172 );
173 }
174
139 showReportModal(event: Event) { 175 showReportModal(event: Event) {
140 event.preventDefault(); 176 event.preventDefault();
141 this.videoReportModal.show(); 177 this.videoReportModal.show();
@@ -154,6 +190,38 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
154 return this.authService.isLoggedIn(); 190 return this.authService.isLoggedIn();
155 } 191 }
156 192
193 private checkUserRating() {
194 // Unlogged users do not have ratings
195 if (this.isUserLoggedIn() === false) return;
196
197 this.videoService.getUserVideoRating(this.video.id)
198 .subscribe(
199 ratingObject => {
200 if (ratingObject) {
201 this.userRating = ratingObject.rating;
202 }
203 },
204
205 err => this.notificationsService.error('Error', err.text)
206 );
207 }
208
209 private updateVideoRating(oldRating: RateType, newRating: RateType) {
210 let likesToIncrement = 0;
211 let dislikesToIncrement = 0;
212
213 if (oldRating) {
214 if (oldRating === 'like') likesToIncrement--;
215 if (oldRating === 'dislike') dislikesToIncrement--;
216 }
217
218 if (newRating === 'like') likesToIncrement++;
219 if (newRating === 'dislike') dislikesToIncrement++;
220
221 this.video.likes += likesToIncrement;
222 this.video.dislikes += dislikesToIncrement;
223 }
224
157 private loadTooLong() { 225 private loadTooLong() {
158 this.error = true; 226 this.error = true;
159 console.error('The video load seems to be abnormally long.'); 227 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
11const videosValidators = middlewares.validators.remote.videos 11const videosValidators = middlewares.validators.remote.videos
12const signatureValidators = middlewares.validators.remote.signature 12const signatureValidators = middlewares.validators.remote.signature
13const logger = require('../../../helpers/logger') 13const logger = require('../../../helpers/logger')
14const friends = require('../../../lib/friends')
14const databaseUtils = require('../../../helpers/database-utils') 15const databaseUtils = require('../../../helpers/database-utils')
15 16
16const ENDPOINT_ACTIONS = constants.REQUEST_ENDPOINT_ACTIONS[constants.REQUEST_ENDPOINTS.VIDEOS] 17const ENDPOINT_ACTIONS = constants.REQUEST_ENDPOINT_ACTIONS[constants.REQUEST_ENDPOINTS.VIDEOS]
@@ -129,18 +130,22 @@ function processVideosEvents (eventData, fromPod, finalCallback) {
129 const options = { transaction: t } 130 const options = { transaction: t }
130 131
131 let columnToUpdate 132 let columnToUpdate
133 let qaduType
132 134
133 switch (eventData.eventType) { 135 switch (eventData.eventType) {
134 case constants.REQUEST_VIDEO_EVENT_TYPES.VIEWS: 136 case constants.REQUEST_VIDEO_EVENT_TYPES.VIEWS:
135 columnToUpdate = 'views' 137 columnToUpdate = 'views'
138 qaduType = constants.REQUEST_VIDEO_QADU_TYPES.VIEWS
136 break 139 break
137 140
138 case constants.REQUEST_VIDEO_EVENT_TYPES.LIKES: 141 case constants.REQUEST_VIDEO_EVENT_TYPES.LIKES:
139 columnToUpdate = 'likes' 142 columnToUpdate = 'likes'
143 qaduType = constants.REQUEST_VIDEO_QADU_TYPES.LIKES
140 break 144 break
141 145
142 case constants.REQUEST_VIDEO_EVENT_TYPES.DISLIKES: 146 case constants.REQUEST_VIDEO_EVENT_TYPES.DISLIKES:
143 columnToUpdate = 'dislikes' 147 columnToUpdate = 'dislikes'
148 qaduType = constants.REQUEST_VIDEO_QADU_TYPES.DISLIKES
144 break 149 break
145 150
146 default: 151 default:
@@ -151,6 +156,19 @@ function processVideosEvents (eventData, fromPod, finalCallback) {
151 query[columnToUpdate] = eventData.count 156 query[columnToUpdate] = eventData.count
152 157
153 videoInstance.increment(query, options).asCallback(function (err) { 158 videoInstance.increment(query, options).asCallback(function (err) {
159 return callback(err, t, videoInstance, qaduType)
160 })
161 },
162
163 function sendQaduToFriends (t, videoInstance, qaduType, callback) {
164 const qadusParams = [
165 {
166 videoId: videoInstance.id,
167 type: qaduType
168 }
169 ]
170
171 friends.quickAndDirtyUpdatesVideoToFriends(qadusParams, t, function (err) {
154 return callback(err, t) 172 return callback(err, t)
155 }) 173 })
156 }, 174 },
@@ -159,7 +177,6 @@ function processVideosEvents (eventData, fromPod, finalCallback) {
159 177
160 ], function (err, t) { 178 ], function (err, t) {
161 if (err) { 179 if (err) {
162 console.log(err)
163 logger.debug('Cannot process a video event.', { error: err }) 180 logger.debug('Cannot process a video event.', { error: err })
164 return databaseUtils.rollbackTransaction(err, t, finalCallback) 181 return databaseUtils.rollbackTransaction(err, t, finalCallback)
165 } 182 }
@@ -278,7 +295,10 @@ function addRemoteVideo (videoToCreateData, fromPod, finalCallback) {
278 duration: videoToCreateData.duration, 295 duration: videoToCreateData.duration,
279 createdAt: videoToCreateData.createdAt, 296 createdAt: videoToCreateData.createdAt,
280 // FIXME: updatedAt does not seems to be considered by Sequelize 297 // FIXME: updatedAt does not seems to be considered by Sequelize
281 updatedAt: videoToCreateData.updatedAt 298 updatedAt: videoToCreateData.updatedAt,
299 views: videoToCreateData.views,
300 likes: videoToCreateData.likes,
301 dislikes: videoToCreateData.dislikes
282 } 302 }
283 303
284 const video = db.Video.build(videoData) 304 const video = db.Video.build(videoData)
@@ -372,6 +392,9 @@ function updateRemoteVideo (videoAttributesToUpdate, fromPod, finalCallback) {
372 videoInstance.set('createdAt', videoAttributesToUpdate.createdAt) 392 videoInstance.set('createdAt', videoAttributesToUpdate.createdAt)
373 videoInstance.set('updatedAt', videoAttributesToUpdate.updatedAt) 393 videoInstance.set('updatedAt', videoAttributesToUpdate.updatedAt)
374 videoInstance.set('extname', videoAttributesToUpdate.extname) 394 videoInstance.set('extname', videoAttributesToUpdate.extname)
395 videoInstance.set('views', videoAttributesToUpdate.views)
396 videoInstance.set('likes', videoAttributesToUpdate.likes)
397 videoInstance.set('dislikes', videoAttributesToUpdate.dislikes)
375 398
376 videoInstance.save(options).asCallback(function (err) { 399 videoInstance.save(options).asCallback(function (err) {
377 return callback(err, t, videoInstance, tagInstances) 400 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
18 18
19const router = express.Router() 19const router = express.Router()
20 20
21router.get('/me', oAuth.authenticate, getUserInformation) 21router.get('/me',
22 oAuth.authenticate,
23 getUserInformation
24)
25
26router.get('/me/videos/:videoId/rating',
27 oAuth.authenticate,
28 validatorsUsers.usersVideoRating,
29 getUserVideoRating
30)
22 31
23router.get('/', 32router.get('/',
24 validatorsPagination.pagination, 33 validatorsPagination.pagination,
@@ -80,6 +89,22 @@ function getUserInformation (req, res, next) {
80 }) 89 })
81} 90}
82 91
92function getUserVideoRating (req, res, next) {
93 const videoId = req.params.videoId
94 const userId = res.locals.oauth.token.User.id
95
96 db.UserVideoRate.load(userId, videoId, function (err, ratingObj) {
97 if (err) return next(err)
98
99 const rating = ratingObj ? ratingObj.type : 'none'
100
101 res.json({
102 videoId,
103 rating
104 })
105 })
106}
107
83function listUsers (req, res, next) { 108function listUsers (req, res, next) {
84 db.User.listForApi(req.query.start, req.query.count, req.query.sort, function (err, usersList, usersTotal) { 109 db.User.listForApi(req.query.start, req.query.count, req.query.sort, function (err, usersList, usersTotal) {
85 if (err) return next(err) 110 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',
60 reportVideoAbuseRetryWrapper 60 reportVideoAbuseRetryWrapper
61) 61)
62 62
63router.put('/:id/rate',
64 oAuth.authenticate,
65 validatorsVideos.videoRate,
66 rateVideoRetryWrapper
67)
68
63router.get('/', 69router.get('/',
64 validatorsPagination.pagination, 70 validatorsPagination.pagination,
65 validatorsSort.videosSort, 71 validatorsSort.videosSort,
@@ -104,6 +110,147 @@ module.exports = router
104 110
105// --------------------------------------------------------------------------- 111// ---------------------------------------------------------------------------
106 112
113function rateVideoRetryWrapper (req, res, next) {
114 const options = {
115 arguments: [ req, res ],
116 errorMessage: 'Cannot update the user video rate.'
117 }
118
119 databaseUtils.retryTransactionWrapper(rateVideo, options, function (err) {
120 if (err) return next(err)
121
122 return res.type('json').status(204).end()
123 })
124}
125
126function rateVideo (req, res, finalCallback) {
127 const rateType = req.body.rating
128 const videoInstance = res.locals.video
129 const userInstance = res.locals.oauth.token.User
130
131 waterfall([
132 databaseUtils.startSerializableTransaction,
133
134 function findPreviousRate (t, callback) {
135 db.UserVideoRate.load(userInstance.id, videoInstance.id, t, function (err, previousRate) {
136 return callback(err, t, previousRate)
137 })
138 },
139
140 function insertUserRateIntoDB (t, previousRate, callback) {
141 const options = { transaction: t }
142
143 let likesToIncrement = 0
144 let dislikesToIncrement = 0
145
146 if (rateType === constants.VIDEO_RATE_TYPES.LIKE) likesToIncrement++
147 else if (rateType === constants.VIDEO_RATE_TYPES.DISLIKE) dislikesToIncrement++
148
149 // There was a previous rate, update it
150 if (previousRate) {
151 // We will remove the previous rate, so we will need to remove it from the video attribute
152 if (previousRate.type === constants.VIDEO_RATE_TYPES.LIKE) likesToIncrement--
153 else if (previousRate.type === constants.VIDEO_RATE_TYPES.DISLIKE) dislikesToIncrement--
154
155 previousRate.type = rateType
156
157 previousRate.save(options).asCallback(function (err) {
158 return callback(err, t, likesToIncrement, dislikesToIncrement)
159 })
160 } else { // There was not a previous rate, insert a new one
161 const query = {
162 userId: userInstance.id,
163 videoId: videoInstance.id,
164 type: rateType
165 }
166
167 db.UserVideoRate.create(query, options).asCallback(function (err) {
168 return callback(err, t, likesToIncrement, dislikesToIncrement)
169 })
170 }
171 },
172
173 function updateVideoAttributeDB (t, likesToIncrement, dislikesToIncrement, callback) {
174 const options = { transaction: t }
175 const incrementQuery = {
176 likes: likesToIncrement,
177 dislikes: dislikesToIncrement
178 }
179
180 // Even if we do not own the video we increment the attributes
181 // It is usefull for the user to have a feedback
182 videoInstance.increment(incrementQuery, options).asCallback(function (err) {
183 return callback(err, t, likesToIncrement, dislikesToIncrement)
184 })
185 },
186
187 function sendEventsToFriendsIfNeeded (t, likesToIncrement, dislikesToIncrement, callback) {
188 // No need for an event type, we own the video
189 if (videoInstance.isOwned()) return callback(null, t, likesToIncrement, dislikesToIncrement)
190
191 const eventsParams = []
192
193 if (likesToIncrement !== 0) {
194 eventsParams.push({
195 videoId: videoInstance.id,
196 type: constants.REQUEST_VIDEO_EVENT_TYPES.LIKES,
197 count: likesToIncrement
198 })
199 }
200
201 if (dislikesToIncrement !== 0) {
202 eventsParams.push({
203 videoId: videoInstance.id,
204 type: constants.REQUEST_VIDEO_EVENT_TYPES.DISLIKES,
205 count: dislikesToIncrement
206 })
207 }
208
209 friends.addEventsToRemoteVideo(eventsParams, t, function (err) {
210 return callback(err, t, likesToIncrement, dislikesToIncrement)
211 })
212 },
213
214 function sendQaduToFriendsIfNeeded (t, likesToIncrement, dislikesToIncrement, callback) {
215 // We do not own the video, there is no need to send a quick and dirty update to friends
216 // Our rate was already sent by the addEvent function
217 if (videoInstance.isOwned() === false) return callback(null, t)
218
219 const qadusParams = []
220
221 if (likesToIncrement !== 0) {
222 qadusParams.push({
223 videoId: videoInstance.id,
224 type: constants.REQUEST_VIDEO_QADU_TYPES.LIKES
225 })
226 }
227
228 if (dislikesToIncrement !== 0) {
229 qadusParams.push({
230 videoId: videoInstance.id,
231 type: constants.REQUEST_VIDEO_QADU_TYPES.DISLIKES
232 })
233 }
234
235 friends.quickAndDirtyUpdatesVideoToFriends(qadusParams, t, function (err) {
236 return callback(err, t)
237 })
238 },
239
240 databaseUtils.commitTransaction
241
242 ], function (err, t) {
243 if (err) {
244 // This is just a debug because we will retry the insert
245 logger.debug('Cannot add the user video rate.', { error: err })
246 return databaseUtils.rollbackTransaction(err, t, finalCallback)
247 }
248
249 logger.info('User video rate for video %s of user %s updated.', videoInstance.name, userInstance.username)
250 return finalCallback(null)
251 })
252}
253
107// Wrapper to video add that retry the function if there is a database error 254// Wrapper to video add that retry the function if there is a database error
108// We need this because we run the transaction in SERIALIZABLE isolation that can fail 255// We need this because we run the transaction in SERIALIZABLE isolation that can fail
109function addVideoRetryWrapper (req, res, next) { 256function addVideoRetryWrapper (req, res, next) {
@@ -155,8 +302,7 @@ function addVideo (req, res, videoFile, finalCallback) {
155 extname: path.extname(videoFile.filename), 302 extname: path.extname(videoFile.filename),
156 description: videoInfos.description, 303 description: videoInfos.description,
157 duration: videoFile.duration, 304 duration: videoFile.duration,
158 authorId: author.id, 305 authorId: author.id
159 views: videoInfos.views
160 } 306 }
161 307
162 const video = db.Video.build(videoData) 308 const video = db.Video.build(videoData)
@@ -332,11 +478,19 @@ function getVideo (req, res, next) {
332 478
333 // FIXME: make a real view system 479 // FIXME: make a real view system
334 // For example, only add a view when a user watch a video during 30s etc 480 // For example, only add a view when a user watch a video during 30s etc
335 friends.quickAndDirtyUpdateVideoToFriends(videoInstance.id, constants.REQUEST_VIDEO_QADU_TYPES.VIEWS) 481 const qaduParams = {
482 videoId: videoInstance.id,
483 type: constants.REQUEST_VIDEO_QADU_TYPES.VIEWS
484 }
485 friends.quickAndDirtyUpdateVideoToFriends(qaduParams)
336 }) 486 })
337 } else { 487 } else {
338 // Just send the event to our friends 488 // Just send the event to our friends
339 friends.addEventToRemoteVideo(videoInstance.id, constants.REQUEST_VIDEO_EVENT_TYPES.VIEWS) 489 const eventParams = {
490 videoId: videoInstance.id,
491 type: constants.REQUEST_VIDEO_EVENT_TYPES.VIEWS
492 }
493 friends.addEventToRemoteVideo(eventParams)
340 } 494 }
341 495
342 // Do not wait the view system 496 // 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) {
92 videosValidators.isVideoTagsValid(video.tags) && 92 videosValidators.isVideoTagsValid(video.tags) &&
93 videosValidators.isVideoRemoteIdValid(video.remoteId) && 93 videosValidators.isVideoRemoteIdValid(video.remoteId) &&
94 videosValidators.isVideoExtnameValid(video.extname) && 94 videosValidators.isVideoExtnameValid(video.extname) &&
95 videosValidators.isVideoViewsValid(video.views) 95 videosValidators.isVideoViewsValid(video.views) &&
96 videosValidators.isVideoLikesValid(video.likes) &&
97 videosValidators.isVideoDislikesValid(video.dislikes)
96} 98}
97 99
98function isRequestTypeAddValid (value) { 100function 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 @@
1'use strict' 1'use strict'
2 2
3const validator = require('express-validator').validator 3const validator = require('express-validator').validator
4const values = require('lodash/values')
4 5
5const constants = require('../../initializers/constants') 6const constants = require('../../initializers/constants')
6const usersValidators = require('./users') 7const usersValidators = require('./users')
@@ -26,6 +27,7 @@ const videosValidators = {
26 isVideoFile, 27 isVideoFile,
27 isVideoViewsValid, 28 isVideoViewsValid,
28 isVideoLikesValid, 29 isVideoLikesValid,
30 isVideoRatingTypeValid,
29 isVideoDislikesValid, 31 isVideoDislikesValid,
30 isVideoEventCountValid 32 isVideoEventCountValid
31} 33}
@@ -103,6 +105,10 @@ function isVideoEventCountValid (value) {
103 return validator.isInt(value + '', VIDEO_EVENTS_CONSTRAINTS_FIELDS.COUNT) 105 return validator.isInt(value + '', VIDEO_EVENTS_CONSTRAINTS_FIELDS.COUNT)
104} 106}
105 107
108function isVideoRatingTypeValid (value) {
109 return values(constants.VIDEO_RATE_TYPES).indexOf(value) !== -1
110}
111
106function isVideoFile (value, files) { 112function isVideoFile (value, files) {
107 // Should have files 113 // Should have files
108 if (!files) return false 114 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')
5 5
6// --------------------------------------------------------------------------- 6// ---------------------------------------------------------------------------
7 7
8const LAST_MIGRATION_VERSION = 15 8const LAST_MIGRATION_VERSION = 25
9 9
10// --------------------------------------------------------------------------- 10// ---------------------------------------------------------------------------
11 11
@@ -95,6 +95,11 @@ const CONSTRAINTS_FIELDS = {
95 } 95 }
96} 96}
97 97
98const VIDEO_RATE_TYPES = {
99 LIKE: 'like',
100 DISLIKE: 'dislike'
101}
102
98// --------------------------------------------------------------------------- 103// ---------------------------------------------------------------------------
99 104
100// Score a pod has when we create it as a friend 105// Score a pod has when we create it as a friend
@@ -249,7 +254,8 @@ module.exports = {
249 STATIC_MAX_AGE, 254 STATIC_MAX_AGE,
250 STATIC_PATHS, 255 STATIC_PATHS,
251 THUMBNAILS_SIZE, 256 THUMBNAILS_SIZE,
252 USER_ROLES 257 USER_ROLES,
258 VIDEO_RATE_TYPES
253} 259}
254 260
255// --------------------------------------------------------------------------- 261// ---------------------------------------------------------------------------
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 @@
1'use strict'
2
3// utils = { transaction, queryInterface, sequelize, Sequelize }
4exports.up = function (utils, finalCallback) {
5 const q = utils.queryInterface
6 const Sequelize = utils.Sequelize
7
8 const data = {
9 type: Sequelize.INTEGER,
10 allowNull: false,
11 defaultValue: 0
12 }
13
14 q.addColumn('Videos', 'likes', data, { transaction: utils.transaction }).asCallback(finalCallback)
15}
16
17exports.down = function (options, callback) {
18 throw new Error('Not implemented.')
19}
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 @@
1'use strict'
2
3// utils = { transaction, queryInterface, sequelize, Sequelize }
4exports.up = function (utils, finalCallback) {
5 const q = utils.queryInterface
6 const Sequelize = utils.Sequelize
7
8 const data = {
9 type: Sequelize.INTEGER,
10 allowNull: false,
11 defaultValue: 0
12 }
13
14 q.addColumn('Videos', 'dislikes', data, { transaction: utils.transaction }).asCallback(finalCallback)
15}
16
17exports.down = function (options, callback) {
18 throw new Error('Not implemented.')
19}
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 @@
3const each = require('async/each') 3const each = require('async/each')
4const eachLimit = require('async/eachLimit') 4const eachLimit = require('async/eachLimit')
5const eachSeries = require('async/eachSeries') 5const eachSeries = require('async/eachSeries')
6const series = require('async/series')
6const request = require('request') 7const request = require('request')
7const waterfall = require('async/waterfall') 8const waterfall = require('async/waterfall')
8 9
@@ -28,7 +29,9 @@ const friends = {
28 updateVideoToFriends, 29 updateVideoToFriends,
29 reportAbuseVideoToFriend, 30 reportAbuseVideoToFriend,
30 quickAndDirtyUpdateVideoToFriends, 31 quickAndDirtyUpdateVideoToFriends,
32 quickAndDirtyUpdatesVideoToFriends,
31 addEventToRemoteVideo, 33 addEventToRemoteVideo,
34 addEventsToRemoteVideo,
32 hasFriends, 35 hasFriends,
33 makeFriends, 36 makeFriends,
34 quitFriends, 37 quitFriends,
@@ -84,24 +87,52 @@ function reportAbuseVideoToFriend (reportData, video) {
84 createRequest(options) 87 createRequest(options)
85} 88}
86 89
87function quickAndDirtyUpdateVideoToFriends (videoId, type, transaction, callback) { 90function quickAndDirtyUpdateVideoToFriends (qaduParams, transaction, callback) {
88 const options = { 91 const options = {
89 videoId, 92 videoId: qaduParams.videoId,
90 type, 93 type: qaduParams.type,
91 transaction 94 transaction
92 } 95 }
93 return createVideoQaduRequest(options, callback) 96 return createVideoQaduRequest(options, callback)
94} 97}
95 98
96function addEventToRemoteVideo (videoId, type, transaction, callback) { 99function quickAndDirtyUpdatesVideoToFriends (qadusParams, transaction, finalCallback) {
100 const tasks = []
101
102 qadusParams.forEach(function (qaduParams) {
103 const fun = function (callback) {
104 quickAndDirtyUpdateVideoToFriends(qaduParams, transaction, callback)
105 }
106
107 tasks.push(fun)
108 })
109
110 series(tasks, finalCallback)
111}
112
113function addEventToRemoteVideo (eventParams, transaction, callback) {
97 const options = { 114 const options = {
98 videoId, 115 videoId: eventParams.videoId,
99 type, 116 type: eventParams.type,
100 transaction 117 transaction
101 } 118 }
102 createVideoEventRequest(options, callback) 119 createVideoEventRequest(options, callback)
103} 120}
104 121
122function addEventsToRemoteVideo (eventsParams, transaction, finalCallback) {
123 const tasks = []
124
125 eventsParams.forEach(function (eventParams) {
126 const fun = function (callback) {
127 addEventToRemoteVideo(eventParams, transaction, callback)
128 }
129
130 tasks.push(fun)
131 })
132
133 series(tasks, finalCallback)
134}
135
105function hasFriends (callback) { 136function hasFriends (callback) {
106 db.Pod.countAll(function (err, count) { 137 db.Pod.countAll(function (err, count) {
107 if (err) return callback(err) 138 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 {
44 } 44 }
45 } 45 }
46 46
47 const videoData = {} 47 // Maybe another attribute was filled for this video
48 let videoData = requestsToMakeGrouped[hashKey].videos[video.id]
49 if (!videoData) videoData = {}
50
48 switch (request.type) { 51 switch (request.type) {
49 case constants.REQUEST_VIDEO_QADU_TYPES.LIKES: 52 case constants.REQUEST_VIDEO_QADU_TYPES.LIKES:
50 videoData.likes = video.likes 53 videoData.likes = video.likes
51 break 54 break
52 55
53 case constants.REQUEST_VIDEO_QADU_TYPES.DISLIKES: 56 case constants.REQUEST_VIDEO_QADU_TYPES.DISLIKES:
54 videoData.likes = video.dislikes 57 videoData.dislikes = video.dislikes
55 break 58 break
56 59
57 case constants.REQUEST_VIDEO_QADU_TYPES.VIEWS: 60 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')
7const validatorsUsers = { 7const validatorsUsers = {
8 usersAdd, 8 usersAdd,
9 usersRemove, 9 usersRemove,
10 usersUpdate 10 usersUpdate,
11 usersVideoRating
11} 12}
12 13
13function usersAdd (req, res, next) { 14function usersAdd (req, res, next) {
@@ -62,6 +63,25 @@ function usersUpdate (req, res, next) {
62 checkErrors(req, res, next) 63 checkErrors(req, res, next)
63} 64}
64 65
66function usersVideoRating (req, res, next) {
67 req.checkParams('videoId', 'Should have a valid video id').notEmpty().isUUID(4)
68
69 logger.debug('Checking usersVideoRating parameters', { parameters: req.params })
70
71 checkErrors(req, res, function () {
72 db.Video.load(req.params.videoId, function (err, video) {
73 if (err) {
74 logger.error('Error in user request validator.', { error: err })
75 return res.sendStatus(500)
76 }
77
78 if (!video) return res.status(404).send('Video not found')
79
80 next()
81 })
82 })
83}
84
65// --------------------------------------------------------------------------- 85// ---------------------------------------------------------------------------
66 86
67module.exports = validatorsUsers 87module.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 = {
13 videosRemove, 13 videosRemove,
14 videosSearch, 14 videosSearch,
15 15
16 videoAbuseReport 16 videoAbuseReport,
17
18 videoRate
17} 19}
18 20
19function videosAdd (req, res, next) { 21function videosAdd (req, res, next) {
@@ -119,6 +121,17 @@ function videoAbuseReport (req, res, next) {
119 }) 121 })
120} 122}
121 123
124function videoRate (req, res, next) {
125 req.checkParams('id', 'Should have a valid id').notEmpty().isUUID(4)
126 req.checkBody('rating', 'Should have a valid rate type').isVideoRatingTypeValid()
127
128 logger.debug('Checking videoRate parameters', { parameters: req.body })
129
130 checkErrors(req, res, function () {
131 checkVideoExists(req.params.id, res, next)
132 })
133}
134
122// --------------------------------------------------------------------------- 135// ---------------------------------------------------------------------------
123 136
124module.exports = validatorsVideos 137module.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) {
83 if (podIds.length === 0) return callback(null, []) 83 if (podIds.length === 0) return callback(null, [])
84 84
85 const query = { 85 const query = {
86 order: [
87 [ 'id', 'ASC' ]
88 ],
86 include: [ 89 include: [
87 { 90 {
88 model: self.sequelize.models.Video, 91 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 @@
1'use strict'
2
3/*
4 User rates per video.
5
6*/
7
8const values = require('lodash/values')
9
10const constants = require('../initializers/constants')
11
12// ---------------------------------------------------------------------------
13
14module.exports = function (sequelize, DataTypes) {
15 const UserVideoRate = sequelize.define('UserVideoRate',
16 {
17 type: {
18 type: DataTypes.ENUM(values(constants.VIDEO_RATE_TYPES)),
19 allowNull: false
20 }
21 },
22 {
23 indexes: [
24 {
25 fields: [ 'videoId', 'userId', 'type' ],
26 unique: true
27 }
28 ],
29 classMethods: {
30 associate,
31
32 load
33 }
34 }
35 )
36
37 return UserVideoRate
38}
39
40// ------------------------------ STATICS ------------------------------
41
42function associate (models) {
43 this.belongsTo(models.Video, {
44 foreignKey: {
45 name: 'videoId',
46 allowNull: false
47 },
48 onDelete: 'CASCADE'
49 })
50
51 this.belongsTo(models.User, {
52 foreignKey: {
53 name: 'userId',
54 allowNull: false
55 },
56 onDelete: 'CASCADE'
57 })
58}
59
60function load (userId, videoId, transaction, callback) {
61 if (!callback) {
62 callback = transaction
63 transaction = null
64 }
65
66 const query = {
67 where: {
68 userId,
69 videoId
70 }
71 }
72
73 const options = {}
74 if (transaction) options.transaction = transaction
75
76 return this.findOne(query, options).asCallback(callback)
77}
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) {
89 min: 0, 89 min: 0,
90 isInt: true 90 isInt: true
91 } 91 }
92 },
93 likes: {
94 type: DataTypes.INTEGER,
95 allowNull: false,
96 defaultValue: 0,
97 validate: {
98 min: 0,
99 isInt: true
100 }
101 },
102 dislikes: {
103 type: DataTypes.INTEGER,
104 allowNull: false,
105 defaultValue: 0,
106 validate: {
107 min: 0,
108 isInt: true
109 }
92 } 110 }
93 }, 111 },
94 { 112 {
@@ -113,6 +131,9 @@ module.exports = function (sequelize, DataTypes) {
113 }, 131 },
114 { 132 {
115 fields: [ 'views' ] 133 fields: [ 'views' ]
134 },
135 {
136 fields: [ 'likes' ]
116 } 137 }
117 ], 138 ],
118 classMethods: { 139 classMethods: {
@@ -349,6 +370,8 @@ function toFormatedJSON () {
349 author: this.Author.name, 370 author: this.Author.name,
350 duration: this.duration, 371 duration: this.duration,
351 views: this.views, 372 views: this.views,
373 likes: this.likes,
374 dislikes: this.dislikes,
352 tags: map(this.Tags, 'name'), 375 tags: map(this.Tags, 'name'),
353 thumbnailPath: pathUtils.join(constants.STATIC_PATHS.THUMBNAILS, this.getThumbnailName()), 376 thumbnailPath: pathUtils.join(constants.STATIC_PATHS.THUMBNAILS, this.getThumbnailName()),
354 createdAt: this.createdAt, 377 createdAt: this.createdAt,
@@ -381,7 +404,9 @@ function toAddRemoteJSON (callback) {
381 createdAt: self.createdAt, 404 createdAt: self.createdAt,
382 updatedAt: self.updatedAt, 405 updatedAt: self.updatedAt,
383 extname: self.extname, 406 extname: self.extname,
384 views: self.views 407 views: self.views,
408 likes: self.likes,
409 dislikes: self.dislikes
385 } 410 }
386 411
387 return callback(null, remoteVideo) 412 return callback(null, remoteVideo)
@@ -400,7 +425,9 @@ function toUpdateRemoteJSON (callback) {
400 createdAt: this.createdAt, 425 createdAt: this.createdAt,
401 updatedAt: this.updatedAt, 426 updatedAt: this.updatedAt,
402 extname: this.extname, 427 extname: this.extname,
403 views: this.views 428 views: this.views,
429 likes: this.likes,
430 dislikes: this.dislikes
404 } 431 }
405 432
406 return json 433 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')
9const requestsUtils = require('../../utils/requests') 9const requestsUtils = require('../../utils/requests')
10const serversUtils = require('../../utils/servers') 10const serversUtils = require('../../utils/servers')
11const usersUtils = require('../../utils/users') 11const usersUtils = require('../../utils/users')
12const videosUtils = require('../../utils/videos')
12 13
13describe('Test users API validators', function () { 14describe('Test users API validators', function () {
14 const path = '/api/v1/users/' 15 const path = '/api/v1/users/'
15 let userId = null 16 let userId = null
16 let rootId = null 17 let rootId = null
18 let videoId = null
17 let server = null 19 let server = null
18 let userAccessToken = null 20 let userAccessToken = null
19 21
@@ -48,6 +50,23 @@ describe('Test users API validators', function () {
48 usersUtils.createUser(server.url, server.accessToken, username, password, next) 50 usersUtils.createUser(server.url, server.accessToken, username, password, next)
49 }, 51 },
50 function (next) { 52 function (next) {
53 const name = 'my super name for pod'
54 const description = 'my super description for pod'
55 const tags = [ 'tag' ]
56 const file = 'video_short2.webm'
57 videosUtils.uploadVideo(server.url, server.accessToken, name, description, tags, file, next)
58 },
59 function (next) {
60 videosUtils.getVideosList(server.url, function (err, res) {
61 if (err) throw err
62
63 const videos = res.body.data
64 videoId = videos[0].id
65
66 next()
67 })
68 },
69 function (next) {
51 const user = { 70 const user = {
52 username: 'user1', 71 username: 'user1',
53 password: 'my super password' 72 password: 'my super password'
@@ -289,6 +308,63 @@ describe('Test users API validators', function () {
289 }) 308 })
290 }) 309 })
291 310
311 describe('When getting my video rating', function () {
312 it('Should fail with a non authenticated user', function (done) {
313 request(server.url)
314 .get(path + 'me/videos/' + videoId + '/rating')
315 .set('Authorization', 'Bearer faketoken')
316 .set('Accept', 'application/json')
317 .expect(401, done)
318 })
319
320 it('Should fail with an incorrect video uuid', function (done) {
321 request(server.url)
322 .get(path + 'me/videos/blabla/rating')
323 .set('Authorization', 'Bearer ' + userAccessToken)
324 .set('Accept', 'application/json')
325 .expect(400, done)
326 })
327
328 it('Should fail with an unknown video', function (done) {
329 request(server.url)
330 .get(path + 'me/videos/4da6fde3-88f7-4d16-b119-108df5630b06/rating')
331 .set('Authorization', 'Bearer ' + userAccessToken)
332 .set('Accept', 'application/json')
333 .expect(404, done)
334 })
335
336 it('Should success with the correct parameters', function (done) {
337 request(server.url)
338 .get(path + 'me/videos/' + videoId + '/rating')
339 .set('Authorization', 'Bearer ' + userAccessToken)
340 .set('Accept', 'application/json')
341 .expect(200, done)
342 })
343 })
344
345 describe('When removing an user', function () {
346 it('Should fail with an incorrect id', function (done) {
347 request(server.url)
348 .delete(path + 'bla-bla')
349 .set('Authorization', 'Bearer ' + server.accessToken)
350 .expect(400, done)
351 })
352
353 it('Should fail with the root user', function (done) {
354 request(server.url)
355 .delete(path + rootId)
356 .set('Authorization', 'Bearer ' + server.accessToken)
357 .expect(400, done)
358 })
359
360 it('Should return 404 with a non existing id', function (done) {
361 request(server.url)
362 .delete(path + '45')
363 .set('Authorization', 'Bearer ' + server.accessToken)
364 .expect(404, done)
365 })
366 })
367
292 describe('When removing an user', function () { 368 describe('When removing an user', function () {
293 it('Should fail with an incorrect id', function (done) { 369 it('Should fail with an incorrect id', function (done) {
294 request(server.url) 370 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 () {
420 it('Should succeed with the correct parameters') 420 it('Should succeed with the correct parameters')
421 }) 421 })
422 422
423 describe('When rating a video', function () {
424 let videoId
425
426 before(function (done) {
427 videosUtils.getVideosList(server.url, function (err, res) {
428 if (err) throw err
429
430 videoId = res.body.data[0].id
431
432 return done()
433 })
434 })
435
436 it('Should fail without a valid uuid', function (done) {
437 const data = {
438 rating: 'like'
439 }
440 requestsUtils.makePutBodyRequest(server.url, path + 'blabla/rate', server.accessToken, data, done)
441 })
442
443 it('Should fail with an unknown id', function (done) {
444 const data = {
445 rating: 'like'
446 }
447 requestsUtils.makePutBodyRequest(server.url, path + '4da6fde3-88f7-4d16-b119-108df5630b06/rate', server.accessToken, data, done, 404)
448 })
449
450 it('Should fail with a wrong rating', function (done) {
451 const data = {
452 rating: 'likes'
453 }
454 requestsUtils.makePutBodyRequest(server.url, path + videoId + '/rate', server.accessToken, data, done)
455 })
456
457 it('Should succeed with the correct parameters', function (done) {
458 const data = {
459 rating: 'like'
460 }
461 requestsUtils.makePutBodyRequest(server.url, path + videoId + '/rate', server.accessToken, data, done, 204)
462 })
463 })
464
423 describe('When removing a video', function () { 465 describe('When removing a video', function () {
424 it('Should have 404 with nothing', function (done) { 466 it('Should have 404 with nothing', function (done) {
425 request(server.url) 467 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 @@
4 4
5const chai = require('chai') 5const chai = require('chai')
6const each = require('async/each') 6const each = require('async/each')
7const eachSeries = require('async/eachSeries')
7const expect = chai.expect 8const expect = chai.expect
8const parallel = require('async/parallel') 9const parallel = require('async/parallel')
9const series = require('async/series') 10const series = require('async/series')
@@ -378,7 +379,7 @@ describe('Test multiple pods', function () {
378 }) 379 })
379 }) 380 })
380 381
381 describe('Should update video views', function () { 382 describe('Should update video views, likes and dislikes', function () {
382 let localVideosPod3 = [] 383 let localVideosPod3 = []
383 let remoteVideosPod1 = [] 384 let remoteVideosPod1 = []
384 let remoteVideosPod2 = [] 385 let remoteVideosPod2 = []
@@ -419,7 +420,7 @@ describe('Test multiple pods', function () {
419 ], done) 420 ], done)
420 }) 421 })
421 422
422 it('Should views multiple videos on owned servers', function (done) { 423 it('Should view multiple videos on owned servers', function (done) {
423 this.timeout(30000) 424 this.timeout(30000)
424 425
425 parallel([ 426 parallel([
@@ -440,18 +441,18 @@ describe('Test multiple pods', function () {
440 }, 441 },
441 442
442 function (callback) { 443 function (callback) {
443 setTimeout(done, 22000) 444 setTimeout(callback, 22000)
444 } 445 }
445 ], function (err) { 446 ], function (err) {
446 if (err) throw err 447 if (err) throw err
447 448
448 each(servers, function (server, callback) { 449 eachSeries(servers, function (server, callback) {
449 videosUtils.getVideosList(server.url, function (err, res) { 450 videosUtils.getVideosList(server.url, function (err, res) {
450 if (err) throw err 451 if (err) throw err
451 452
452 const videos = res.body.data 453 const videos = res.body.data
453 expect(videos.find(video => video.views === 3)).to.be.exist 454 expect(videos.find(video => video.views === 3)).to.exist
454 expect(videos.find(video => video.views === 1)).to.be.exist 455 expect(videos.find(video => video.views === 1)).to.exist
455 456
456 callback() 457 callback()
457 }) 458 })
@@ -459,7 +460,7 @@ describe('Test multiple pods', function () {
459 }) 460 })
460 }) 461 })
461 462
462 it('Should views multiple videos on each servers', function (done) { 463 it('Should view multiple videos on each servers', function (done) {
463 this.timeout(30000) 464 this.timeout(30000)
464 465
465 parallel([ 466 parallel([
@@ -504,17 +505,17 @@ describe('Test multiple pods', function () {
504 }, 505 },
505 506
506 function (callback) { 507 function (callback) {
507 setTimeout(done, 22000) 508 setTimeout(callback, 22000)
508 } 509 }
509 ], function (err) { 510 ], function (err) {
510 if (err) throw err 511 if (err) throw err
511 512
512 let baseVideos = null 513 let baseVideos = null
513 each(servers, function (server, callback) { 514 eachSeries(servers, function (server, callback) {
514 videosUtils.getVideosList(server.url, function (err, res) { 515 videosUtils.getVideosList(server.url, function (err, res) {
515 if (err) throw err 516 if (err) throw err
516 517
517 const videos = res.body 518 const videos = res.body.data
518 519
519 // Initialize base videos for future comparisons 520 // Initialize base videos for future comparisons
520 if (baseVideos === null) { 521 if (baseVideos === null) {
@@ -522,10 +523,74 @@ describe('Test multiple pods', function () {
522 return callback() 523 return callback()
523 } 524 }
524 525
525 for (let i = 0; i < baseVideos.length; i++) { 526 baseVideos.forEach(baseVideo => {
526 expect(baseVideos[i].views).to.equal(videos[i].views) 527 const sameVideo = videos.find(video => video.name === baseVideo.name)
528 expect(baseVideo.views).to.equal(sameVideo.views)
529 })
530
531 callback()
532 })
533 }, done)
534 })
535 })
536
537 it('Should like and dislikes videos on different services', function (done) {
538 this.timeout(30000)
539
540 parallel([
541 function (callback) {
542 videosUtils.rateVideo(servers[0].url, servers[0].accessToken, remoteVideosPod1[0], 'like', callback)
543 },
544
545 function (callback) {
546 videosUtils.rateVideo(servers[0].url, servers[0].accessToken, remoteVideosPod1[0], 'dislike', callback)
547 },
548
549 function (callback) {
550 videosUtils.rateVideo(servers[0].url, servers[0].accessToken, remoteVideosPod1[0], 'like', callback)
551 },
552
553 function (callback) {
554 videosUtils.rateVideo(servers[2].url, servers[2].accessToken, localVideosPod3[1], 'like', callback)
555 },
556
557 function (callback) {
558 videosUtils.rateVideo(servers[2].url, servers[2].accessToken, localVideosPod3[1], 'dislike', callback)
559 },
560
561 function (callback) {
562 videosUtils.rateVideo(servers[2].url, servers[2].accessToken, remoteVideosPod3[1], 'dislike', callback)
563 },
564
565 function (callback) {
566 videosUtils.rateVideo(servers[2].url, servers[2].accessToken, remoteVideosPod3[0], 'like', callback)
567 },
568
569 function (callback) {
570 setTimeout(callback, 22000)
571 }
572 ], function (err) {
573 if (err) throw err
574
575 let baseVideos = null
576 eachSeries(servers, function (server, callback) {
577 videosUtils.getVideosList(server.url, function (err, res) {
578 if (err) throw err
579
580 const videos = res.body.data
581
582 // Initialize base videos for future comparisons
583 if (baseVideos === null) {
584 baseVideos = videos
585 return callback()
527 } 586 }
528 587
588 baseVideos.forEach(baseVideo => {
589 const sameVideo = videos.find(video => video.name === baseVideo.name)
590 expect(baseVideo.likes).to.equal(sameVideo.likes)
591 expect(baseVideo.dislikes).to.equal(sameVideo.dislikes)
592 })
593
529 callback() 594 callback()
530 }) 595 })
531 }, done) 596 }, 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 () {
609 }) 609 })
610 }) 610 })
611 611
612 it('Should like a video', function (done) {
613 videosUtils.rateVideo(server.url, server.accessToken, videoId, 'like', function (err) {
614 if (err) throw err
615
616 videosUtils.getVideo(server.url, videoId, function (err, res) {
617 if (err) throw err
618
619 const video = res.body
620
621 expect(video.likes).to.equal(1)
622 expect(video.dislikes).to.equal(0)
623
624 done()
625 })
626 })
627 })
628
629 it('Should dislike the same video', function (done) {
630 videosUtils.rateVideo(server.url, server.accessToken, videoId, 'dislike', function (err) {
631 if (err) throw err
632
633 videosUtils.getVideo(server.url, videoId, function (err, res) {
634 if (err) throw err
635
636 const video = res.body
637
638 expect(video.likes).to.equal(0)
639 expect(video.dislikes).to.equal(1)
640
641 done()
642 })
643 })
644 })
645
612 after(function (done) { 646 after(function (done) {
613 process.kill(-server.app.pid) 647 process.kill(-server.app.pid)
614 648
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')
10const podsUtils = require('../utils/pods') 10const podsUtils = require('../utils/pods')
11const serversUtils = require('../utils/servers') 11const serversUtils = require('../utils/servers')
12const usersUtils = require('../utils/users') 12const usersUtils = require('../utils/users')
13const requestsUtils = require('../utils/requests')
13const videosUtils = require('../utils/videos') 14const videosUtils = require('../utils/videos')
14 15
15describe('Test users', function () { 16describe('Test users', function () {
@@ -138,6 +139,23 @@ describe('Test users', function () {
138 videosUtils.uploadVideo(server.url, accessToken, name, description, tags, video, 204, done) 139 videosUtils.uploadVideo(server.url, accessToken, name, description, tags, video, 204, done)
139 }) 140 })
140 141
142 it('Should retrieve a video rating', function (done) {
143 videosUtils.rateVideo(server.url, accessToken, videoId, 'like', function (err) {
144 if (err) throw err
145
146 usersUtils.getUserVideoRating(server.url, accessToken, videoId, function (err, res) {
147 if (err) throw err
148
149 const rating = res.body
150
151 expect(rating.videoId).to.equal(videoId)
152 expect(rating.rating).to.equal('like')
153
154 done()
155 })
156 })
157 })
158
141 it('Should not be able to remove the video with an incorrect token', function (done) { 159 it('Should not be able to remove the video with an incorrect token', function (done) {
142 videosUtils.removeVideo(server.url, 'bad_token', videoId, 401, done) 160 videosUtils.removeVideo(server.url, 'bad_token', videoId, 401, done)
143 }) 161 })
@@ -150,10 +168,21 @@ describe('Test users', function () {
150 168
151 it('Should logout (revoke token)') 169 it('Should logout (revoke token)')
152 170
171 it('Should not be able to get the user informations')
172
153 it('Should not be able to upload a video') 173 it('Should not be able to upload a video')
154 174
155 it('Should not be able to remove a video') 175 it('Should not be able to remove a video')
156 176
177 it('Should not be able to rate a video', function (done) {
178 const path = '/api/v1/videos/'
179 const data = {
180 rating: 'likes'
181 }
182
183 requestsUtils.makePutBodyRequest(server.url, path + videoId, 'wrong token', data, done, 401)
184 })
185
157 it('Should be able to login again') 186 it('Should be able to login again')
158 187
159 it('Should have an expired access token') 188 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')
5const usersUtils = { 5const usersUtils = {
6 createUser, 6 createUser,
7 getUserInformation, 7 getUserInformation,
8 getUserVideoRating,
8 getUsersList, 9 getUsersList,
9 getUsersListPaginationAndSort, 10 getUsersListPaginationAndSort,
10 removeUser, 11 removeUser,
@@ -47,6 +48,18 @@ function getUserInformation (url, accessToken, end) {
47 .end(end) 48 .end(end)
48} 49}
49 50
51function getUserVideoRating (url, accessToken, videoId, end) {
52 const path = '/api/v1/users/me/videos/' + videoId + '/rating'
53
54 request(url)
55 .get(path)
56 .set('Accept', 'application/json')
57 .set('Authorization', 'Bearer ' + accessToken)
58 .expect(200)
59 .expect('Content-Type', /json/)
60 .end(end)
61}
62
50function getUsersList (url, end) { 63function getUsersList (url, end) {
51 const path = '/api/v1/users' 64 const path = '/api/v1/users'
52 65
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 = {
16 searchVideoWithSort, 16 searchVideoWithSort,
17 testVideoImage, 17 testVideoImage,
18 uploadVideo, 18 uploadVideo,
19 updateVideo 19 updateVideo,
20 rateVideo
20} 21}
21 22
22// ---------------------- Export functions -------------------- 23// ---------------------- Export functions --------------------
@@ -236,6 +237,23 @@ function updateVideo (url, accessToken, id, name, description, tags, specialStat
236 req.expect(specialStatus).end(end) 237 req.expect(specialStatus).end(end)
237} 238}
238 239
240function rateVideo (url, accessToken, id, rating, specialStatus, end) {
241 if (!end) {
242 end = specialStatus
243 specialStatus = 204
244 }
245
246 const path = '/api/v1/videos/' + id + '/rate'
247
248 request(url)
249 .put(path)
250 .set('Accept', 'application/json')
251 .set('Authorization', 'Bearer ' + accessToken)
252 .send({ rating })
253 .expect(specialStatus)
254 .end(end)
255}
256
239// --------------------------------------------------------------------------- 257// ---------------------------------------------------------------------------
240 258
241module.exports = videosUtils 259module.exports = videosUtils