diff options
author | Green-Star <Green-Star@users.noreply.github.com> | 2017-04-26 21:22:10 +0200 |
---|---|---|
committer | Bigard Florian <florian.bigard@gmail.com> | 2017-04-26 21:22:10 +0200 |
commit | 198b205c10dba362b9ae1ef6895b29d7e0dd685f (patch) | |
tree | 3be413139784f7445e775cbecccc6091a738360b | |
parent | 00871a261787ae1ed8446861ba2bd5eea9faca6d (diff) | |
download | PeerTube-198b205c10dba362b9ae1ef6895b29d7e0dd685f.tar.gz PeerTube-198b205c10dba362b9ae1ef6895b29d7e0dd685f.tar.zst PeerTube-198b205c10dba362b9ae1ef6895b29d7e0dd685f.zip |
Add ability for an administrator to remove any video (#61)
* Add ability for an admin to remove every video on the pod.
* Server: add BlacklistedVideos relation.
* Server: Insert in BlacklistedVideos relation upon deletion of a video.
* Server: Modify BlacklistedVideos schema to add Pod id information.
* Server: Moving insertion of a blacklisted video from the `afterDestroy` hook into the process of deletion of a video.
To avoid inserting a video when it is removed on its origin pod.
When a video is removed on its origin pod, the `afterDestroy` hook is fire, but no request is made on the delete('/:videoId') interface.
Hence, we insert into `BlacklistedVideos` only on request on delete('/:videoId') (if requirements for insertion are met).
* Server: Add removeVideoFromBlacklist hook on deletion of a video.
We are going to proceed in another way :).
We will add a new route : /:videoId/blacklist to blacklist a video.
We do not blacklist a video upon its deletion now (to distinguish a video blacklist from a regular video delete)
When we blacklist a video, the video remains in the DB, so we don't have any concern about its update. It just doesn't appear in the video list.
When we remove a video, we then have to remove it from the blacklist too.
We could also remove a video from the blacklist to 'unremove' it and make it appear again in the video list (will be another feature).
* Server: Add handler for new route post(/:videoId/blacklist)
* Client: Add isBlacklistable method
* Client: Update isRemovableBy method.
* Client: Move 'Delete video' feature from the video-list to the video-watch module.
* Server: Exclude blacklisted videos from the video list
* Server: Use findAll() in BlacklistedVideos.list() method
* Server: Fix addVideoToBlacklist function.
* Client: Add blacklist feature.
* Server: Use JavaScript Standard Style.
* Server: In checkUserCanDeleteVideo, move the callback call inside the db callback function
* Server: Modify BlacklistVideo relation
* Server: Modifiy Videos methods.
* Server: Add checkVideoIsBlacklistable method
* Server: Rewrite addVideoToBlacklist method
* Server: Fix checkVideoIsBlacklistable method
* Server: Add return to addVideoToBlacklist method
-rw-r--r-- | client/src/app/videos/shared/video.model.ts | 8 | ||||
-rw-r--r-- | client/src/app/videos/shared/video.service.ts | 6 | ||||
-rw-r--r-- | client/src/app/videos/video-list/video-list.component.ts | 5 | ||||
-rw-r--r-- | client/src/app/videos/video-list/video-miniature.component.html | 4 | ||||
-rw-r--r-- | client/src/app/videos/video-list/video-miniature.component.scss | 14 | ||||
-rw-r--r-- | client/src/app/videos/video-list/video-miniature.component.ts | 20 | ||||
-rw-r--r-- | client/src/app/videos/video-watch/video-watch.component.html | 12 | ||||
-rw-r--r-- | client/src/app/videos/video-watch/video-watch.component.ts | 47 | ||||
-rw-r--r-- | server/controllers/api/videos.js | 25 | ||||
-rw-r--r-- | server/middlewares/validators/videos.js | 63 | ||||
-rw-r--r-- | server/models/user.js | 8 | ||||
-rw-r--r-- | server/models/video-blacklist.js | 89 | ||||
-rw-r--r-- | server/models/video.js | 48 |
13 files changed, 283 insertions, 66 deletions
diff --git a/client/src/app/videos/shared/video.model.ts b/client/src/app/videos/shared/video.model.ts index 404e3bf45..1cfb312b6 100644 --- a/client/src/app/videos/shared/video.model.ts +++ b/client/src/app/videos/shared/video.model.ts | |||
@@ -85,8 +85,12 @@ export class Video { | |||
85 | this.by = Video.createByString(hash.author, hash.podHost); | 85 | this.by = Video.createByString(hash.author, hash.podHost); |
86 | } | 86 | } |
87 | 87 | ||
88 | isRemovableBy(user: User) { | 88 | isRemovableBy(user) { |
89 | return this.isLocal === true && user && this.author === user.username; | 89 | return user && this.isLocal === true && (this.author === user.username || user.isAdmin() === true); |
90 | } | ||
91 | |||
92 | isBlackistableBy(user) { | ||
93 | return user && user.isAdmin() === true && this.isLocal === false; | ||
90 | } | 94 | } |
91 | 95 | ||
92 | isVideoNSFWForUser(user: User) { | 96 | isVideoNSFWForUser(user: User) { |
diff --git a/client/src/app/videos/shared/video.service.ts b/client/src/app/videos/shared/video.service.ts index ee67bc1ae..a0965e20c 100644 --- a/client/src/app/videos/shared/video.service.ts +++ b/client/src/app/videos/shared/video.service.ts | |||
@@ -150,6 +150,12 @@ export class VideoService { | |||
150 | .catch((res) => this.restExtractor.handleError(res)); | 150 | .catch((res) => this.restExtractor.handleError(res)); |
151 | } | 151 | } |
152 | 152 | ||
153 | blacklistVideo(id: string) { | ||
154 | return this.authHttp.post(VideoService.BASE_VIDEO_URL + id + '/blacklist', {}) | ||
155 | .map(this.restExtractor.extractDataBool) | ||
156 | .catch((res) => this.restExtractor.handleError(res)); | ||
157 | } | ||
158 | |||
153 | private setVideoRate(id: string, rateType: RateType) { | 159 | private setVideoRate(id: string, rateType: RateType) { |
154 | const url = VideoService.BASE_VIDEO_URL + id + '/rate'; | 160 | const url = VideoService.BASE_VIDEO_URL + id + '/rate'; |
155 | const body = { | 161 | const body = { |
diff --git a/client/src/app/videos/video-list/video-list.component.ts b/client/src/app/videos/video-list/video-list.component.ts index ede1b51a9..b9f19b4f1 100644 --- a/client/src/app/videos/video-list/video-list.component.ts +++ b/client/src/app/videos/video-list/video-list.component.ts | |||
@@ -108,11 +108,6 @@ export class VideoListComponent implements OnInit, OnDestroy { | |||
108 | this.navigateToNewParams(); | 108 | this.navigateToNewParams(); |
109 | } | 109 | } |
110 | 110 | ||
111 | onRemoved(video: Video) { | ||
112 | this.notificationsService.success('Success', `Video ${video.name} deleted.`); | ||
113 | this.getVideos(); | ||
114 | } | ||
115 | |||
116 | onSort(sort: SortField) { | 111 | onSort(sort: SortField) { |
117 | this.sort = sort; | 112 | this.sort = sort; |
118 | 113 | ||
diff --git a/client/src/app/videos/video-list/video-miniature.component.html b/client/src/app/videos/video-list/video-miniature.component.html index 94b892698..b8b448631 100644 --- a/client/src/app/videos/video-list/video-miniature.component.html +++ b/client/src/app/videos/video-list/video-miniature.component.html | |||
@@ -10,10 +10,6 @@ | |||
10 | 10 | ||
11 | <span class="video-miniature-duration">{{ video.duration }}</span> | 11 | <span class="video-miniature-duration">{{ video.duration }}</span> |
12 | </a> | 12 | </a> |
13 | <span | ||
14 | *ngIf="displayRemoveIcon()" (click)="removeVideo(video.id)" | ||
15 | class="video-miniature-remove glyphicon glyphicon-remove" | ||
16 | ></span> | ||
17 | 13 | ||
18 | <div class="video-miniature-informations"> | 14 | <div class="video-miniature-informations"> |
19 | <span class="video-miniature-name-tags"> | 15 | <span class="video-miniature-name-tags"> |
diff --git a/client/src/app/videos/video-list/video-miniature.component.scss b/client/src/app/videos/video-list/video-miniature.component.scss index b8e90e8c5..f88ced819 100644 --- a/client/src/app/videos/video-list/video-miniature.component.scss +++ b/client/src/app/videos/video-list/video-miniature.component.scss | |||
@@ -42,20 +42,6 @@ | |||
42 | } | 42 | } |
43 | } | 43 | } |
44 | 44 | ||
45 | .video-miniature-remove { | ||
46 | display: inline-block; | ||
47 | position: absolute; | ||
48 | left: 16px; | ||
49 | background-color: rgba(0, 0, 0, 0.8); | ||
50 | color: rgba(255, 255, 255, 0.8); | ||
51 | padding: 2px; | ||
52 | cursor: pointer; | ||
53 | |||
54 | &:hover { | ||
55 | color: rgba(255, 255, 255, 0.9); | ||
56 | } | ||
57 | } | ||
58 | |||
59 | .video-miniature-informations { | 45 | .video-miniature-informations { |
60 | width: 200px; | 46 | width: 200px; |
61 | 47 | ||
diff --git a/client/src/app/videos/video-list/video-miniature.component.ts b/client/src/app/videos/video-list/video-miniature.component.ts index 888026dde..13deec381 100644 --- a/client/src/app/videos/video-list/video-miniature.component.ts +++ b/client/src/app/videos/video-list/video-miniature.component.ts | |||
@@ -13,8 +13,6 @@ import { User } from '../../shared'; | |||
13 | }) | 13 | }) |
14 | 14 | ||
15 | export class VideoMiniatureComponent { | 15 | export class VideoMiniatureComponent { |
16 | @Output() removed = new EventEmitter<any>(); | ||
17 | |||
18 | @Input() currentSort: SortField; | 16 | @Input() currentSort: SortField; |
19 | @Input() user: User; | 17 | @Input() user: User; |
20 | @Input() video: Video; | 18 | @Input() video: Video; |
@@ -28,10 +26,6 @@ export class VideoMiniatureComponent { | |||
28 | private videoService: VideoService | 26 | private videoService: VideoService |
29 | ) {} | 27 | ) {} |
30 | 28 | ||
31 | displayRemoveIcon() { | ||
32 | return this.hovering && this.video.isRemovableBy(this.user); | ||
33 | } | ||
34 | |||
35 | getVideoName() { | 29 | getVideoName() { |
36 | if (this.isVideoNSFWForThisUser()) | 30 | if (this.isVideoNSFWForThisUser()) |
37 | return 'NSFW'; | 31 | return 'NSFW'; |
@@ -47,20 +41,6 @@ export class VideoMiniatureComponent { | |||
47 | this.hovering = true; | 41 | this.hovering = true; |
48 | } | 42 | } |
49 | 43 | ||
50 | removeVideo(id: string) { | ||
51 | this.confirmService.confirm('Do you really want to delete this video?', 'Delete').subscribe( | ||
52 | res => { | ||
53 | if (res === false) return; | ||
54 | |||
55 | this.videoService.removeVideo(id).subscribe( | ||
56 | status => this.removed.emit(true), | ||
57 | |||
58 | error => this.notificationsService.error('Error', error.text) | ||
59 | ); | ||
60 | } | ||
61 | ); | ||
62 | } | ||
63 | |||
64 | isVideoNSFWForThisUser() { | 44 | isVideoNSFWForThisUser() { |
65 | return this.video.isVideoNSFWForUser(this.user); | 45 | return this.video.isVideoNSFWForUser(this.user); |
66 | } | 46 | } |
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 19e9bd9ed..ed26b513e 100644 --- a/client/src/app/videos/video-watch/video-watch.component.html +++ b/client/src/app/videos/video-watch/video-watch.component.html | |||
@@ -96,6 +96,18 @@ | |||
96 | <span class="glyphicon glyphicon-alert"></span> Report | 96 | <span class="glyphicon glyphicon-alert"></span> Report |
97 | </a> | 97 | </a> |
98 | </li> | 98 | </li> |
99 | |||
100 | <li *ngIf="isVideoRemovable()" role="menuitem"> | ||
101 | <a class="dropdown-item" title="Delete this video" href="#" (click)="removeVideo($event)"> | ||
102 | <span class="glyphicon glyphicon-remove"></span> Delete | ||
103 | </a> | ||
104 | </li> | ||
105 | |||
106 | <li *ngIf="isVideoBlacklistable()" role="menuitem"> | ||
107 | <a class="dropdown-item" title="Blacklist this video" href="#" (click)="blacklistVideo($event)"> | ||
108 | <span class="glyphicon glyphicon-eye-close"></span> Blacklist | ||
109 | </a> | ||
110 | </li> | ||
99 | </ul> | 111 | </ul> |
100 | </div> | 112 | </div> |
101 | </div> | 113 | </div> |
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 e04626a67..f582df45c 100644 --- a/client/src/app/videos/video-watch/video-watch.component.ts +++ b/client/src/app/videos/video-watch/video-watch.component.ts | |||
@@ -169,6 +169,45 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
169 | ); | 169 | ); |
170 | } | 170 | } |
171 | 171 | ||
172 | removeVideo(event: Event) { | ||
173 | event.preventDefault(); | ||
174 | this.confirmService.confirm('Do you really want to delete this video?', 'Delete').subscribe( | ||
175 | res => { | ||
176 | if (res === false) return; | ||
177 | |||
178 | this.videoService.removeVideo(this.video.id) | ||
179 | .subscribe( | ||
180 | status => { | ||
181 | this.notificationsService.success('Success', `Video ${this.video.name} deleted.`) | ||
182 | // Go back to the video-list. | ||
183 | this.router.navigate(['/videos/list']) | ||
184 | }, | ||
185 | |||
186 | error => this.notificationsService.error('Error', error.text) | ||
187 | ); | ||
188 | } | ||
189 | ); | ||
190 | } | ||
191 | |||
192 | blacklistVideo(event: Event) { | ||
193 | event.preventDefault() | ||
194 | this.confirmService.confirm('Do you really want to blacklist this video ?', 'Blacklist').subscribe( | ||
195 | res => { | ||
196 | if (res === false) return; | ||
197 | |||
198 | this.videoService.blacklistVideo(this.video.id) | ||
199 | .subscribe( | ||
200 | status => { | ||
201 | this.notificationsService.success('Success', `Video ${this.video.name} had been blacklisted.`) | ||
202 | this.router.navigate(['/videos/list']) | ||
203 | }, | ||
204 | |||
205 | error => this.notificationsService.error('Error', error.text) | ||
206 | ) | ||
207 | } | ||
208 | ) | ||
209 | } | ||
210 | |||
172 | showReportModal(event: Event) { | 211 | showReportModal(event: Event) { |
173 | event.preventDefault(); | 212 | event.preventDefault(); |
174 | this.videoReportModal.show(); | 213 | this.videoReportModal.show(); |
@@ -192,6 +231,14 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
192 | this.authService.getUser().username === this.video.author; | 231 | this.authService.getUser().username === this.video.author; |
193 | } | 232 | } |
194 | 233 | ||
234 | isVideoRemovable() { | ||
235 | return this.video.isRemovableBy(this.authService.getUser()); | ||
236 | } | ||
237 | |||
238 | isVideoBlacklistable() { | ||
239 | return this.video.isBlackistableBy(this.authService.getUser()); | ||
240 | } | ||
241 | |||
195 | private checkUserRating() { | 242 | private checkUserRating() { |
196 | // Unlogged users do not have ratings | 243 | // Unlogged users do not have ratings |
197 | if (this.isUserLoggedIn() === false) return; | 244 | if (this.isUserLoggedIn() === false) return; |
diff --git a/server/controllers/api/videos.js b/server/controllers/api/videos.js index 5e9ff482f..1f7d30eef 100644 --- a/server/controllers/api/videos.js +++ b/server/controllers/api/videos.js | |||
@@ -93,11 +93,13 @@ router.get('/:id', | |||
93 | validatorsVideos.videosGet, | 93 | validatorsVideos.videosGet, |
94 | getVideo | 94 | getVideo |
95 | ) | 95 | ) |
96 | |||
96 | router.delete('/:id', | 97 | router.delete('/:id', |
97 | oAuth.authenticate, | 98 | oAuth.authenticate, |
98 | validatorsVideos.videosRemove, | 99 | validatorsVideos.videosRemove, |
99 | removeVideo | 100 | removeVideo |
100 | ) | 101 | ) |
102 | |||
101 | router.get('/search/:value', | 103 | router.get('/search/:value', |
102 | validatorsVideos.videosSearch, | 104 | validatorsVideos.videosSearch, |
103 | validatorsPagination.pagination, | 105 | validatorsPagination.pagination, |
@@ -108,6 +110,13 @@ router.get('/search/:value', | |||
108 | searchVideos | 110 | searchVideos |
109 | ) | 111 | ) |
110 | 112 | ||
113 | router.post('/:id/blacklist', | ||
114 | oAuth.authenticate, | ||
115 | admin.ensureIsAdmin, | ||
116 | validatorsVideos.videosBlacklist, | ||
117 | addVideoToBlacklist | ||
118 | ) | ||
119 | |||
111 | // --------------------------------------------------------------------------- | 120 | // --------------------------------------------------------------------------- |
112 | 121 | ||
113 | module.exports = router | 122 | module.exports = router |
@@ -622,3 +631,19 @@ function reportVideoAbuse (req, res, finalCallback) { | |||
622 | return finalCallback(null) | 631 | return finalCallback(null) |
623 | }) | 632 | }) |
624 | } | 633 | } |
634 | |||
635 | function addVideoToBlacklist (req, res, next) { | ||
636 | const videoInstance = res.locals.video | ||
637 | |||
638 | db.BlacklistedVideo.create({ | ||
639 | videoId: videoInstance.id | ||
640 | }) | ||
641 | .asCallback(function (err) { | ||
642 | if (err) { | ||
643 | logger.error('Errors when blacklisting video ', { error: err }) | ||
644 | return next(err) | ||
645 | } | ||
646 | |||
647 | return res.type('json').status(204).end() | ||
648 | }) | ||
649 | } | ||
diff --git a/server/middlewares/validators/videos.js b/server/middlewares/validators/videos.js index c07825e50..86a7e39ae 100644 --- a/server/middlewares/validators/videos.js +++ b/server/middlewares/validators/videos.js | |||
@@ -15,7 +15,9 @@ const validatorsVideos = { | |||
15 | 15 | ||
16 | videoAbuseReport, | 16 | videoAbuseReport, |
17 | 17 | ||
18 | videoRate | 18 | videoRate, |
19 | |||
20 | videosBlacklist | ||
19 | } | 21 | } |
20 | 22 | ||
21 | function videosAdd (req, res, next) { | 23 | function videosAdd (req, res, next) { |
@@ -95,15 +97,10 @@ function videosRemove (req, res, next) { | |||
95 | checkVideoExists(req.params.id, res, function () { | 97 | checkVideoExists(req.params.id, res, function () { |
96 | // We need to make additional checks | 98 | // We need to make additional checks |
97 | 99 | ||
98 | if (res.locals.video.isOwned() === false) { | 100 | // Check if the user who did the request is able to delete the video |
99 | return res.status(403).send('Cannot remove video of another pod') | 101 | checkUserCanDeleteVideo(res.locals.oauth.token.User.id, res, function () { |
100 | } | 102 | next() |
101 | 103 | }) | |
102 | if (res.locals.video.Author.userId !== res.locals.oauth.token.User.id) { | ||
103 | return res.status(403).send('Cannot remove video of another user') | ||
104 | } | ||
105 | |||
106 | next() | ||
107 | }) | 104 | }) |
108 | }) | 105 | }) |
109 | } | 106 | } |
@@ -159,3 +156,49 @@ function checkVideoExists (id, res, callback) { | |||
159 | callback() | 156 | callback() |
160 | }) | 157 | }) |
161 | } | 158 | } |
159 | |||
160 | function checkUserCanDeleteVideo (userId, res, callback) { | ||
161 | // Retrieve the user who did the request | ||
162 | db.User.loadById(userId, function (err, user) { | ||
163 | if (err) { | ||
164 | logger.error('Error in video request validator.', { error: err }) | ||
165 | return res.sendStatus(500) | ||
166 | } | ||
167 | |||
168 | // Check if the user can delete the video | ||
169 | // The user can delete it if s/he an admin | ||
170 | // Or if s/he is the video's author | ||
171 | if (user.isAdmin() === false) { | ||
172 | if (res.locals.video.isOwned() === false) { | ||
173 | return res.status(403).send('Cannot remove video of another pod') | ||
174 | } | ||
175 | |||
176 | if (res.locals.video.Author.userId !== res.locals.oauth.token.User.id) { | ||
177 | return res.status(403).send('Cannot remove video of another user') | ||
178 | } | ||
179 | } | ||
180 | |||
181 | // If we reach this comment, we can delete the video | ||
182 | callback() | ||
183 | }) | ||
184 | } | ||
185 | |||
186 | function checkVideoIsBlacklistable (req, res, callback) { | ||
187 | if (res.locals.video.isOwned() === true) { | ||
188 | return res.status(403).send('Cannot blacklist a local video') | ||
189 | } | ||
190 | |||
191 | callback() | ||
192 | } | ||
193 | |||
194 | function videosBlacklist (req, res, next) { | ||
195 | req.checkParams('id', 'Should have a valid id').notEmpty().isUUID(4) | ||
196 | |||
197 | logger.debug('Checking videosBlacklist parameters', { parameters: req.params }) | ||
198 | |||
199 | checkErrors(req, res, function () { | ||
200 | checkVideoExists(req.params.id, res, function() { | ||
201 | checkVideoIsBlacklistable(req, res, next) | ||
202 | }) | ||
203 | }) | ||
204 | } | ||
diff --git a/server/models/user.js b/server/models/user.js index e64bab8ab..8f9c2bf65 100644 --- a/server/models/user.js +++ b/server/models/user.js | |||
@@ -79,7 +79,8 @@ module.exports = function (sequelize, DataTypes) { | |||
79 | }, | 79 | }, |
80 | instanceMethods: { | 80 | instanceMethods: { |
81 | isPasswordMatch, | 81 | isPasswordMatch, |
82 | toFormatedJSON | 82 | toFormatedJSON, |
83 | isAdmin | ||
83 | }, | 84 | }, |
84 | hooks: { | 85 | hooks: { |
85 | beforeCreate: beforeCreateOrUpdate, | 86 | beforeCreate: beforeCreateOrUpdate, |
@@ -117,6 +118,11 @@ function toFormatedJSON () { | |||
117 | createdAt: this.createdAt | 118 | createdAt: this.createdAt |
118 | } | 119 | } |
119 | } | 120 | } |
121 | |||
122 | function isAdmin () { | ||
123 | return this.role === constants.USER_ROLES.ADMIN | ||
124 | } | ||
125 | |||
120 | // ------------------------------ STATICS ------------------------------ | 126 | // ------------------------------ STATICS ------------------------------ |
121 | 127 | ||
122 | function associate (models) { | 128 | function associate (models) { |
diff --git a/server/models/video-blacklist.js b/server/models/video-blacklist.js new file mode 100644 index 000000000..02ea15760 --- /dev/null +++ b/server/models/video-blacklist.js | |||
@@ -0,0 +1,89 @@ | |||
1 | 'use strict' | ||
2 | |||
3 | const modelUtils = require('./utils') | ||
4 | |||
5 | // --------------------------------------------------------------------------- | ||
6 | |||
7 | module.exports = function (sequelize, DataTypes) { | ||
8 | const BlacklistedVideo = sequelize.define('BlacklistedVideo', | ||
9 | {}, | ||
10 | { | ||
11 | indexes: [ | ||
12 | { | ||
13 | fields: [ 'videoId' ], | ||
14 | unique: true | ||
15 | } | ||
16 | ], | ||
17 | classMethods: { | ||
18 | associate, | ||
19 | |||
20 | countTotal, | ||
21 | list, | ||
22 | listForApi, | ||
23 | loadById, | ||
24 | loadByVideoId | ||
25 | }, | ||
26 | instanceMethods: { | ||
27 | toFormatedJSON | ||
28 | }, | ||
29 | hooks: {} | ||
30 | } | ||
31 | ) | ||
32 | |||
33 | return BlacklistedVideo | ||
34 | } | ||
35 | |||
36 | // ------------------------------ METHODS ------------------------------ | ||
37 | |||
38 | function toFormatedJSON () { | ||
39 | return { | ||
40 | id: this.id, | ||
41 | videoId: this.videoId, | ||
42 | createdAt: this.createdAt | ||
43 | } | ||
44 | } | ||
45 | |||
46 | // ------------------------------ STATICS ------------------------------ | ||
47 | |||
48 | function associate (models) { | ||
49 | this.belongsTo(models.Video, { | ||
50 | foreignKey: 'videoId', | ||
51 | onDelete: 'cascade' | ||
52 | }) | ||
53 | } | ||
54 | |||
55 | function countTotal (callback) { | ||
56 | return this.count().asCallback(callback) | ||
57 | } | ||
58 | |||
59 | function list (callback) { | ||
60 | return this.findAll().asCallback(callback) | ||
61 | } | ||
62 | |||
63 | function listForApi (start, count, sort, callback) { | ||
64 | const query = { | ||
65 | offset: start, | ||
66 | limit: count, | ||
67 | order: [ modelUtils.getSort(sort) ] | ||
68 | } | ||
69 | |||
70 | return this.findAndCountAll(query).asCallback(function (err, result) { | ||
71 | if (err) return callback(err) | ||
72 | |||
73 | return callback(null, result.rows, result.count) | ||
74 | }) | ||
75 | } | ||
76 | |||
77 | function loadById (id, callback) { | ||
78 | return this.findById(id).asCallback(callback) | ||
79 | } | ||
80 | |||
81 | function loadByVideoId (id, callback) { | ||
82 | const query = { | ||
83 | where: { | ||
84 | videoId: id | ||
85 | } | ||
86 | } | ||
87 | |||
88 | return this.find(query).asCallback(callback) | ||
89 | } | ||
diff --git a/server/models/video.js b/server/models/video.js index 39eb28ed9..1addfa682 100644 --- a/server/models/video.js +++ b/server/models/video.js | |||
@@ -16,6 +16,7 @@ const logger = require('../helpers/logger') | |||
16 | const friends = require('../lib/friends') | 16 | const friends = require('../lib/friends') |
17 | const modelUtils = require('./utils') | 17 | const modelUtils = require('./utils') |
18 | const customVideosValidators = require('../helpers/custom-validators').videos | 18 | const customVideosValidators = require('../helpers/custom-validators').videos |
19 | const db = require('../initializers/database') | ||
19 | 20 | ||
20 | // --------------------------------------------------------------------------- | 21 | // --------------------------------------------------------------------------- |
21 | 22 | ||
@@ -201,7 +202,8 @@ module.exports = function (sequelize, DataTypes) { | |||
201 | isOwned, | 202 | isOwned, |
202 | toFormatedJSON, | 203 | toFormatedJSON, |
203 | toAddRemoteJSON, | 204 | toAddRemoteJSON, |
204 | toUpdateRemoteJSON | 205 | toUpdateRemoteJSON, |
206 | removeFromBlacklist | ||
205 | }, | 207 | }, |
206 | hooks: { | 208 | hooks: { |
207 | beforeValidate, | 209 | beforeValidate, |
@@ -528,6 +530,7 @@ function list (callback) { | |||
528 | } | 530 | } |
529 | 531 | ||
530 | function listForApi (start, count, sort, callback) { | 532 | function listForApi (start, count, sort, callback) { |
533 | // Exclude Blakclisted videos from the list | ||
531 | const query = { | 534 | const query = { |
532 | offset: start, | 535 | offset: start, |
533 | limit: count, | 536 | limit: count, |
@@ -540,7 +543,12 @@ function listForApi (start, count, sort, callback) { | |||
540 | }, | 543 | }, |
541 | 544 | ||
542 | this.sequelize.models.Tag | 545 | this.sequelize.models.Tag |
543 | ] | 546 | ], |
547 | where: { | ||
548 | id: { $notIn: this.sequelize.literal( | ||
549 | '(SELECT "BlacklistedVideos"."videoId" FROM "BlacklistedVideos")' | ||
550 | )} | ||
551 | } | ||
544 | } | 552 | } |
545 | 553 | ||
546 | return this.findAndCountAll(query).asCallback(function (err, result) { | 554 | return this.findAndCountAll(query).asCallback(function (err, result) { |
@@ -648,7 +656,11 @@ function searchAndPopulateAuthorAndPodAndTags (value, field, start, count, sort, | |||
648 | } | 656 | } |
649 | 657 | ||
650 | const query = { | 658 | const query = { |
651 | where: {}, | 659 | where: { |
660 | id: { $notIn: this.sequelize.literal( | ||
661 | '(SELECT "BlacklistedVideos"."videoId" FROM "BlacklistedVideos")' | ||
662 | )} | ||
663 | }, | ||
652 | offset: start, | 664 | offset: start, |
653 | limit: count, | 665 | limit: count, |
654 | distinct: true, // For the count, a video can have many tags | 666 | distinct: true, // For the count, a video can have many tags |
@@ -661,13 +673,9 @@ function searchAndPopulateAuthorAndPodAndTags (value, field, start, count, sort, | |||
661 | query.where.infoHash = infoHash | 673 | query.where.infoHash = infoHash |
662 | } else if (field === 'tags') { | 674 | } else if (field === 'tags') { |
663 | const escapedValue = this.sequelize.escape('%' + value + '%') | 675 | const escapedValue = this.sequelize.escape('%' + value + '%') |
664 | query.where = { | 676 | query.where.id.$in = this.sequelize.literal( |
665 | id: { | 677 | '(SELECT "VideoTags"."videoId" FROM "Tags" INNER JOIN "VideoTags" ON "Tags"."id" = "VideoTags"."tagId" WHERE name LIKE ' + escapedValue + ')' |
666 | $in: this.sequelize.literal( | 678 | ) |
667 | '(SELECT "VideoTags"."videoId" FROM "Tags" INNER JOIN "VideoTags" ON "Tags"."id" = "VideoTags"."tagId" WHERE name LIKE ' + escapedValue + ')' | ||
668 | ) | ||
669 | } | ||
670 | } | ||
671 | } else if (field === 'host') { | 679 | } else if (field === 'host') { |
672 | // FIXME: Include our pod? (not stored in the database) | 680 | // FIXME: Include our pod? (not stored in the database) |
673 | podInclude.where = { | 681 | podInclude.where = { |
@@ -755,3 +763,23 @@ function generateImage (video, videoPath, folder, imageName, size, callback) { | |||
755 | }) | 763 | }) |
756 | .thumbnail(options) | 764 | .thumbnail(options) |
757 | } | 765 | } |
766 | |||
767 | function removeFromBlacklist (video, callback) { | ||
768 | // Find the blacklisted video | ||
769 | db.BlacklistedVideo.loadByVideoId(video.id, function (err, video) { | ||
770 | // If an error occured, stop here | ||
771 | if (err) { | ||
772 | logger.error('Error when fetching video from blacklist.', { error: err }) | ||
773 | |||
774 | return callback(err) | ||
775 | } | ||
776 | |||
777 | // If we found the video, remove it from the blacklist | ||
778 | if (video) { | ||
779 | video.destroy().asCallback(callback) | ||
780 | } else { | ||
781 | // If haven't found it, simply ignore it and do nothing | ||
782 | return callback() | ||
783 | } | ||
784 | }) | ||
785 | } | ||