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