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