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