aboutsummaryrefslogblamecommitdiffhomepage
path: root/server/models/video.js
blob: 6cffa87afa206054c7d7d7f304fcd0ae44d0e13b (plain) (tree)
1
2
3
4
5
6
7
8
9

            
                                               

                                       
                                          
                                             
                                 
                                    


                                                      
                                                                             
                                           
                                     
 




                                                                              




                                           












                      





                                                                                        


                                                                                                                  
                                                                                                                     
  
                                                                          

                       


                 


                 


                       

                      
                       
            



                    
        


















                                           


                                      



       
                       






                                         
                                                                                              
                                                

               
                           
                           

                         
                                                                                                                                                       

                    
                                                                                                 



                                                                   

                                       
                                                                                                                


                                                       
                                                                                                      




                                                          

                           
                                                   

                           
                                                 


       
                         
          
                                                             






                                                                        











                                
                     
                               












                                                    
                                                                                









                                  

                                                                                                   








                                                            
                         





















                                                                        
                                                    
                  
                                                                                       

 

                                                              

 

                                          


                               

                                            

 
                                               
                                                         

 
                                 
                                                  











                                                              
                                         

   
                                                                                

 

                                                                              
                                            
                                                                                    


                                       
                                                                                

 
                                          
                                                                                     

 


                                          
                                                                                  

 

                                                                                  

 

                                                                                                               

 





                                                                                     

                                 










                                                                   
 









                              
      
                       
 
'use strict'

const createTorrent = require('create-torrent')
const ffmpeg = require('fluent-ffmpeg')
const fs = require('fs')
const parallel = require('async/parallel')
const parseTorrent = require('parse-torrent')
const pathUtils = require('path')
const magnet = require('magnet-uri')
const mongoose = require('mongoose')

const constants = require('../initializers/constants')
const customVideosValidators = require('../helpers/custom-validators').videos
const logger = require('../helpers/logger')
const modelUtils = require('./utils')

// ---------------------------------------------------------------------------

// TODO: add indexes on searchable columns
const VideoSchema = mongoose.Schema({
  name: String,
  extname: {
    type: String,
    enum: [ '.mp4', '.webm', '.ogv' ]
  },
  remoteId: mongoose.Schema.Types.ObjectId,
  description: String,
  magnetUri: String,
  podUrl: String,
  author: String,
  duration: Number,
  thumbnail: String,
  tags: [ String ],
  createdDate: {
    type: Date,
    default: Date.now
  }
})

VideoSchema.path('name').validate(customVideosValidators.isVideoNameValid)
VideoSchema.path('description').validate(customVideosValidators.isVideoDescriptionValid)
VideoSchema.path('magnetUri').validate(customVideosValidators.isVideoMagnetUriValid)
VideoSchema.path('podUrl').validate(customVideosValidators.isVideoPodUrlValid)
VideoSchema.path('author').validate(customVideosValidators.isVideoAuthorValid)
VideoSchema.path('duration').validate(customVideosValidators.isVideoDurationValid)
// The tumbnail can be the path or the data in base 64
// The pre save hook will convert the base 64 data in a file on disk and replace the thumbnail key by the filename
VideoSchema.path('thumbnail').validate(function (value) {
  return customVideosValidators.isVideoThumbnailValid(value) || customVideosValidators.isVideoThumbnail64Valid(value)
})
VideoSchema.path('tags').validate(customVideosValidators.isVideoTagsValid)

VideoSchema.methods = {
  getFilename,
  getJPEGName,
  getTorrentName,
  isOwned,
  toFormatedJSON,
  toRemoteJSON
}

VideoSchema.statics = {
  getDurationFromFile,
  listForApi,
  listByUrlAndRemoteId,
  listByUrl,
  listOwned,
  listOwnedByAuthor,
  listRemotes,
  load,
  search
}

VideoSchema.pre('remove', function (next) {
  const video = this
  const tasks = []

  tasks.push(
    function (callback) {
      removeThumbnail(video, callback)
    }
  )

  if (video.isOwned()) {
    tasks.push(
      function (callback) {
        removeFile(video, callback)
      },
      function (callback) {
        removeTorrent(video, callback)
      },
      function (callback) {
        removePreview(video, callback)
      }
    )
  }

  parallel(tasks, next)
})

VideoSchema.pre('save', function (next) {
  const video = this
  const tasks = []

  if (video.isOwned()) {
    const videoPath = pathUtils.join(constants.CONFIG.STORAGE.VIDEOS_DIR, video.getFilename())
    this.podUrl = constants.CONFIG.WEBSERVER.URL

    tasks.push(
      // TODO: refractoring
      function (callback) {
        const options = {
          announceList: [
            [ constants.CONFIG.WEBSERVER.WS + '://' + constants.CONFIG.WEBSERVER.HOSTNAME + ':' + constants.CONFIG.WEBSERVER.PORT + '/tracker/socket' ]
          ],
          urlList: [
            constants.CONFIG.WEBSERVER.URL + constants.STATIC_PATHS.WEBSEED + video.getFilename()
          ]
        }

        createTorrent(videoPath, options, function (err, torrent) {
          if (err) return callback(err)

          fs.writeFile(constants.CONFIG.STORAGE.TORRENTS_DIR + video.getTorrentName(), torrent, function (err) {
            if (err) return callback(err)

            const parsedTorrent = parseTorrent(torrent)
            parsedTorrent.xs = video.podUrl + constants.STATIC_PATHS.TORRENTS + video.getTorrentName()
            video.magnetUri = magnet.encode(parsedTorrent)

            callback(null)
          })
        })
      },
      function (callback) {
        createThumbnail(video, videoPath, callback)
      },
      function (callback) {
        createPreview(video, videoPath, callback)
      }
    )

    parallel(tasks, next)
  } else {
    generateThumbnailFromBase64(video, video.thumbnail, next)
  }
})

