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