]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/models/video.js
Implement user API (create, update, remove, list)
[github/Chocobozzz/PeerTube.git] / server / models / video.js
CommitLineData
aaf61f38
C
1'use strict'
2
aaf61f38 3const config = require('config')
419633ce 4const eachLimit = require('async/eachLimit')
aaf61f38
C
5const ffmpeg = require('fluent-ffmpeg')
6const fs = require('fs')
1a42c9e2 7const parallel = require('async/parallel')
aaf61f38
C
8const pathUtils = require('path')
9const mongoose = require('mongoose')
10
11const constants = require('../initializers/constants')
e4c55619 12const customVideosValidators = require('../helpers/custom-validators').videos
aaf61f38
C
13const logger = require('../helpers/logger')
14const utils = require('../helpers/utils')
15const webtorrent = require('../lib/webtorrent')
16
17const http = config.get('webserver.https') === true ? 'https' : 'http'
18const host = config.get('webserver.host')
19const port = config.get('webserver.port')
20const uploadsDir = pathUtils.join(__dirname, '..', '..', config.get('storage.uploads'))
21const thumbnailsDir = pathUtils.join(__dirname, '..', '..', config.get('storage.thumbnails'))
22
23// ---------------------------------------------------------------------------
24
25// TODO: add indexes on searchable columns
26const VideoSchema = mongoose.Schema({
27 name: String,
5189d08a 28 filename: String,
aaf61f38
C
29 description: String,
30 magnetUri: String,
31 podUrl: String,
32 author: String,
33 duration: Number,
34 thumbnail: String,
35 tags: [ String ],
36 createdDate: {
37 type: Date,
38 default: Date.now
39 }
40})
41
e4c55619
C
42VideoSchema.path('name').validate(customVideosValidators.isVideoNameValid)
43VideoSchema.path('description').validate(customVideosValidators.isVideoDescriptionValid)
44VideoSchema.path('magnetUri').validate(customVideosValidators.isVideoMagnetUriValid)
45VideoSchema.path('podUrl').validate(customVideosValidators.isVideoPodUrlValid)
46VideoSchema.path('author').validate(customVideosValidators.isVideoAuthorValid)
47VideoSchema.path('duration').validate(customVideosValidators.isVideoDurationValid)
aaf61f38
C
48// The tumbnail can be the path or the data in base 64
49// The pre save hook will convert the base 64 data in a file on disk and replace the thumbnail key by the filename
50VideoSchema.path('thumbnail').validate(function (value) {
e4c55619 51 return customVideosValidators.isVideoThumbnailValid(value) || customVideosValidators.isVideoThumbnail64Valid(value)
aaf61f38 52})
e4c55619 53VideoSchema.path('tags').validate(customVideosValidators.isVideoTagsValid)
aaf61f38
C
54
55VideoSchema.methods = {
56 isOwned: isOwned,
57 toFormatedJSON: toFormatedJSON,
58 toRemoteJSON: toRemoteJSON
59}
60
61VideoSchema.statics = {
62 getDurationFromFile: getDurationFromFile,
63 list: list,
64 listByUrlAndMagnet: listByUrlAndMagnet,
65 listByUrls: listByUrls,
66 listOwned: listOwned,
9bd26629 67 listOwnedByAuthor: listOwnedByAuthor,
aaf61f38
C
68 listRemotes: listRemotes,
69 load: load,
70 search: search,
71 seedAllExisting: seedAllExisting
72}
73
74VideoSchema.pre('remove', function (next) {
75 const video = this
76 const tasks = []
77
78 tasks.push(
79 function (callback) {
80 removeThumbnail(video, callback)
81 }
82 )
83
84 if (video.isOwned()) {
85 tasks.push(
86 function (callback) {
87 removeFile(video, callback)
88 },
89 function (callback) {
90 removeTorrent(video, callback)
91 }
92 )
93 }
94
1a42c9e2 95 parallel(tasks, next)
aaf61f38
C
96})
97
98VideoSchema.pre('save', function (next) {
99 const video = this
100 const tasks = []
101
102 if (video.isOwned()) {
5189d08a 103 const videoPath = pathUtils.join(uploadsDir, video.filename)
aaf61f38
C
104 this.podUrl = http + '://' + host + ':' + port
105
106 tasks.push(
107 function (callback) {
108 seed(videoPath, callback)
109 },
110 function (callback) {
111 createThumbnail(videoPath, callback)
112 }
113 )
114
1a42c9e2 115 parallel(tasks, function (err, results) {
aaf61f38
C
116 if (err) return next(err)
117
118 video.magnetUri = results[0].magnetURI
119 video.thumbnail = results[1]
120
121 return next()
122 })
123 } else {
124 generateThumbnailFromBase64(video.thumbnail, function (err, thumbnailName) {
125 if (err) return next(err)
126
127 video.thumbnail = thumbnailName
128
129 return next()
130 })
131 }
132})
133
134mongoose.model('Video', VideoSchema)
135
136// ------------------------------ METHODS ------------------------------
137
138function isOwned () {
5189d08a 139 return this.filename !== null
aaf61f38
C
140}
141
142function toFormatedJSON () {
143 const json = {
144 id: this._id,
145 name: this.name,
146 description: this.description,
147 podUrl: this.podUrl.replace(/^https?:\/\//, ''),
148 isLocal: this.isOwned(),
149 magnetUri: this.magnetUri,
150 author: this.author,
151 duration: this.duration,
152 tags: this.tags,
153 thumbnailPath: constants.THUMBNAILS_STATIC_PATH + '/' + this.thumbnail,
154 createdDate: this.createdDate
155 }
156
157 return json
158}
159
160function toRemoteJSON (callback) {
161 const self = this
162
163 // Convert thumbnail to base64
164 fs.readFile(pathUtils.join(thumbnailsDir, this.thumbnail), function (err, thumbnailData) {
165 if (err) {
166 logger.error('Cannot read the thumbnail of the video')
167 return callback(err)
168 }
169
170 const remoteVideo = {
171 name: self.name,
172 description: self.description,
173 magnetUri: self.magnetUri,
5189d08a 174 filename: null,
aaf61f38
C
175 author: self.author,
176 duration: self.duration,
177 thumbnailBase64: new Buffer(thumbnailData).toString('base64'),
178 tags: self.tags,
179 createdDate: self.createdDate,
180 podUrl: self.podUrl
181 }
182
183 return callback(null, remoteVideo)
184 })
185}
186
187// ------------------------------ STATICS ------------------------------
188
189function getDurationFromFile (videoPath, callback) {
190 ffmpeg.ffprobe(videoPath, function (err, metadata) {
191 if (err) return callback(err)
192
193 return callback(null, Math.floor(metadata.format.duration))
194 })
195}
196
197function list (start, count, sort, callback) {
198 const query = {}
199 return findWithCount.call(this, query, start, count, sort, callback)
200}
201
202function listByUrlAndMagnet (fromUrl, magnetUri, callback) {
203 this.find({ podUrl: fromUrl, magnetUri: magnetUri }, callback)
204}
205
206function listByUrls (fromUrls, callback) {
207 this.find({ podUrl: { $in: fromUrls } }, callback)
208}
209
210function listOwned (callback) {
5189d08a
C
211 // If filename is not null this is *our* video
212 this.find({ filename: { $ne: null } }, callback)
aaf61f38
C
213}
214
9bd26629
C
215function listOwnedByAuthor (author, callback) {
216 this.find({ filename: { $ne: null }, author: author }, callback)
217}
218
aaf61f38 219function listRemotes (callback) {
5189d08a 220 this.find({ filename: null }, callback)
aaf61f38
C
221}
222
223function load (id, callback) {
224 this.findById(id, callback)
225}
226
227function search (value, field, start, count, sort, callback) {
228 const query = {}
229 // Make an exact search with the magnet
230 if (field === 'magnetUri' || field === 'tags') {
231 query[field] = value
232 } else {
233 query[field] = new RegExp(value)
234 }
235
236 findWithCount.call(this, query, start, count, sort, callback)
237}
238
907e9510
C
239function seedAllExisting (callback) {
240 listOwned.call(this, function (err, videos) {
241 if (err) return callback(err)
aaf61f38 242
419633ce 243 eachLimit(videos, constants.SEEDS_IN_PARALLEL, function (video, callbackEach) {
5189d08a 244 const videoPath = pathUtils.join(uploadsDir, video.filename)
907e9510
C
245 seed(videoPath, callbackEach)
246 }, callback)
247 })
aaf61f38
C
248}
249
250// ---------------------------------------------------------------------------
251
252function findWithCount (query, start, count, sort, callback) {
253 const self = this
254
1a42c9e2 255 parallel([
aaf61f38 256 function (asyncCallback) {
4fea95df 257 self.find(query).skip(start).limit(count).sort(sort).exec(asyncCallback)
aaf61f38
C
258 },
259 function (asyncCallback) {
260 self.count(query, asyncCallback)
261 }
262 ], function (err, results) {
263 if (err) return callback(err)
264
265 const videos = results[0]
266 const totalVideos = results[1]
267 return callback(null, videos, totalVideos)
268 })
269}
270
271function removeThumbnail (video, callback) {
272 fs.unlink(thumbnailsDir + video.thumbnail, callback)
273}
274
275function removeFile (video, callback) {
5189d08a 276 fs.unlink(uploadsDir + video.filename, callback)
aaf61f38
C
277}
278
279// Maybe the torrent is not seeded, but we catch the error to don't stop the removing process
280function removeTorrent (video, callback) {
281 try {
282 webtorrent.remove(video.magnetUri, callback)
283 } catch (err) {
284 logger.warn('Cannot remove the torrent from WebTorrent', { err: err })
285 return callback(null)
286 }
287}
288
289function createThumbnail (videoPath, callback) {
290 const filename = pathUtils.basename(videoPath) + '.jpg'
291 ffmpeg(videoPath)
292 .on('error', callback)
293 .on('end', function () {
294 callback(null, filename)
295 })
296 .thumbnail({
297 count: 1,
298 folder: thumbnailsDir,
299 size: constants.THUMBNAILS_SIZE,
300 filename: filename
301 })
302}
303
304function seed (path, callback) {
305 logger.info('Seeding %s...', path)
306
307 webtorrent.seed(path, function (torrent) {
308 logger.info('%s seeded (%s).', path, torrent.magnetURI)
309
310 return callback(null, torrent)
311 })
312}
313
314function generateThumbnailFromBase64 (data, callback) {
315 // Creating the thumbnail for this remote video
316 utils.generateRandomString(16, function (err, randomString) {
317 if (err) return callback(err)
318
319 const thumbnailName = randomString + '.jpg'
320 fs.writeFile(thumbnailsDir + thumbnailName, data, { encoding: 'base64' }, function (err) {
321 if (err) return callback(err)
322
323 return callback(null, thumbnailName)
324 })
325 })
326}