mongoose.model('Video', VideoSchema)

// ------------------------------ METHODS ------------------------------

function getFilename () {
  return this._id + this.extname
}

function getJPEGName () {
  return this._id + '.jpg'
}

function getTorrentName () {
  return this._id + '.torrent'
}

function isOwned () {
  return this.remoteId === null
}

function toFormatedJSON () {
  const json = {
    id: this._id,
    name: this.name,
    description: this.description,
    podUrl: this.podUrl.replace(/^https?:\/\//, ''),
    isLocal: this.isOwned(),
    magnetUri: this.magnetUri,
    author: this.author,
    duration: this.duration,
    tags: this.tags,
    thumbnailPath: constants.STATIC_PATHS.THUMBNAILS + '/' + this.getJPEGName(),
    createdDate: this.createdDate
  }

  return json
}

function toRemoteJSON (callback) {
  const self = this

  // Convert thumbnail to base64
  const thumbnailPath = pathUtils.join(constants.CONFIG.STORAGE.THUMBNAILS_DIR, this.getJPEGName())
  fs.readFile(thumbnailPath, function (err, thumbnailData) {
    if (err) {
      logger.error('Cannot read the thumbnail of the video')
      return callback(err)
    }

    const remoteVideo = {
      name: self.name,
      description: self.description,
      magnetUri: self.magnetUri,
      remoteId: self._id,
      author: self.author,
      duration: self.duration,
      thumbnailBase64: new Buffer(thumbnailData).toString('base64'),
      tags: self.tags,
      createdDate: self.createdDate,
      podUrl: self.podUrl
    }

    return callback(null, remoteVideo)
  })
}

// ------------------------------ STATICS ------------------------------

function getDurationFromFile (videoPath, callback) {
  ffmpeg.ffprobe(videoPath, function (err, metadata) {
    if (err) return callback(err)

    return callback(null, Math.floor(metadata.format.duration))
  })
}

function listForApi (start, count, sort, callback) {
  const query = {}
  return modelUtils.listForApiWithCount.call(this, query, start, count, sort, callback)
}

function listByUrlAndRemoteId (fromUrl, remoteId, callback) {
  this.find({ podUrl: fromUrl, remoteId: remoteId }, callback)
}

function listByUrl (fromUrl, callback) {
  this.find({ podUrl: fromUrl }, callback)
}

function listOwned (callback) {
  // If remoteId is null this is *our* video
  this.find({ remoteId: null }, callback)
}

function listOwnedByAuthor (author, callback) {
  this.find({ remoteId: null, author: author }, callback)
}

function listRemotes (callback) {
  this.find({ remoteId: { $ne: null } }, callback)
}

function load (id, callback) {
  this.findById(id, callback)
}

function search (value, field, start, count, sort, callback) {
  const query = {}
  // Make an exact search with the magnet
  if (field === 'magnetUri' || field === 'tags') {
    query[field] = value
  } else {
    query[field] = new RegExp(value, 'i')
  }

  modelUtils.listForApiWithCount.call(this, query, start, count, sort, callback)
}

// ---------------------------------------------------------------------------

function removeThumbnail (video, callback) {
  fs.unlink(constants.CONFIG.STORAGE.THUMBNAILS_DIR + video.getJPEGName(), callback)
}

function removeFile (video, callback) {
  fs.unlink(constants.CONFIG.STORAGE.VIDEOS_DIR + video.getFilename(), callback)
}

function removeTorrent (video, callback) {
  fs.unlink(constants.CONFIG.STORAGE.TORRENTS_DIR + video.getTorrentName(), callback)
}

function removePreview (video, callback) {
  // Same name than video thumnail
  // TODO: refractoring
  fs.unlink(constants.CONFIG.STORAGE.PREVIEWS_DIR + video.getJPEGName(), callback)
}

function createPreview (video, videoPath, callback) {
  generateImage(video, videoPath, constants.CONFIG.STORAGE.PREVIEWS_DIR, callback)
}

function createThumbnail (video, videoPath, callback) {
  generateImage(video, videoPath, constants.CONFIG.STORAGE.THUMBNAILS_DIR, constants.THUMBNAILS_SIZE, callback)
}

function generateThumbnailFromBase64 (video, thumbnailData, callback) {
  // Creating the thumbnail for this remote video)

  const thumbnailName = video.getJPEGName()
  const thumbnailPath = constants.CONFIG.STORAGE.THUMBNAILS_DIR + thumbnailName
  fs.writeFile(thumbnailPath, thumbnailData, { encoding: 'base64' }, function (err) {
    if (err) return callback(err)

    return callback(null, thumbnailName)
  })
}

function generateImage (video, videoPath, folder, size, callback) {
  const filename = video.getJPEGName()
  const options = {
    filename,
    count: 1,
    folder
  }

  if (!callback) {
    callback = size
  } else {
    options.size = size
  }

  ffmpeg(videoPath)
    .on('error', callback)
    .on('end', function () {
      callback(null, filename)
    })
    .thumbnail(options)
}