diff options
Diffstat (limited to 'server/models/video.js')
-rw-r--r-- | server/models/video.js | 532 |
1 files changed, 403 insertions, 129 deletions
diff --git a/server/models/video.js b/server/models/video.js index 330067cdf..17eff6428 100644 --- a/server/models/video.js +++ b/server/models/video.js | |||
@@ -1,108 +1,160 @@ | |||
1 | 'use strict' | 1 | 'use strict' |
2 | 2 | ||
3 | const Buffer = require('safe-buffer').Buffer | ||
3 | const createTorrent = require('create-torrent') | 4 | const createTorrent = require('create-torrent') |
4 | const ffmpeg = require('fluent-ffmpeg') | 5 | const ffmpeg = require('fluent-ffmpeg') |
5 | const fs = require('fs') | 6 | const fs = require('fs') |
6 | const magnetUtil = require('magnet-uri') | 7 | const magnetUtil = require('magnet-uri') |
8 | const map = require('lodash/map') | ||
7 | const parallel = require('async/parallel') | 9 | const parallel = require('async/parallel') |
8 | const parseTorrent = require('parse-torrent') | 10 | const parseTorrent = require('parse-torrent') |
9 | const pathUtils = require('path') | 11 | const pathUtils = require('path') |
10 | const mongoose = require('mongoose') | 12 | const values = require('lodash/values') |
11 | 13 | ||
12 | const constants = require('../initializers/constants') | 14 | const constants = require('../initializers/constants') |
13 | const customVideosValidators = require('../helpers/custom-validators').videos | ||
14 | const logger = require('../helpers/logger') | 15 | const logger = require('../helpers/logger') |
16 | const friends = require('../lib/friends') | ||
15 | const modelUtils = require('./utils') | 17 | const modelUtils = require('./utils') |
18 | const customVideosValidators = require('../helpers/custom-validators').videos | ||
16 | 19 | ||
17 | // --------------------------------------------------------------------------- | 20 | // --------------------------------------------------------------------------- |
18 | 21 | ||
19 | // TODO: add indexes on searchable columns | 22 | module.exports = function (sequelize, DataTypes) { |
20 | const VideoSchema = mongoose.Schema({ | 23 | const Video = sequelize.define('Video', |
21 | name: String, | 24 | { |
22 | extname: { | 25 | id: { |
23 | type: String, | 26 | type: DataTypes.UUID, |
24 | enum: [ '.mp4', '.webm', '.ogv' ] | 27 | defaultValue: DataTypes.UUIDV4, |
25 | }, | 28 | primaryKey: true, |
26 | remoteId: mongoose.Schema.Types.ObjectId, | 29 | validate: { |
27 | description: String, | 30 | isUUID: 4 |
28 | magnet: { | 31 | } |
29 | infoHash: String | ||
30 | }, | ||
31 | podHost: String, | ||
32 | author: String, | ||
33 | duration: Number, | ||
34 | tags: [ String ], | ||
35 | createdDate: { | ||
36 | type: Date, | ||
37 | default: Date.now | ||
38 | } | ||
39 | }) | ||
40 | |||
41 | VideoSchema.path('name').validate(customVideosValidators.isVideoNameValid) | ||
42 | VideoSchema.path('description').validate(customVideosValidators.isVideoDescriptionValid) | ||
43 | VideoSchema.path('podHost').validate(customVideosValidators.isVideoPodHostValid) | ||
44 | VideoSchema.path('author').validate(customVideosValidators.isVideoAuthorValid) | ||
45 | VideoSchema.path('duration').validate(customVideosValidators.isVideoDurationValid) | ||
46 | VideoSchema.path('tags').validate(customVideosValidators.isVideoTagsValid) | ||
47 | |||
48 | VideoSchema.methods = { | ||
49 | generateMagnetUri, | ||
50 | getVideoFilename, | ||
51 | getThumbnailName, | ||
52 | getPreviewName, | ||
53 | getTorrentName, | ||
54 | isOwned, | ||
55 | toFormatedJSON, | ||
56 | toRemoteJSON | ||
57 | } | ||
58 | |||
59 | VideoSchema.statics = { | ||
60 | generateThumbnailFromBase64, | ||
61 | getDurationFromFile, | ||
62 | listForApi, | ||
63 | listByHostAndRemoteId, | ||
64 | listByHost, | ||
65 | listOwned, | ||
66 | listOwnedByAuthor, | ||
67 | listRemotes, | ||
68 | load, | ||
69 | search | ||
70 | } | ||
71 | |||
72 | VideoSchema.pre('remove', function (next) { | ||
73 | const video = this | ||
74 | const tasks = [] | ||
75 | |||
76 | tasks.push( | ||
77 | function (callback) { | ||
78 | removeThumbnail(video, callback) | ||
79 | } | ||
80 | ) | ||
81 | |||
82 | if (video.isOwned()) { | ||
83 | tasks.push( | ||
84 | function (callback) { | ||
85 | removeFile(video, callback) | ||
86 | }, | 32 | }, |
87 | function (callback) { | 33 | name: { |
88 | removeTorrent(video, callback) | 34 | type: DataTypes.STRING, |
35 | allowNull: false, | ||
36 | validate: { | ||
37 | nameValid: function (value) { | ||
38 | const res = customVideosValidators.isVideoNameValid(value) | ||
39 | if (res === false) throw new Error('Video name is not valid.') | ||
40 | } | ||
41 | } | ||
89 | }, | 42 | }, |
90 | function (callback) { | 43 | extname: { |
91 | removePreview(video, callback) | 44 | type: DataTypes.ENUM(values(constants.CONSTRAINTS_FIELDS.VIDEOS.EXTNAME)), |
45 | allowNull: false | ||
46 | }, | ||
47 | remoteId: { | ||
48 | type: DataTypes.UUID, | ||
49 | allowNull: true, | ||
50 | validate: { | ||
51 | isUUID: 4 | ||
52 | } | ||
53 | }, | ||
54 | description: { | ||
55 | type: DataTypes.STRING, | ||
56 | allowNull: false, | ||
57 | validate: { | ||
58 | descriptionValid: function (value) { | ||
59 | const res = customVideosValidators.isVideoDescriptionValid(value) | ||
60 | if (res === false) throw new Error('Video description is not valid.') | ||
61 | } | ||
62 | } | ||
63 | }, | ||
64 | infoHash: { | ||
65 | type: DataTypes.STRING, | ||
66 | allowNull: false, | ||
67 | validate: { | ||
68 | infoHashValid: function (value) { | ||
69 | const res = customVideosValidators.isVideoInfoHashValid(value) | ||
70 | if (res === false) throw new Error('Video info hash is not valid.') | ||
71 | } | ||
72 | } | ||
73 | }, | ||
74 | duration: { | ||
75 | type: DataTypes.INTEGER, | ||
76 | allowNull: false, | ||
77 | validate: { | ||
78 | durationValid: function (value) { | ||
79 | const res = customVideosValidators.isVideoDurationValid(value) | ||
80 | if (res === false) throw new Error('Video duration is not valid.') | ||
81 | } | ||
82 | } | ||
92 | } | 83 | } |
93 | ) | 84 | }, |
85 | { | ||
86 | indexes: [ | ||
87 | { | ||
88 | fields: [ 'authorId' ] | ||
89 | }, | ||
90 | { | ||
91 | fields: [ 'remoteId' ] | ||
92 | }, | ||
93 | { | ||
94 | fields: [ 'name' ] | ||
95 | }, | ||
96 | { | ||
97 | fields: [ 'createdAt' ] | ||
98 | }, | ||
99 | { | ||
100 | fields: [ 'duration' ] | ||
101 | }, | ||
102 | { | ||
103 | fields: [ 'infoHash' ] | ||
104 | } | ||
105 | ], | ||
106 | classMethods: { | ||
107 | associate, | ||
108 | |||
109 | generateThumbnailFromData, | ||
110 | getDurationFromFile, | ||
111 | list, | ||
112 | listForApi, | ||
113 | listOwnedAndPopulateAuthorAndTags, | ||
114 | listOwnedByAuthor, | ||
115 | load, | ||
116 | loadByHostAndRemoteId, | ||
117 | loadAndPopulateAuthor, | ||
118 | loadAndPopulateAuthorAndPodAndTags, | ||
119 | searchAndPopulateAuthorAndPodAndTags | ||
120 | }, | ||
121 | instanceMethods: { | ||
122 | generateMagnetUri, | ||
123 | getVideoFilename, | ||
124 | getThumbnailName, | ||
125 | getPreviewName, | ||
126 | getTorrentName, | ||
127 | isOwned, | ||
128 | toFormatedJSON, | ||
129 | toAddRemoteJSON, | ||
130 | toUpdateRemoteJSON | ||
131 | }, | ||
132 | hooks: { | ||
133 | beforeValidate, | ||
134 | beforeCreate, | ||
135 | afterDestroy | ||
136 | } | ||
137 | } | ||
138 | ) | ||
139 | |||
140 | return Video | ||
141 | } | ||
142 | |||
143 | function beforeValidate (video, options, next) { | ||
144 | // Put a fake infoHash if it does not exists yet | ||
145 | if (video.isOwned() && !video.infoHash) { | ||
146 | // 40 hexa length | ||
147 | video.infoHash = '0123456789abcdef0123456789abcdef01234567' | ||
94 | } | 148 | } |
95 | 149 | ||
96 | parallel(tasks, next) | 150 | return next(null) |
97 | }) | 151 | } |
98 | 152 | ||
99 | VideoSchema.pre('save', function (next) { | 153 | function beforeCreate (video, options, next) { |
100 | const video = this | ||
101 | const tasks = [] | 154 | const tasks = [] |
102 | 155 | ||
103 | if (video.isOwned()) { | 156 | if (video.isOwned()) { |
104 | const videoPath = pathUtils.join(constants.CONFIG.STORAGE.VIDEOS_DIR, video.getVideoFilename()) | 157 | const videoPath = pathUtils.join(constants.CONFIG.STORAGE.VIDEOS_DIR, video.getVideoFilename()) |
105 | this.podHost = constants.CONFIG.WEBSERVER.HOST | ||
106 | 158 | ||
107 | tasks.push( | 159 | tasks.push( |
108 | // TODO: refractoring | 160 | // TODO: refractoring |
@@ -123,9 +175,8 @@ VideoSchema.pre('save', function (next) { | |||
123 | if (err) return callback(err) | 175 | if (err) return callback(err) |
124 | 176 | ||
125 | const parsedTorrent = parseTorrent(torrent) | 177 | const parsedTorrent = parseTorrent(torrent) |
126 | video.magnet.infoHash = parsedTorrent.infoHash | 178 | video.set('infoHash', parsedTorrent.infoHash) |
127 | 179 | video.validate().asCallback(callback) | |
128 | callback(null) | ||
129 | }) | 180 | }) |
130 | }) | 181 | }) |
131 | }, | 182 | }, |
@@ -141,12 +192,72 @@ VideoSchema.pre('save', function (next) { | |||
141 | } | 192 | } |
142 | 193 | ||
143 | return next() | 194 | return next() |
144 | }) | 195 | } |
196 | |||
197 | function afterDestroy (video, options, next) { | ||
198 | const tasks = [] | ||
145 | 199 | ||
146 | mongoose.model('Video', VideoSchema) | 200 | tasks.push( |
201 | function (callback) { | ||
202 | removeThumbnail(video, callback) | ||
203 | } | ||
204 | ) | ||
205 | |||
206 | if (video.isOwned()) { | ||
207 | tasks.push( | ||
208 | function (callback) { | ||
209 | removeFile(video, callback) | ||
210 | }, | ||
211 | |||
212 | function (callback) { | ||
213 | removeTorrent(video, callback) | ||
214 | }, | ||
215 | |||
216 | function (callback) { | ||
217 | removePreview(video, callback) | ||
218 | }, | ||
219 | |||
220 | function (callback) { | ||
221 | const params = { | ||
222 | remoteId: video.id | ||
223 | } | ||
224 | |||
225 | friends.removeVideoToFriends(params) | ||
226 | |||
227 | return callback() | ||
228 | } | ||
229 | ) | ||
230 | } | ||
231 | |||
232 | parallel(tasks, next) | ||
233 | } | ||
147 | 234 | ||
148 | // ------------------------------ METHODS ------------------------------ | 235 | // ------------------------------ METHODS ------------------------------ |
149 | 236 | ||
237 | function associate (models) { | ||
238 | this.belongsTo(models.Author, { | ||
239 | foreignKey: { | ||
240 | name: 'authorId', | ||
241 | allowNull: false | ||
242 | }, | ||
243 | onDelete: 'cascade' | ||
244 | }) | ||
245 | |||
246 | this.belongsToMany(models.Tag, { | ||
247 | foreignKey: 'videoId', | ||
248 | through: models.VideoTag, | ||
249 | onDelete: 'cascade' | ||
250 | }) | ||
251 | |||
252 | this.hasMany(models.VideoAbuse, { | ||
253 | foreignKey: { | ||
254 | name: 'videoId', | ||
255 | allowNull: false | ||
256 | }, | ||
257 | onDelete: 'cascade' | ||
258 | }) | ||
259 | } | ||
260 | |||
150 | function generateMagnetUri () { | 261 | function generateMagnetUri () { |
151 | let baseUrlHttp, baseUrlWs | 262 | let baseUrlHttp, baseUrlWs |
152 | 263 | ||
@@ -154,8 +265,8 @@ function generateMagnetUri () { | |||
154 | baseUrlHttp = constants.CONFIG.WEBSERVER.URL | 265 | baseUrlHttp = constants.CONFIG.WEBSERVER.URL |
155 | baseUrlWs = constants.CONFIG.WEBSERVER.WS + '://' + constants.CONFIG.WEBSERVER.HOSTNAME + ':' + constants.CONFIG.WEBSERVER.PORT | 266 | baseUrlWs = constants.CONFIG.WEBSERVER.WS + '://' + constants.CONFIG.WEBSERVER.HOSTNAME + ':' + constants.CONFIG.WEBSERVER.PORT |
156 | } else { | 267 | } else { |
157 | baseUrlHttp = constants.REMOTE_SCHEME.HTTP + '://' + this.podHost | 268 | baseUrlHttp = constants.REMOTE_SCHEME.HTTP + '://' + this.Author.Pod.host |
158 | baseUrlWs = constants.REMOTE_SCHEME.WS + '://' + this.podHost | 269 | baseUrlWs = constants.REMOTE_SCHEME.WS + '://' + this.Author.Pod.host |
159 | } | 270 | } |
160 | 271 | ||
161 | const xs = baseUrlHttp + constants.STATIC_PATHS.TORRENTS + this.getTorrentName() | 272 | const xs = baseUrlHttp + constants.STATIC_PATHS.TORRENTS + this.getTorrentName() |
@@ -166,7 +277,7 @@ function generateMagnetUri () { | |||
166 | xs, | 277 | xs, |
167 | announce, | 278 | announce, |
168 | urlList, | 279 | urlList, |
169 | infoHash: this.magnet.infoHash, | 280 | infoHash: this.infoHash, |
170 | name: this.name | 281 | name: this.name |
171 | } | 282 | } |
172 | 283 | ||
@@ -174,20 +285,20 @@ function generateMagnetUri () { | |||
174 | } | 285 | } |
175 | 286 | ||
176 | function getVideoFilename () { | 287 | function getVideoFilename () { |
177 | if (this.isOwned()) return this._id + this.extname | 288 | if (this.isOwned()) return this.id + this.extname |
178 | 289 | ||
179 | return this.remoteId + this.extname | 290 | return this.remoteId + this.extname |
180 | } | 291 | } |
181 | 292 | ||
182 | function getThumbnailName () { | 293 | function getThumbnailName () { |
183 | // We always have a copy of the thumbnail | 294 | // We always have a copy of the thumbnail |
184 | return this._id + '.jpg' | 295 | return this.id + '.jpg' |
185 | } | 296 | } |
186 | 297 | ||
187 | function getPreviewName () { | 298 | function getPreviewName () { |
188 | const extension = '.jpg' | 299 | const extension = '.jpg' |
189 | 300 | ||
190 | if (this.isOwned()) return this._id + extension | 301 | if (this.isOwned()) return this.id + extension |
191 | 302 | ||
192 | return this.remoteId + extension | 303 | return this.remoteId + extension |
193 | } | 304 | } |
@@ -195,7 +306,7 @@ function getPreviewName () { | |||
195 | function getTorrentName () { | 306 | function getTorrentName () { |
196 | const extension = '.torrent' | 307 | const extension = '.torrent' |
197 | 308 | ||
198 | if (this.isOwned()) return this._id + extension | 309 | if (this.isOwned()) return this.id + extension |
199 | 310 | ||
200 | return this.remoteId + extension | 311 | return this.remoteId + extension |
201 | } | 312 | } |
@@ -205,27 +316,37 @@ function isOwned () { | |||
205 | } | 316 | } |
206 | 317 | ||
207 | function toFormatedJSON () { | 318 | function toFormatedJSON () { |
319 | let podHost | ||
320 | |||
321 | if (this.Author.Pod) { | ||
322 | podHost = this.Author.Pod.host | ||
323 | } else { | ||
324 | // It means it's our video | ||
325 | podHost = constants.CONFIG.WEBSERVER.HOST | ||
326 | } | ||
327 | |||
208 | const json = { | 328 | const json = { |
209 | id: this._id, | 329 | id: this.id, |
210 | name: this.name, | 330 | name: this.name, |
211 | description: this.description, | 331 | description: this.description, |
212 | podHost: this.podHost, | 332 | podHost, |
213 | isLocal: this.isOwned(), | 333 | isLocal: this.isOwned(), |
214 | magnetUri: this.generateMagnetUri(), | 334 | magnetUri: this.generateMagnetUri(), |
215 | author: this.author, | 335 | author: this.Author.name, |
216 | duration: this.duration, | 336 | duration: this.duration, |
217 | tags: this.tags, | 337 | tags: map(this.Tags, 'name'), |
218 | thumbnailPath: constants.STATIC_PATHS.THUMBNAILS + '/' + this.getThumbnailName(), | 338 | thumbnailPath: pathUtils.join(constants.STATIC_PATHS.THUMBNAILS, this.getThumbnailName()), |
219 | createdDate: this.createdDate | 339 | createdAt: this.createdAt, |
340 | updatedAt: this.updatedAt | ||
220 | } | 341 | } |
221 | 342 | ||
222 | return json | 343 | return json |
223 | } | 344 | } |
224 | 345 | ||
225 | function toRemoteJSON (callback) { | 346 | function toAddRemoteJSON (callback) { |
226 | const self = this | 347 | const self = this |
227 | 348 | ||
228 | // Convert thumbnail to base64 | 349 | // Get thumbnail data to send to the other pod |
229 | const thumbnailPath = pathUtils.join(constants.CONFIG.STORAGE.THUMBNAILS_DIR, this.getThumbnailName()) | 350 | const thumbnailPath = pathUtils.join(constants.CONFIG.STORAGE.THUMBNAILS_DIR, this.getThumbnailName()) |
230 | fs.readFile(thumbnailPath, function (err, thumbnailData) { | 351 | fs.readFile(thumbnailPath, function (err, thumbnailData) { |
231 | if (err) { | 352 | if (err) { |
@@ -236,13 +357,14 @@ function toRemoteJSON (callback) { | |||
236 | const remoteVideo = { | 357 | const remoteVideo = { |
237 | name: self.name, | 358 | name: self.name, |
238 | description: self.description, | 359 | description: self.description, |
239 | magnet: self.magnet, | 360 | infoHash: self.infoHash, |
240 | remoteId: self._id, | 361 | remoteId: self.id, |
241 | author: self.author, | 362 | author: self.Author.name, |
242 | duration: self.duration, | 363 | duration: self.duration, |
243 | thumbnailBase64: new Buffer(thumbnailData).toString('base64'), | 364 | thumbnailData: thumbnailData.toString('binary'), |
244 | tags: self.tags, | 365 | tags: map(self.Tags, 'name'), |
245 | createdDate: self.createdDate, | 366 | createdAt: self.createdAt, |
367 | updatedAt: self.updatedAt, | ||
246 | extname: self.extname | 368 | extname: self.extname |
247 | } | 369 | } |
248 | 370 | ||
@@ -250,14 +372,31 @@ function toRemoteJSON (callback) { | |||
250 | }) | 372 | }) |
251 | } | 373 | } |
252 | 374 | ||
375 | function toUpdateRemoteJSON (callback) { | ||
376 | const json = { | ||
377 | name: this.name, | ||
378 | description: this.description, | ||
379 | infoHash: this.infoHash, | ||
380 | remoteId: this.id, | ||
381 | author: this.Author.name, | ||
382 | duration: this.duration, | ||
383 | tags: map(this.Tags, 'name'), | ||
384 | createdAt: this.createdAt, | ||
385 | updatedAt: this.updatedAt, | ||
386 | extname: this.extname | ||
387 | } | ||
388 | |||
389 | return json | ||
390 | } | ||
391 | |||
253 | // ------------------------------ STATICS ------------------------------ | 392 | // ------------------------------ STATICS ------------------------------ |
254 | 393 | ||
255 | function generateThumbnailFromBase64 (video, thumbnailData, callback) { | 394 | function generateThumbnailFromData (video, thumbnailData, callback) { |
256 | // Creating the thumbnail for a remote video | 395 | // Creating the thumbnail for a remote video |
257 | 396 | ||
258 | const thumbnailName = video.getThumbnailName() | 397 | const thumbnailName = video.getThumbnailName() |
259 | const thumbnailPath = constants.CONFIG.STORAGE.THUMBNAILS_DIR + thumbnailName | 398 | const thumbnailPath = constants.CONFIG.STORAGE.THUMBNAILS_DIR + thumbnailName |
260 | fs.writeFile(thumbnailPath, thumbnailData, { encoding: 'base64' }, function (err) { | 399 | fs.writeFile(thumbnailPath, Buffer.from(thumbnailData, 'binary'), function (err) { |
261 | if (err) return callback(err) | 400 | if (err) return callback(err) |
262 | 401 | ||
263 | return callback(null, thumbnailName) | 402 | return callback(null, thumbnailName) |
@@ -272,51 +411,186 @@ function getDurationFromFile (videoPath, callback) { | |||
272 | }) | 411 | }) |
273 | } | 412 | } |
274 | 413 | ||
275 | function listForApi (start, count, sort, callback) { | 414 | function list (callback) { |
276 | const query = {} | 415 | return this.find().asCallback() |
277 | return modelUtils.listForApiWithCount.call(this, query, start, count, sort, callback) | ||
278 | } | 416 | } |
279 | 417 | ||
280 | function listByHostAndRemoteId (fromHost, remoteId, callback) { | 418 | function listForApi (start, count, sort, callback) { |
281 | this.find({ podHost: fromHost, remoteId: remoteId }, callback) | 419 | const query = { |
420 | offset: start, | ||
421 | limit: count, | ||
422 | distinct: true, // For the count, a video can have many tags | ||
423 | order: [ modelUtils.getSort(sort), [ this.sequelize.models.Tag, 'name', 'ASC' ] ], | ||
424 | include: [ | ||
425 | { | ||
426 | model: this.sequelize.models.Author, | ||
427 | include: [ { model: this.sequelize.models.Pod, required: false } ] | ||
428 | }, | ||
429 | |||
430 | this.sequelize.models.Tag | ||
431 | ] | ||
432 | } | ||
433 | |||
434 | return this.findAndCountAll(query).asCallback(function (err, result) { | ||
435 | if (err) return callback(err) | ||
436 | |||
437 | return callback(null, result.rows, result.count) | ||
438 | }) | ||
282 | } | 439 | } |
283 | 440 | ||
284 | function listByHost (fromHost, callback) { | 441 | function loadByHostAndRemoteId (fromHost, remoteId, callback) { |
285 | this.find({ podHost: fromHost }, callback) | 442 | const query = { |
443 | where: { | ||
444 | remoteId: remoteId | ||
445 | }, | ||
446 | include: [ | ||
447 | { | ||
448 | model: this.sequelize.models.Author, | ||
449 | include: [ | ||
450 | { | ||
451 | model: this.sequelize.models.Pod, | ||
452 | required: true, | ||
453 | where: { | ||
454 | host: fromHost | ||
455 | } | ||
456 | } | ||
457 | ] | ||
458 | } | ||
459 | ] | ||
460 | } | ||
461 | |||
462 | return this.findOne(query).asCallback(callback) | ||
286 | } | 463 | } |
287 | 464 | ||
288 | function listOwned (callback) { | 465 | function listOwnedAndPopulateAuthorAndTags (callback) { |
289 | // If remoteId is null this is *our* video | 466 | // If remoteId is null this is *our* video |
290 | this.find({ remoteId: null }, callback) | 467 | const query = { |
468 | where: { | ||
469 | remoteId: null | ||
470 | }, | ||
471 | include: [ this.sequelize.models.Author, this.sequelize.models.Tag ] | ||
472 | } | ||
473 | |||
474 | return this.findAll(query).asCallback(callback) | ||
291 | } | 475 | } |
292 | 476 | ||
293 | function listOwnedByAuthor (author, callback) { | 477 | function listOwnedByAuthor (author, callback) { |
294 | this.find({ remoteId: null, author: author }, callback) | 478 | const query = { |
295 | } | 479 | where: { |
480 | remoteId: null | ||
481 | }, | ||
482 | include: [ | ||
483 | { | ||
484 | model: this.sequelize.models.Author, | ||
485 | where: { | ||
486 | name: author | ||
487 | } | ||
488 | } | ||
489 | ] | ||
490 | } | ||
296 | 491 | ||
297 | function listRemotes (callback) { | 492 | return this.findAll(query).asCallback(callback) |
298 | this.find({ remoteId: { $ne: null } }, callback) | ||
299 | } | 493 | } |
300 | 494 | ||
301 | function load (id, callback) { | 495 | function load (id, callback) { |
302 | this.findById(id, callback) | 496 | return this.findById(id).asCallback(callback) |
497 | } | ||
498 | |||
499 | function loadAndPopulateAuthor (id, callback) { | ||
500 | const options = { | ||
501 | include: [ this.sequelize.models.Author ] | ||
502 | } | ||
503 | |||
504 | return this.findById(id, options).asCallback(callback) | ||
505 | } | ||
506 | |||
507 | function loadAndPopulateAuthorAndPodAndTags (id, callback) { | ||
508 | const options = { | ||
509 | include: [ | ||
510 | { | ||
511 | model: this.sequelize.models.Author, | ||
512 | include: [ { model: this.sequelize.models.Pod, required: false } ] | ||
513 | }, | ||
514 | this.sequelize.models.Tag | ||
515 | ] | ||
516 | } | ||
517 | |||
518 | return this.findById(id, options).asCallback(callback) | ||
303 | } | 519 | } |
304 | 520 | ||
305 | function search (value, field, start, count, sort, callback) { | 521 | function searchAndPopulateAuthorAndPodAndTags (value, field, start, count, sort, callback) { |
306 | const query = {} | 522 | const podInclude = { |
523 | model: this.sequelize.models.Pod, | ||
524 | required: false | ||
525 | } | ||
526 | |||
527 | const authorInclude = { | ||
528 | model: this.sequelize.models.Author, | ||
529 | include: [ | ||
530 | podInclude | ||
531 | ] | ||
532 | } | ||
533 | |||
534 | const tagInclude = { | ||
535 | model: this.sequelize.models.Tag | ||
536 | } | ||
537 | |||
538 | const query = { | ||
539 | where: {}, | ||
540 | offset: start, | ||
541 | limit: count, | ||
542 | distinct: true, // For the count, a video can have many tags | ||
543 | order: [ modelUtils.getSort(sort), [ this.sequelize.models.Tag, 'name', 'ASC' ] ] | ||
544 | } | ||
545 | |||
307 | // Make an exact search with the magnet | 546 | // Make an exact search with the magnet |
308 | if (field === 'magnetUri') { | 547 | if (field === 'magnetUri') { |
309 | const infoHash = magnetUtil.decode(value).infoHash | 548 | const infoHash = magnetUtil.decode(value).infoHash |
310 | query.magnet = { | 549 | query.where.infoHash = infoHash |
311 | infoHash | ||
312 | } | ||
313 | } else if (field === 'tags') { | 550 | } else if (field === 'tags') { |
314 | query[field] = value | 551 | const escapedValue = this.sequelize.escape('%' + value + '%') |
552 | query.where = { | ||
553 | id: { | ||
554 | $in: this.sequelize.literal( | ||
555 | '(SELECT "VideoTags"."videoId" FROM "Tags" INNER JOIN "VideoTags" ON "Tags"."id" = "VideoTags"."tagId" WHERE name LIKE ' + escapedValue + ')' | ||
556 | ) | ||
557 | } | ||
558 | } | ||
559 | } else if (field === 'host') { | ||
560 | // FIXME: Include our pod? (not stored in the database) | ||
561 | podInclude.where = { | ||
562 | host: { | ||
563 | $like: '%' + value + '%' | ||
564 | } | ||
565 | } | ||
566 | podInclude.required = true | ||
567 | } else if (field === 'author') { | ||
568 | authorInclude.where = { | ||
569 | name: { | ||
570 | $like: '%' + value + '%' | ||
571 | } | ||
572 | } | ||
573 | |||
574 | // authorInclude.or = true | ||
315 | } else { | 575 | } else { |
316 | query[field] = new RegExp(value, 'i') | 576 | query.where[field] = { |
577 | $like: '%' + value + '%' | ||
578 | } | ||
317 | } | 579 | } |
318 | 580 | ||
319 | modelUtils.listForApiWithCount.call(this, query, start, count, sort, callback) | 581 | query.include = [ |
582 | authorInclude, tagInclude | ||
583 | ] | ||
584 | |||
585 | if (tagInclude.where) { | ||
586 | // query.include.push([ this.sequelize.models.Tag ]) | ||
587 | } | ||
588 | |||
589 | return this.findAndCountAll(query).asCallback(function (err, result) { | ||
590 | if (err) return callback(err) | ||
591 | |||
592 | return callback(null, result.rows, result.count) | ||
593 | }) | ||
320 | } | 594 | } |
321 | 595 | ||
322 | // --------------------------------------------------------------------------- | 596 | // --------------------------------------------------------------------------- |