/*.sublime-workspace
/dist
/.idea
+/PeerTube.iml
-import { Video as VideoServerModel } from '../../../../../shared'
+import { Video as VideoServerModel, VideoFile } from '../../../../../shared'
import { User } from '../../shared'
export class Video implements VideoServerModel {
id: number
uuid: string
isLocal: boolean
- magnetUri: string
name: string
podHost: string
tags: string[]
likes: number
dislikes: number
nsfw: boolean
+ files: VideoFile[]
private static createByString (author: string, podHost: string) {
return author + '@' + podHost
id: number,
uuid: string,
isLocal: boolean,
- magnetUri: string,
name: string,
podHost: string,
tags: string[],
views: number,
likes: number,
dislikes: number,
- nsfw: boolean
+ nsfw: boolean,
+ files: VideoFile[]
}) {
this.author = hash.author
this.createdAt = new Date(hash.createdAt)
this.id = hash.id
this.uuid = hash.uuid
this.isLocal = hash.isLocal
- this.magnetUri = hash.magnetUri
this.name = hash.name
this.podHost = hash.podHost
this.tags = hash.tags
this.likes = hash.likes
this.dislikes = hash.dislikes
this.nsfw = hash.nsfw
+ this.files = hash.files
this.by = Video.createByString(hash.author, hash.podHost)
}
return (this.nsfw && (!user || user.displayNSFW === false))
}
+ getDefaultMagnetUri () {
+ if (this.files === undefined || this.files.length === 0) return ''
+
+ // TODO: choose the original file
+ return this.files[0].magnetUri
+ }
+
patch (values: Object) {
Object.keys(values).forEach((key) => {
this[key] = values[key]
duration: this.duration,
id: this.id,
isLocal: this.isLocal,
- magnetUri: this.magnetUri,
name: this.name,
podHost: this.podHost,
tags: this.tags,
views: this.views,
likes: this.likes,
dislikes: this.dislikes,
- nsfw: this.nsfw
+ nsfw: this.nsfw,
+ files: this.files
}
}
}
</div>
<div class="modal-body">
- <input #magnetUriInput (click)="magnetUriInput.select()" type="text" class="form-control input-sm readonly" readonly [value]="video.magnetUri" />
+ <input #magnetUriInput (click)="magnetUriInput.select()" type="text" class="form-control input-sm readonly" readonly [value]="video.getDefaultMagnetUri()" />
</div>
</div>
</div>
window.clearInterval(this.torrentInfosInterval)
window.clearTimeout(this.errorTimer)
- if (this.video !== null && this.webTorrentService.has(this.video.magnetUri)) {
- this.webTorrentService.remove(this.video.magnetUri)
+ if (this.video !== null && this.webTorrentService.has(this.video.getDefaultMagnetUri())) {
+ this.webTorrentService.remove(this.video.getDefaultMagnetUri())
}
// Remove player
// We are loading the video
this.loading = true
- console.log('Adding ' + this.video.magnetUri + '.')
+ console.log('Adding ' + this.video.getDefaultMagnetUri() + '.')
// The callback might never return if there are network issues
// So we create a timer to inform the user the load is abnormally long
this.errorTimer = window.setTimeout(() => this.loadTooLong(), VideoWatchComponent.LOADTIME_TOO_LONG)
- const torrent = this.webTorrentService.add(this.video.magnetUri, torrent => {
+ const torrent = this.webTorrentService.add(this.video.getDefaultMagnetUri(), torrent => {
// Clear the error timer
window.clearTimeout(this.errorTimer)
// Maybe the error was fired by the timer, so reset it
// We are not loading the video anymore
this.loading = false
- console.log('Added ' + this.video.magnetUri + '.')
+ console.log('Added ' + this.video.getDefaultMagnetUri() + '.')
torrent.files[0].renderTo(this.playerElement, (err) => {
if (err) {
this.notificationsService.error('Error', 'Cannot append the file in the video element.')
return
}
- const magnetUri = videoInfos.magnetUri
+ let magnetUri = ''
+ if (videoInfos.files !== undefined && videoInfos.files.length !== 0) {
+ magnetUri = videoInfos.files[0].magnetUri
+ }
+
const videoContainer = document.getElementById('video-container') as HTMLVideoElement
const previewUrl = window.location.origin + videoInfos.previewPath
videoContainer.poster = previewUrl
"danger:clean:modules": "scripty",
"reset-password": "ts-node ./scripts/reset-password.ts",
"play": "scripty",
+ "dev": "scripty",
"dev:server": "scripty",
"dev:client": "scripty",
"start": "node dist/server",
--- /dev/null
+#!/usr/bin/env sh
+
+NODE_ENV=test concurrently -k \
+ "npm run watch:client" \
+ "npm run watch:server"
import { readFileSync, writeFileSync } from 'fs'
+import { join } from 'path'
import * as parseTorrent from 'parse-torrent'
import { CONFIG, STATIC_PATHS } from '../server/initializers/constants'
return db.Video.list()
})
.then(videos => {
- videos.forEach(function (video) {
- const torrentName = video.id + '.torrent'
- const torrentPath = CONFIG.STORAGE.TORRENTS_DIR + torrentName
- const filename = video.id + video.extname
-
- const parsed = parseTorrent(readFileSync(torrentPath))
- parsed.announce = [ CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOST + '/tracker/socket' ]
- parsed.urlList = [ CONFIG.WEBSERVER.URL + STATIC_PATHS.WEBSEED + filename ]
-
- const buf = parseTorrent.toTorrentFile(parsed)
- writeFileSync(torrentPath, buf)
+ videos.forEach(video => {
+ video.VideoFiles.forEach(file => {
+ video.createTorrentAndSetInfoHash(file)
+ })
})
process.exit(0)
// ----------- Database -----------
// Do not use barrels because we don't want to load all modules here (we need to initialize database first)
import { logger } from './server/helpers/logger'
-import { API_VERSION, CONFIG } from './server/initializers/constants'
+import { API_VERSION, CONFIG, STATIC_PATHS } from './server/initializers/constants'
// Initialize database and models
import { database as db } from './server/initializers/database'
db.init(false).then(() => onDatabaseInitDone())
// Enable CORS for develop
if (isTestInstance()) {
- app.use(cors({
- origin: 'http://localhost:3000',
- credentials: true
- }))
+ app.use((req, res, next) => {
+ // These routes have already cors
+ if (
+ req.path.indexOf(STATIC_PATHS.TORRENTS) === -1 &&
+ req.path.indexOf(STATIC_PATHS.WEBSEED) === -1
+ ) {
+ return (cors({
+ origin: 'http://localhost:3000',
+ credentials: true
+ }))(req, res, next)
+ }
+
+ return next()
+ })
}
// For the logger
const videoData = {
name: videoToCreateData.name,
uuid: videoToCreateData.uuid,
- extname: videoToCreateData.extname,
- infoHash: videoToCreateData.infoHash,
category: videoToCreateData.category,
licence: videoToCreateData.licence,
language: videoToCreateData.language,
return video.save(options).then(videoCreated => ({ tagInstances, videoCreated }))
})
+ .then(({ tagInstances, videoCreated }) => {
+ const tasks = []
+ const options = {
+ transaction: t
+ }
+
+ videoToCreateData.files.forEach(fileData => {
+ const videoFileInstance = db.VideoFile.build({
+ extname: fileData.extname,
+ infoHash: fileData.infoHash,
+ resolution: fileData.resolution,
+ size: fileData.size,
+ videoId: videoCreated.id
+ })
+
+ tasks.push(videoFileInstance.save(options))
+ })
+
+ return Promise.all(tasks).then(() => ({ tagInstances, videoCreated }))
+ })
.then(({ tagInstances, videoCreated }) => {
const options = {
transaction: t
return videoInstance.save(options).then(() => ({ videoInstance, tagInstances }))
})
+ .then(({ tagInstances, videoInstance }) => {
+ const tasks = []
+ const options = {
+ transaction: t
+ }
+
+ videoAttributesToUpdate.files.forEach(fileData => {
+ const videoFileInstance = db.VideoFile.build({
+ extname: fileData.extname,
+ infoHash: fileData.infoHash,
+ resolution: fileData.resolution,
+ size: fileData.size,
+ videoId: videoInstance.id
+ })
+
+ tasks.push(videoFileInstance.save(options))
+ })
+
+ return Promise.all(tasks).then(() => ({ tagInstances, videoInstance }))
+ })
.then(({ videoInstance, tagInstances }) => {
const options = { transaction: t }
import * as express from 'express'
import * as Promise from 'bluebird'
import * as multer from 'multer'
-import * as path from 'path'
+import { extname, join } from 'path'
import { database as db } from '../../../initializers/database'
import {
addEventToRemoteVideo,
quickAndDirtyUpdateVideoToFriends,
addVideoToFriends,
- updateVideoToFriends
+ updateVideoToFriends,
+ JobScheduler
} from '../../../lib'
import {
authenticate,
.catch(err => next(err))
}
-function addVideo (req: express.Request, res: express.Response, videoFile: Express.Multer.File) {
+function addVideo (req: express.Request, res: express.Response, videoPhysicalFile: Express.Multer.File) {
const videoInfos: VideoCreate = req.body
return db.sequelize.transaction(t => {
const videoData = {
name: videoInfos.name,
remote: false,
- extname: path.extname(videoFile.filename),
+ extname: extname(videoPhysicalFile.filename),
category: videoInfos.category,
licence: videoInfos.licence,
language: videoInfos.language,
nsfw: videoInfos.nsfw,
description: videoInfos.description,
- duration: videoFile['duration'], // duration was added by a previous middleware
+ duration: videoPhysicalFile['duration'], // duration was added by a previous middleware
authorId: author.id
}
return { author, tagInstances, video }
})
.then(({ author, tagInstances, video }) => {
+ const videoFileData = {
+ extname: extname(videoPhysicalFile.filename),
+ resolution: 0, // TODO: improve readability,
+ size: videoPhysicalFile.size
+ }
+
+ const videoFile = db.VideoFile.build(videoFileData)
+ return { author, tagInstances, video, videoFile }
+ })
+ .then(({ author, tagInstances, video, videoFile }) => {
const videoDir = CONFIG.STORAGE.VIDEOS_DIR
- const source = path.join(videoDir, videoFile.filename)
- const destination = path.join(videoDir, video.getVideoFilename())
+ const source = join(videoDir, videoPhysicalFile.filename)
+ const destination = join(videoDir, video.getVideoFilename(videoFile))
return renamePromise(source, destination)
.then(() => {
// This is important in case if there is another attempt in the retry process
- videoFile.filename = video.getVideoFilename()
- return { author, tagInstances, video }
+ videoPhysicalFile.filename = video.getVideoFilename(videoFile)
+ return { author, tagInstances, video, videoFile }
})
})
- .then(({ author, tagInstances, video }) => {
+ .then(({ author, tagInstances, video, videoFile }) => {
+ const tasks = []
+
+ tasks.push(
+ video.createTorrentAndSetInfoHash(videoFile),
+ video.createThumbnail(videoFile),
+ video.createPreview(videoFile)
+ )
+
+ if (CONFIG.TRANSCODING.ENABLED === true) {
+ // Put uuid because we don't have id auto incremented for now
+ const dataInput = {
+ videoUUID: video.uuid
+ }
+
+ tasks.push(
+ JobScheduler.Instance.createJob(t, 'videoTranscoder', dataInput)
+ )
+ }
+
+ return Promise.all(tasks).then(() => ({ author, tagInstances, video, videoFile }))
+ })
+ .then(({ author, tagInstances, video, videoFile }) => {
const options = { transaction: t }
return video.save(options)
// Do not forget to add Author informations to the created video
videoCreated.Author = author
- return { tagInstances, video: videoCreated }
+ return { tagInstances, video: videoCreated, videoFile }
})
})
+ .then(({ tagInstances, video, videoFile }) => {
+ const options = { transaction: t }
+ videoFile.videoId = video.id
+
+ return videoFile.save(options)
+ .then(() => video.VideoFiles = [ videoFile ])
+ .then(() => ({ tagInstances, video }))
+ })
.then(({ tagInstances, video }) => {
if (!tagInstances) return video
})
.then(() => logger.info('Video with name %s created.', videoInfos.name))
.catch((err: Error) => {
- logger.debug('Cannot insert the video.', { error: err.stack })
+ logger.debug('Cannot insert the video.', err)
throw err
})
}
isVideoNSFWValid,
isVideoDescriptionValid,
isVideoDurationValid,
- isVideoInfoHashValid,
+ isVideoFileInfoHashValid,
isVideoNameValid,
isVideoTagsValid,
- isVideoExtnameValid
+ isVideoFileExtnameValid,
+ isVideoFileResolutionValid
} from '../videos'
const ENDPOINT_ACTIONS = REQUEST_ENDPOINT_ACTIONS[REQUEST_ENDPOINTS.VIDEOS]
isVideoNSFWValid(video.nsfw) &&
isVideoDescriptionValid(video.description) &&
isVideoDurationValid(video.duration) &&
- isVideoInfoHashValid(video.infoHash) &&
isVideoNameValid(video.name) &&
isVideoTagsValid(video.tags) &&
isVideoUUIDValid(video.uuid) &&
- isVideoExtnameValid(video.extname) &&
isVideoViewsValid(video.views) &&
isVideoLikesValid(video.likes) &&
- isVideoDislikesValid(video.dislikes)
+ isVideoDislikesValid(video.dislikes) &&
+ isArray(video.files) &&
+ video.files.every(videoFile => {
+ if (!videoFile) return false
+
+ return (
+ isVideoFileInfoHashValid(videoFile.infoHash) &&
+ isVideoFileExtnameValid(videoFile.extname) &&
+ isVideoFileResolutionValid(videoFile.resolution)
+ )
+ })
}
function isRequestTypeAddValid (value: string) {
VIDEO_CATEGORIES,
VIDEO_LICENCES,
VIDEO_LANGUAGES,
- VIDEO_RATE_TYPES
+ VIDEO_RATE_TYPES,
+ VIDEO_FILE_RESOLUTIONS
} from '../../initializers'
import { isUserUsernameValid } from './users'
import { isArray, exists } from './misc'
return exists(value) && validator.isInt(value + '', VIDEOS_CONSTRAINTS_FIELDS.DURATION)
}
-function isVideoExtnameValid (value: string) {
- return VIDEOS_CONSTRAINTS_FIELDS.EXTNAME.indexOf(value) !== -1
-}
-
-function isVideoInfoHashValid (value: string) {
- return exists(value) && validator.isLength(value, VIDEOS_CONSTRAINTS_FIELDS.INFO_HASH)
-}
-
function isVideoNameValid (value: string) {
return exists(value) && validator.isLength(value, VIDEOS_CONSTRAINTS_FIELDS.NAME)
}
return new RegExp('^video/(webm|mp4|ogg)$', 'i').test(file.mimetype)
}
+function isVideoFileSizeValid (value: string) {
+ return exists(value) && validator.isInt(value + '', VIDEOS_CONSTRAINTS_FIELDS.FILE_SIZE)
+}
+
+function isVideoFileResolutionValid (value: string) {
+ return VIDEO_FILE_RESOLUTIONS[value] !== undefined
+}
+
+function isVideoFileExtnameValid (value: string) {
+ return VIDEOS_CONSTRAINTS_FIELDS.EXTNAME.indexOf(value) !== -1
+}
+
+function isVideoFileInfoHashValid (value: string) {
+ return exists(value) && validator.isLength(value, VIDEOS_CONSTRAINTS_FIELDS.INFO_HASH)
+}
+
// ---------------------------------------------------------------------------
export {
isVideoNSFWValid,
isVideoDescriptionValid,
isVideoDurationValid,
- isVideoInfoHashValid,
+ isVideoFileInfoHashValid,
isVideoNameValid,
isVideoTagsValid,
isVideoThumbnailValid,
isVideoThumbnailDataValid,
- isVideoExtnameValid,
+ isVideoFileExtnameValid,
isVideoUUIDValid,
isVideoAbuseReasonValid,
isVideoAbuseReporterUsernameValid,
isVideoLikesValid,
isVideoRatingTypeValid,
isVideoDislikesValid,
- isVideoEventCountValid
+ isVideoEventCountValid,
+ isVideoFileSizeValid,
+ isVideoFileResolutionValid
}
declare global {
isVideoLikesValid,
isVideoRatingTypeValid,
isVideoDislikesValid,
- isVideoEventCountValid
+ isVideoEventCountValid,
+ isVideoFileSizeValid,
+ isVideoFileResolutionValid
}
}
}
// ---------------------------------------------------------------------------
-const LAST_MIGRATION_VERSION = 55
+const LAST_MIGRATION_VERSION = 65
// ---------------------------------------------------------------------------
THUMBNAIL_DATA: { min: 0, max: 20000 }, // Bytes
VIEWS: { min: 0 },
LIKES: { min: 0 },
- DISLIKES: { min: 0 }
+ DISLIKES: { min: 0 },
+ FILE_SIZE: { min: 10, max: 1024 * 1024 * 1024 * 3 /* 3Go */ }
},
VIDEO_EVENTS: {
COUNT: { min: 0 }
14: 'Italien'
}
+const VIDEO_FILE_RESOLUTIONS = {
+ 0: 'original',
+ 1: '360p',
+ 2: '480p',
+ 3: '720p',
+ 4: '1080p'
+}
+
// ---------------------------------------------------------------------------
// Score a pod has when we create it as a friend
THUMBNAILS_SIZE,
USER_ROLES,
VIDEO_CATEGORIES,
+ VIDEO_FILE_RESOLUTIONS,
VIDEO_LANGUAGES,
VIDEO_LICENCES,
VIDEO_RATE_TYPES
UserVideoRateModel,
VideoAbuseModel,
BlacklistedVideoModel,
+ VideoFileModel,
VideoTagModel,
VideoModel
} from '../models'
UserVideoRate?: UserVideoRateModel,
User?: UserModel,
VideoAbuse?: VideoAbuseModel,
+ VideoFile?: VideoFileModel,
BlacklistedVideo?: BlacklistedVideoModel,
VideoTag?: VideoTagModel,
Video?: VideoModel
--- /dev/null
+import * as Sequelize from 'sequelize'
+import * as Promise from 'bluebird'
+
+function up (utils: {
+ transaction: Sequelize.Transaction,
+ queryInterface: Sequelize.QueryInterface,
+ sequelize: Sequelize.Sequelize,
+ db: any
+}): Promise<void> {
+ const q = utils.queryInterface
+
+ const query = 'INSERT INTO "VideoFiles" ("videoId", "resolution", "size", "extname", "infoHash", "createdAt", "updatedAt") ' +
+ 'SELECT "id" AS "videoId", 0 AS "resolution", 0 AS "size", ' +
+ '"extname"::"text"::"enum_VideoFiles_extname" as "extname", "infoHash", "createdAt", "updatedAt" ' +
+ 'FROM "Videos"'
+
+ return utils.db.VideoFile.sync()
+ .then(() => utils.sequelize.query(query))
+ .then(() => {
+ return q.removeColumn('Videos', 'extname')
+ })
+ .then(() => {
+ return q.removeColumn('Videos', 'infoHash')
+ })
+}
+
+function down (options) {
+ throw new Error('Not implemented.')
+}
+
+export {
+ up,
+ down
+}
--- /dev/null
+import * as Sequelize from 'sequelize'
+import * as Promise from 'bluebird'
+import { stat } from 'fs'
+
+import { VideoInstance } from '../../models'
+
+function up (utils: {
+ transaction: Sequelize.Transaction,
+ queryInterface: Sequelize.QueryInterface,
+ sequelize: Sequelize.Sequelize,
+ db: any
+}): Promise<void> {
+ return utils.db.Video.listOwnedAndPopulateAuthorAndTags()
+ .then((videos: VideoInstance[]) => {
+ const tasks: Promise<any>[] = []
+
+ videos.forEach(video => {
+ video.VideoFiles.forEach(videoFile => {
+ const p = new Promise((res, rej) => {
+ stat(video.getVideoFilePath(videoFile), (err, stats) => {
+ if (err) return rej(err)
+
+ videoFile.size = stats.size
+ videoFile.save().then(res).catch(rej)
+ })
+ })
+
+ tasks.push(p)
+ })
+ })
+
+ return tasks
+ })
+ .then((tasks: Promise<any>[]) => {
+ return Promise.all(tasks)
+ })
+}
+
+function down (options) {
+ throw new Error('Not implemented.')
+}
+
+export {
+ up,
+ down
+}
script: string
}[] = []
- files.forEach(file => {
- // Filename is something like 'version-blabla.js'
- const version = file.split('-')[0]
- filesToMigrate.push({
- version,
- script: file
+ files
+ .filter(file => file.endsWith('.js.map') === false)
+ .forEach(file => {
+ // Filename is something like 'version-blabla.js'
+ const version = file.split('-')[0]
+ filesToMigrate.push({
+ version,
+ script: file
+ })
})
- })
return filesToMigrate
})
const options = {
transaction: t,
queryInterface: db.sequelize.getQueryInterface(),
- sequelize: db.sequelize
+ sequelize: db.sequelize,
+ db
}
return migrationScript.up(options)
function process (data: { videoUUID: string }) {
return db.Video.loadByUUIDAndPopulateAuthorAndPodAndTags(data.videoUUID).then(video => {
- return video.transcodeVideofile().then(() => video)
+ // TODO: handle multiple resolutions
+ const videoFile = video.VideoFiles[0]
+ return video.transcodeVideofile(videoFile).then(() => video)
})
}
export * from './video-abuse-interface'
export * from './video-blacklist-interface'
export * from './video-tag-interface'
+export * from './video-file-interface'
export * from './video-interface'
--- /dev/null
+import * as Sequelize from 'sequelize'
+
+export namespace VideoFileMethods {
+}
+
+export interface VideoFileClass {
+}
+
+export interface VideoFileAttributes {
+ resolution: number
+ size: number
+ infoHash?: string
+ extname: string
+
+ videoId?: number
+}
+
+export interface VideoFileInstance extends VideoFileClass, VideoFileAttributes, Sequelize.Instance<VideoFileAttributes> {
+ id: number
+ createdAt: Date
+ updatedAt: Date
+}
+
+export interface VideoFileModel extends VideoFileClass, Sequelize.Model<VideoFileInstance, VideoFileAttributes> {}
--- /dev/null
+import * as Sequelize from 'sequelize'
+import { values } from 'lodash'
+
+import { CONSTRAINTS_FIELDS } from '../../initializers'
+import {
+ isVideoFileResolutionValid,
+ isVideoFileSizeValid,
+ isVideoFileInfoHashValid
+} from '../../helpers'
+
+import { addMethodsToModel } from '../utils'
+import {
+ VideoFileInstance,
+ VideoFileAttributes
+} from './video-file-interface'
+
+let VideoFile: Sequelize.Model<VideoFileInstance, VideoFileAttributes>
+
+export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) {
+ VideoFile = sequelize.define<VideoFileInstance, VideoFileAttributes>('VideoFile',
+ {
+ resolution: {
+ type: DataTypes.INTEGER,
+ allowNull: false,
+ validate: {
+ resolutionValid: value => {
+ const res = isVideoFileResolutionValid(value)
+ if (res === false) throw new Error('Video file resolution is not valid.')
+ }
+ }
+ },
+ size: {
+ type: DataTypes.INTEGER,
+ allowNull: false,
+ validate: {
+ sizeValid: value => {
+ const res = isVideoFileSizeValid(value)
+ if (res === false) throw new Error('Video file size is not valid.')
+ }
+ }
+ },
+ extname: {
+ type: DataTypes.ENUM(values(CONSTRAINTS_FIELDS.VIDEOS.EXTNAME)),
+ allowNull: false
+ },
+ infoHash: {
+ type: DataTypes.STRING,
+ allowNull: false,
+ validate: {
+ infoHashValid: value => {
+ const res = isVideoFileInfoHashValid(value)
+ if (res === false) throw new Error('Video file info hash is not valid.')
+ }
+ }
+ }
+ },
+ {
+ indexes: [
+ {
+ fields: [ 'videoId' ]
+ },
+ {
+ fields: [ 'infoHash' ]
+ }
+ ]
+ }
+ )
+
+ const classMethods = [
+ associate
+ ]
+ addMethodsToModel(VideoFile, classMethods)
+
+ return VideoFile
+}
+
+// ------------------------------ STATICS ------------------------------
+
+function associate (models) {
+ VideoFile.belongsTo(models.Video, {
+ foreignKey: {
+ name: 'videoId',
+ allowNull: false
+ },
+ onDelete: 'CASCADE'
+ })
+}
+
+// ------------------------------ METHODS ------------------------------
import { AuthorInstance } from './author-interface'
import { TagAttributes, TagInstance } from './tag-interface'
+import { VideoFileAttributes, VideoFileInstance } from './video-file-interface'
// Don't use barrel, import just what we need
import { Video as FormatedVideo } from '../../../shared/models/videos/video.model'
import { ResultList } from '../../../shared/models/result-list.model'
+export type FormatedRemoteVideoFile = {
+ infoHash: string
+ resolution: number
+ extname: string
+ size: number
+}
+
export type FormatedAddRemoteVideo = {
uuid: string
name: string
language: number
nsfw: boolean
description: string
- infoHash: string
author: string
duration: number
thumbnailData: string
tags: string[]
createdAt: Date
updatedAt: Date
- extname: string
views: number
likes: number
dislikes: number
+ files: FormatedRemoteVideoFile[]
}
export type FormatedUpdateRemoteVideo = {
language: number
nsfw: boolean
description: string
- infoHash: string
author: string
duration: number
tags: string[]
createdAt: Date
updatedAt: Date
- extname: string
views: number
likes: number
dislikes: number
+ files: FormatedRemoteVideoFile[]
}
export namespace VideoMethods {
- export type GenerateMagnetUri = (this: VideoInstance) => string
- export type GetVideoFilename = (this: VideoInstance) => string
export type GetThumbnailName = (this: VideoInstance) => string
export type GetPreviewName = (this: VideoInstance) => string
- export type GetTorrentName = (this: VideoInstance) => string
export type IsOwned = (this: VideoInstance) => boolean
export type ToFormatedJSON = (this: VideoInstance) => FormatedVideo
+ export type GenerateMagnetUri = (this: VideoInstance, videoFile: VideoFileInstance) => string
+ export type GetTorrentFileName = (this: VideoInstance, videoFile: VideoFileInstance) => string
+ export type GetVideoFilename = (this: VideoInstance, videoFile: VideoFileInstance) => string
+ export type CreatePreview = (this: VideoInstance, videoFile: VideoFileInstance) => Promise<string>
+ export type CreateThumbnail = (this: VideoInstance, videoFile: VideoFileInstance) => Promise<string>
+ export type GetVideoFilePath = (this: VideoInstance, videoFile: VideoFileInstance) => string
+ export type CreateTorrentAndSetInfoHash = (this: VideoInstance, videoFile: VideoFileInstance) => Promise<void>
+
export type ToAddRemoteJSON = (this: VideoInstance) => Promise<FormatedAddRemoteVideo>
export type ToUpdateRemoteJSON = (this: VideoInstance) => FormatedUpdateRemoteVideo
- export type TranscodeVideofile = (this: VideoInstance) => Promise<void>
+ export type TranscodeVideofile = (this: VideoInstance, inputVideoFile: VideoFileInstance) => Promise<void>
// Return thumbnail name
export type GenerateThumbnailFromData = (video: VideoInstance, thumbnailData: string) => Promise<string>
export type LoadAndPopulateAuthor = (id: number) => Promise<VideoInstance>
export type LoadAndPopulateAuthorAndPodAndTags = (id: number) => Promise<VideoInstance>
export type LoadByUUIDAndPopulateAuthorAndPodAndTags = (uuid: string) => Promise<VideoInstance>
+
+ export type RemoveThumbnail = (this: VideoInstance) => Promise<void>
+ export type RemovePreview = (this: VideoInstance) => Promise<void>
+ export type RemoveFile = (this: VideoInstance, videoFile: VideoFileInstance) => Promise<void>
+ export type RemoveTorrent = (this: VideoInstance, videoFile: VideoFileInstance) => Promise<void>
}
export interface VideoClass {
- generateMagnetUri: VideoMethods.GenerateMagnetUri
- getVideoFilename: VideoMethods.GetVideoFilename
- getThumbnailName: VideoMethods.GetThumbnailName
- getPreviewName: VideoMethods.GetPreviewName
- getTorrentName: VideoMethods.GetTorrentName
- isOwned: VideoMethods.IsOwned
- toFormatedJSON: VideoMethods.ToFormatedJSON
- toAddRemoteJSON: VideoMethods.ToAddRemoteJSON
- toUpdateRemoteJSON: VideoMethods.ToUpdateRemoteJSON
- transcodeVideofile: VideoMethods.TranscodeVideofile
-
generateThumbnailFromData: VideoMethods.GenerateThumbnailFromData
getDurationFromFile: VideoMethods.GetDurationFromFile
list: VideoMethods.List
listForApi: VideoMethods.ListForApi
- loadByHostAndUUID: VideoMethods.LoadByHostAndUUID
listOwnedAndPopulateAuthorAndTags: VideoMethods.ListOwnedAndPopulateAuthorAndTags
listOwnedByAuthor: VideoMethods.ListOwnedByAuthor
load: VideoMethods.Load
- loadByUUID: VideoMethods.LoadByUUID
loadAndPopulateAuthor: VideoMethods.LoadAndPopulateAuthor
loadAndPopulateAuthorAndPodAndTags: VideoMethods.LoadAndPopulateAuthorAndPodAndTags
+ loadByHostAndUUID: VideoMethods.LoadByHostAndUUID
+ loadByUUID: VideoMethods.LoadByUUID
loadByUUIDAndPopulateAuthorAndPodAndTags: VideoMethods.LoadByUUIDAndPopulateAuthorAndPodAndTags
searchAndPopulateAuthorAndPodAndTags: VideoMethods.SearchAndPopulateAuthorAndPodAndTags
}
export interface VideoAttributes {
uuid?: string
name: string
- extname: string
category: number
licence: number
language: number
nsfw: boolean
description: string
- infoHash?: string
duration: number
views?: number
likes?: number
Author?: AuthorInstance
Tags?: TagInstance[]
+ VideoFiles?: VideoFileInstance[]
}
export interface VideoInstance extends VideoClass, VideoAttributes, Sequelize.Instance<VideoAttributes> {
createdAt: Date
updatedAt: Date
+ createPreview: VideoMethods.CreatePreview
+ createThumbnail: VideoMethods.CreateThumbnail
+ createTorrentAndSetInfoHash: VideoMethods.CreateTorrentAndSetInfoHash
generateMagnetUri: VideoMethods.GenerateMagnetUri
- getVideoFilename: VideoMethods.GetVideoFilename
- getThumbnailName: VideoMethods.GetThumbnailName
getPreviewName: VideoMethods.GetPreviewName
- getTorrentName: VideoMethods.GetTorrentName
+ getThumbnailName: VideoMethods.GetThumbnailName
+ getTorrentFileName: VideoMethods.GetTorrentFileName
+ getVideoFilename: VideoMethods.GetVideoFilename
+ getVideoFilePath: VideoMethods.GetVideoFilePath
isOwned: VideoMethods.IsOwned
- toFormatedJSON: VideoMethods.ToFormatedJSON
+ removeFile: VideoMethods.RemoveFile
+ removePreview: VideoMethods.RemovePreview
+ removeThumbnail: VideoMethods.RemoveThumbnail
+ removeTorrent: VideoMethods.RemoveTorrent
toAddRemoteJSON: VideoMethods.ToAddRemoteJSON
+ toFormatedJSON: VideoMethods.ToFormatedJSON
toUpdateRemoteJSON: VideoMethods.ToUpdateRemoteJSON
transcodeVideofile: VideoMethods.TranscodeVideofile
setTags: Sequelize.HasManySetAssociationsMixin<TagAttributes, string>
+ setVideoFiles: Sequelize.HasManySetAssociationsMixin<VideoFileAttributes, string>
}
export interface VideoModel extends VideoClass, Sequelize.Model<VideoInstance, VideoAttributes> {}
const Buffer = safeBuffer.Buffer
import * as ffmpeg from 'fluent-ffmpeg'
import * as magnetUtil from 'magnet-uri'
-import { map, values } from 'lodash'
+import { map } from 'lodash'
import * as parseTorrent from 'parse-torrent'
import { join } from 'path'
import * as Sequelize from 'sequelize'
import * as Promise from 'bluebird'
-import { database as db } from '../../initializers/database'
import { TagInstance } from './tag-interface'
import {
logger,
isVideoLanguageValid,
isVideoNSFWValid,
isVideoDescriptionValid,
- isVideoInfoHashValid,
isVideoDurationValid,
readFileBufferPromise,
unlinkPromise,
createTorrentPromise
} from '../../helpers'
import {
- CONSTRAINTS_FIELDS,
CONFIG,
REMOTE_SCHEME,
STATIC_PATHS,
VIDEO_CATEGORIES,
VIDEO_LICENCES,
VIDEO_LANGUAGES,
- THUMBNAILS_SIZE
+ THUMBNAILS_SIZE,
+ VIDEO_FILE_RESOLUTIONS
} from '../../initializers'
-import { JobScheduler, removeVideoToFriends } from '../../lib'
+import { removeVideoToFriends } from '../../lib'
+import { VideoFileInstance } from './video-file-interface'
import { addMethodsToModel, getSort } from '../utils'
import {
let getVideoFilename: VideoMethods.GetVideoFilename
let getThumbnailName: VideoMethods.GetThumbnailName
let getPreviewName: VideoMethods.GetPreviewName
-let getTorrentName: VideoMethods.GetTorrentName
+let getTorrentFileName: VideoMethods.GetTorrentFileName
let isOwned: VideoMethods.IsOwned
let toFormatedJSON: VideoMethods.ToFormatedJSON
let toAddRemoteJSON: VideoMethods.ToAddRemoteJSON
let toUpdateRemoteJSON: VideoMethods.ToUpdateRemoteJSON
let transcodeVideofile: VideoMethods.TranscodeVideofile
+let createPreview: VideoMethods.CreatePreview
+let createThumbnail: VideoMethods.CreateThumbnail
+let getVideoFilePath: VideoMethods.GetVideoFilePath
+let createTorrentAndSetInfoHash: VideoMethods.CreateTorrentAndSetInfoHash
let generateThumbnailFromData: VideoMethods.GenerateThumbnailFromData
let getDurationFromFile: VideoMethods.GetDurationFromFile
let loadAndPopulateAuthorAndPodAndTags: VideoMethods.LoadAndPopulateAuthorAndPodAndTags
let loadByUUIDAndPopulateAuthorAndPodAndTags: VideoMethods.LoadByUUIDAndPopulateAuthorAndPodAndTags
let searchAndPopulateAuthorAndPodAndTags: VideoMethods.SearchAndPopulateAuthorAndPodAndTags
+let removeThumbnail: VideoMethods.RemoveThumbnail
+let removePreview: VideoMethods.RemovePreview
+let removeFile: VideoMethods.RemoveFile
+let removeTorrent: VideoMethods.RemoveTorrent
export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) {
Video = sequelize.define<VideoInstance, VideoAttributes>('Video',
}
}
},
- extname: {
- type: DataTypes.ENUM(values(CONSTRAINTS_FIELDS.VIDEOS.EXTNAME)),
- allowNull: false
- },
category: {
type: DataTypes.INTEGER,
allowNull: false,
}
}
},
- infoHash: {
- type: DataTypes.STRING,
- allowNull: false,
- validate: {
- infoHashValid: value => {
- const res = isVideoInfoHashValid(value)
- if (res === false) throw new Error('Video info hash is not valid.')
- }
- }
- },
duration: {
type: DataTypes.INTEGER,
allowNull: false,
{
fields: [ 'duration' ]
},
- {
- fields: [ 'infoHash' ]
- },
{
fields: [ 'views' ]
},
}
],
hooks: {
- beforeValidate,
- beforeCreate,
afterDestroy
}
}
listOwnedAndPopulateAuthorAndTags,
listOwnedByAuthor,
load,
- loadByUUID,
- loadByHostAndUUID,
loadAndPopulateAuthor,
loadAndPopulateAuthorAndPodAndTags,
+ loadByHostAndUUID,
+ loadByUUID,
loadByUUIDAndPopulateAuthorAndPodAndTags,
- searchAndPopulateAuthorAndPodAndTags,
- removeFromBlacklist
+ searchAndPopulateAuthorAndPodAndTags
]
const instanceMethods = [
+ createPreview,
+ createThumbnail,
+ createTorrentAndSetInfoHash,
generateMagnetUri,
- getVideoFilename,
- getThumbnailName,
getPreviewName,
- getTorrentName,
+ getThumbnailName,
+ getTorrentFileName,
+ getVideoFilename,
+ getVideoFilePath,
isOwned,
- toFormatedJSON,
+ removeFile,
+ removePreview,
+ removeThumbnail,
+ removeTorrent,
toAddRemoteJSON,
+ toFormatedJSON,
toUpdateRemoteJSON,
transcodeVideofile
]
return Video
}
-function beforeValidate (video: VideoInstance) {
- // Put a fake infoHash if it does not exists yet
- if (video.isOwned() && !video.infoHash) {
- // 40 hexa length
- video.infoHash = '0123456789abcdef0123456789abcdef01234567'
- }
-}
-
-function beforeCreate (video: VideoInstance, options: { transaction: Sequelize.Transaction }) {
- if (video.isOwned()) {
- const videoPath = join(CONFIG.STORAGE.VIDEOS_DIR, video.getVideoFilename())
- const tasks = []
-
- tasks.push(
- createTorrentFromVideo(video, videoPath),
- createThumbnail(video, videoPath),
- createPreview(video, videoPath)
- )
-
- if (CONFIG.TRANSCODING.ENABLED === true) {
- // Put uuid because we don't have id auto incremented for now
- const dataInput = {
- videoUUID: video.uuid
- }
-
- tasks.push(
- JobScheduler.Instance.createJob(options.transaction, 'videoTranscoder', dataInput)
- )
- }
-
- return Promise.all(tasks)
- }
-
- return Promise.resolve()
-}
-
-function afterDestroy (video: VideoInstance) {
- const tasks = []
-
- tasks.push(
- removeThumbnail(video)
- )
-
- if (video.isOwned()) {
- const removeVideoToFriendsParams = {
- uuid: video.uuid
- }
-
- tasks.push(
- removeFile(video),
- removeTorrent(video),
- removePreview(video),
- removeVideoToFriends(removeVideoToFriendsParams)
- )
- }
-
- return Promise.all(tasks)
-}
-
// ------------------------------ METHODS ------------------------------
function associate (models) {
},
onDelete: 'cascade'
})
+
+ Video.hasMany(models.VideoFile, {
+ foreignKey: {
+ name: 'videoId',
+ allowNull: false
+ },
+ onDelete: 'cascade'
+ })
}
-generateMagnetUri = function (this: VideoInstance) {
- let baseUrlHttp
- let baseUrlWs
+function afterDestroy (video: VideoInstance) {
+ const tasks = []
- if (this.isOwned()) {
- baseUrlHttp = CONFIG.WEBSERVER.URL
- baseUrlWs = CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT
- } else {
- baseUrlHttp = REMOTE_SCHEME.HTTP + '://' + this.Author.Pod.host
- baseUrlWs = REMOTE_SCHEME.WS + '://' + this.Author.Pod.host
- }
+ tasks.push(
+ video.removeThumbnail()
+ )
- const xs = baseUrlHttp + STATIC_PATHS.TORRENTS + this.getTorrentName()
- const announce = [ baseUrlWs + '/tracker/socket' ]
- const urlList = [ baseUrlHttp + STATIC_PATHS.WEBSEED + this.getVideoFilename() ]
+ if (video.isOwned()) {
+ const removeVideoToFriendsParams = {
+ uuid: video.uuid
+ }
- const magnetHash = {
- xs,
- announce,
- urlList,
- infoHash: this.infoHash,
- name: this.name
+ tasks.push(
+ video.removePreview(),
+ removeVideoToFriends(removeVideoToFriendsParams)
+ )
+
+ // TODO: check files is populated
+ video.VideoFiles.forEach(file => {
+ video.removeFile(file),
+ video.removeTorrent(file)
+ })
}
- return magnetUtil.encode(magnetHash)
+ return Promise.all(tasks)
}
-getVideoFilename = function (this: VideoInstance) {
- return this.uuid + this.extname
+getVideoFilename = function (this: VideoInstance, videoFile: VideoFileInstance) {
+ // return this.uuid + '-' + VIDEO_FILE_RESOLUTIONS[videoFile.resolution] + videoFile.extname
+ return this.uuid + videoFile.extname
}
getThumbnailName = function (this: VideoInstance) {
return this.uuid + extension
}
-getTorrentName = function (this: VideoInstance) {
+getTorrentFileName = function (this: VideoInstance, videoFile: VideoFileInstance) {
const extension = '.torrent'
+ // return this.uuid + '-' + VIDEO_FILE_RESOLUTIONS[videoFile.resolution] + extension
return this.uuid + extension
}
return this.remote === false
}
+createPreview = function (this: VideoInstance, videoFile: VideoFileInstance) {
+ return generateImage(this, this.getVideoFilePath(videoFile), CONFIG.STORAGE.PREVIEWS_DIR, this.getPreviewName(), null)
+}
+
+createThumbnail = function (this: VideoInstance, videoFile: VideoFileInstance) {
+ return generateImage(this, this.getVideoFilePath(videoFile), CONFIG.STORAGE.THUMBNAILS_DIR, this.getThumbnailName(), THUMBNAILS_SIZE)
+}
+
+getVideoFilePath = function (this: VideoInstance, videoFile: VideoFileInstance) {
+ return join(CONFIG.STORAGE.VIDEOS_DIR, this.getVideoFilename(videoFile))
+}
+
+createTorrentAndSetInfoHash = function (this: VideoInstance, videoFile: VideoFileInstance) {
+ const options = {
+ announceList: [
+ [ CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT + '/tracker/socket' ]
+ ],
+ urlList: [
+ CONFIG.WEBSERVER.URL + STATIC_PATHS.WEBSEED + this.getVideoFilename(videoFile)
+ ]
+ }
+
+ return createTorrentPromise(this.getVideoFilePath(videoFile), options)
+ .then(torrent => {
+ const filePath = join(CONFIG.STORAGE.TORRENTS_DIR, this.getTorrentFileName(videoFile))
+ return writeFilePromise(filePath, torrent).then(() => torrent)
+ })
+ .then(torrent => {
+ const parsedTorrent = parseTorrent(torrent)
+
+ videoFile.infoHash = parsedTorrent.infoHash
+ })
+}
+
+generateMagnetUri = function (this: VideoInstance, videoFile: VideoFileInstance) {
+ let baseUrlHttp
+ let baseUrlWs
+
+ if (this.isOwned()) {
+ baseUrlHttp = CONFIG.WEBSERVER.URL
+ baseUrlWs = CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT
+ } else {
+ baseUrlHttp = REMOTE_SCHEME.HTTP + '://' + this.Author.Pod.host
+ baseUrlWs = REMOTE_SCHEME.WS + '://' + this.Author.Pod.host
+ }
+
+ const xs = baseUrlHttp + STATIC_PATHS.TORRENTS + this.getTorrentFileName(videoFile)
+ const announce = [ baseUrlWs + '/tracker/socket' ]
+ const urlList = [ baseUrlHttp + STATIC_PATHS.WEBSEED + this.getVideoFilename(videoFile) ]
+
+ const magnetHash = {
+ xs,
+ announce,
+ urlList,
+ infoHash: videoFile.infoHash,
+ name: this.name
+ }
+
+ return magnetUtil.encode(magnetHash)
+}
+
toFormatedJSON = function (this: VideoInstance) {
let podHost
description: this.description,
podHost,
isLocal: this.isOwned(),
- magnetUri: this.generateMagnetUri(),
author: this.Author.name,
duration: this.duration,
views: this.views,
thumbnailPath: join(STATIC_PATHS.THUMBNAILS, this.getThumbnailName()),
previewPath: join(STATIC_PATHS.PREVIEWS, this.getPreviewName()),
createdAt: this.createdAt,
- updatedAt: this.updatedAt
+ updatedAt: this.updatedAt,
+ files: []
}
+ this.VideoFiles.forEach(videoFile => {
+ let resolutionLabel = VIDEO_FILE_RESOLUTIONS[videoFile.resolution]
+ if (!resolutionLabel) resolutionLabel = 'Unknown'
+
+ const videoFileJson = {
+ resolution: videoFile.resolution,
+ resolutionLabel,
+ magnetUri: this.generateMagnetUri(videoFile),
+ size: videoFile.size
+ }
+
+ json.files.push(videoFileJson)
+ })
+
return json
}
language: this.language,
nsfw: this.nsfw,
description: this.description,
- infoHash: this.infoHash,
author: this.Author.name,
duration: this.duration,
thumbnailData: thumbnailData.toString('binary'),
tags: map<TagInstance, string>(this.Tags, 'name'),
createdAt: this.createdAt,
updatedAt: this.updatedAt,
- extname: this.extname,
views: this.views,
likes: this.likes,
- dislikes: this.dislikes
+ dislikes: this.dislikes,
+ files: []
}
+ this.VideoFiles.forEach(videoFile => {
+ remoteVideo.files.push({
+ infoHash: videoFile.infoHash,
+ resolution: videoFile.resolution,
+ extname: videoFile.extname,
+ size: videoFile.size
+ })
+ })
+
return remoteVideo
})
}
language: this.language,
nsfw: this.nsfw,
description: this.description,
- infoHash: this.infoHash,
author: this.Author.name,
duration: this.duration,
tags: map<TagInstance, string>(this.Tags, 'name'),
createdAt: this.createdAt,
updatedAt: this.updatedAt,
- extname: this.extname,
views: this.views,
likes: this.likes,
- dislikes: this.dislikes
+ dislikes: this.dislikes,
+ files: []
}
+ this.VideoFiles.forEach(videoFile => {
+ json.files.push({
+ infoHash: videoFile.infoHash,
+ resolution: videoFile.resolution,
+ extname: videoFile.extname,
+ size: videoFile.size
+ })
+ })
+
return json
}
-transcodeVideofile = function (this: VideoInstance) {
- const video = this
-
+transcodeVideofile = function (this: VideoInstance, inputVideoFile: VideoFileInstance) {
const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR
const newExtname = '.mp4'
- const videoInputPath = join(videosDirectory, video.getVideoFilename())
- const videoOutputPath = join(videosDirectory, video.id + '-transcoded' + newExtname)
+ const videoInputPath = join(videosDirectory, this.getVideoFilename(inputVideoFile))
+ const videoOutputPath = join(videosDirectory, this.id + '-transcoded' + newExtname)
return new Promise<void>((res, rej) => {
ffmpeg(videoInputPath)
return unlinkPromise(videoInputPath)
.then(() => {
// Important to do this before getVideoFilename() to take in account the new file extension
- video.set('extname', newExtname)
+ inputVideoFile.set('extname', newExtname)
- const newVideoPath = join(videosDirectory, video.getVideoFilename())
- return renamePromise(videoOutputPath, newVideoPath)
+ return renamePromise(videoOutputPath, this.getVideoFilePath(inputVideoFile))
})
.then(() => {
- const newVideoPath = join(videosDirectory, video.getVideoFilename())
- return createTorrentFromVideo(video, newVideoPath)
+ return this.createTorrentAndSetInfoHash(inputVideoFile)
})
.then(() => {
- return video.save()
+ return inputVideoFile.save()
})
.then(() => {
return res()
})
.catch(err => {
- // Autodesctruction...
- video.destroy().catch(err => logger.error('Cannot destruct video after transcoding failure.', err))
+ // Autodestruction...
+ this.destroy().catch(err => logger.error('Cannot destruct video after transcoding failure.', err))
return rej(err)
})
})
}
+removeThumbnail = function (this: VideoInstance) {
+ const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, this.getThumbnailName())
+ return unlinkPromise(thumbnailPath)
+}
+
+removePreview = function (this: VideoInstance) {
+ // Same name than video thumbnail
+ return unlinkPromise(CONFIG.STORAGE.PREVIEWS_DIR + this.getPreviewName())
+}
+
+removeFile = function (this: VideoInstance, videoFile: VideoFileInstance) {
+ const filePath = join(CONFIG.STORAGE.VIDEOS_DIR, this.getVideoFilename(videoFile))
+ return unlinkPromise(filePath)
+}
+
+removeTorrent = function (this: VideoInstance, videoFile: VideoFileInstance) {
+ const torrenPath = join(CONFIG.STORAGE.TORRENTS_DIR, this.getTorrentFileName(videoFile))
+ return unlinkPromise(torrenPath)
+}
+
// ------------------------------ STATICS ------------------------------
generateThumbnailFromData = function (video: VideoInstance, thumbnailData: string) {
}
list = function () {
- return Video.findAll()
+ const query = {
+ include: [ Video['sequelize'].models.VideoFile ]
+ }
+
+ return Video.findAll(query)
}
listForApi = function (start: number, count: number, sort: string) {
model: Video['sequelize'].models.Author,
include: [ { model: Video['sequelize'].models.Pod, required: false } ]
},
-
- Video['sequelize'].models.Tag
+ Video['sequelize'].models.Tag,
+ Video['sequelize'].models.VideoFile
],
where: createBaseVideosWhere()
}
uuid
},
include: [
+ {
+ model: Video['sequelize'].models.VideoFile
+ },
{
model: Video['sequelize'].models.Author,
include: [
where: {
remote: false
},
- include: [ Video['sequelize'].models.Author, Video['sequelize'].models.Tag ]
+ include: [
+ Video['sequelize'].models.VideoFile,
+ Video['sequelize'].models.Author,
+ Video['sequelize'].models.Tag
+ ]
}
return Video.findAll(query)
remote: false
},
include: [
+ {
+ model: Video['sequelize'].models.VideoFile
+ },
{
model: Video['sequelize'].models.Author,
where: {
const query = {
where: {
uuid
- }
+ },
+ include: [ Video['sequelize'].models.VideoFile ]
}
return Video.findOne(query)
}
loadAndPopulateAuthor = function (id: number) {
const options = {
- include: [ Video['sequelize'].models.Author ]
+ include: [ Video['sequelize'].models.VideoFile, Video['sequelize'].models.Author ]
}
return Video.findById(id, options)
model: Video['sequelize'].models.Author,
include: [ { model: Video['sequelize'].models.Pod, required: false } ]
},
- Video['sequelize'].models.Tag
+ Video['sequelize'].models.Tag,
+ Video['sequelize'].models.VideoFile
]
}
model: Video['sequelize'].models.Author,
include: [ { model: Video['sequelize'].models.Pod, required: false } ]
},
- Video['sequelize'].models.Tag
+ Video['sequelize'].models.Tag,
+ Video['sequelize'].models.VideoFile
]
}
model: Video['sequelize'].models.Tag
}
+ const videoFileInclude: Sequelize.IncludeOptions = {
+ model: Video['sequelize'].models.VideoFile
+ }
+
const query: Sequelize.FindOptions = {
distinct: true,
where: createBaseVideosWhere(),
// Make an exact search with the magnet
if (field === 'magnetUri') {
- const infoHash = magnetUtil.decode(value).infoHash
- query.where['infoHash'] = infoHash
+ videoFileInclude.where = {
+ infoHash: magnetUtil.decode(value).infoHash
+ }
} else if (field === 'tags') {
const escapedValue = Video['sequelize'].escape('%' + value + '%')
query.where['id'].$in = Video['sequelize'].literal(
}
query.include = [
- authorInclude, tagInclude
+ authorInclude, tagInclude, videoFileInclude
]
return Video.findAndCountAll(query).then(({ rows, count }) => {
}
}
-function removeThumbnail (video: VideoInstance) {
- const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, video.getThumbnailName())
- return unlinkPromise(thumbnailPath)
-}
-
-function removeFile (video: VideoInstance) {
- const filePath = join(CONFIG.STORAGE.VIDEOS_DIR, video.getVideoFilename())
- return unlinkPromise(filePath)
-}
-
-function removeTorrent (video: VideoInstance) {
- const torrenPath = join(CONFIG.STORAGE.TORRENTS_DIR, video.getTorrentName())
- return unlinkPromise(torrenPath)
-}
-
-function removePreview (video: VideoInstance) {
- // Same name than video thumnail
- return unlinkPromise(CONFIG.STORAGE.PREVIEWS_DIR + video.getPreviewName())
-}
-
-function createTorrentFromVideo (video: VideoInstance, videoPath: string) {
- const options = {
- announceList: [
- [ CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT + '/tracker/socket' ]
- ],
- urlList: [
- CONFIG.WEBSERVER.URL + STATIC_PATHS.WEBSEED + video.getVideoFilename()
- ]
- }
-
- return createTorrentPromise(videoPath, options)
- .then(torrent => {
- const filePath = join(CONFIG.STORAGE.TORRENTS_DIR, video.getTorrentName())
- return writeFilePromise(filePath, torrent).then(() => torrent)
- })
- .then(torrent => {
- const parsedTorrent = parseTorrent(torrent)
- video.set('infoHash', parsedTorrent.infoHash)
- return video.validate()
- })
-}
-
-function createPreview (video: VideoInstance, videoPath: string) {
- return generateImage(video, videoPath, CONFIG.STORAGE.PREVIEWS_DIR, video.getPreviewName(), null)
-}
-
-function createThumbnail (video: VideoInstance, videoPath: string) {
- return generateImage(video, videoPath, CONFIG.STORAGE.THUMBNAILS_DIR, video.getThumbnailName(), THUMBNAILS_SIZE)
-}
-
function generateImage (video: VideoInstance, videoPath: string, folder: string, imageName: string, size: string) {
const options = {
filename: imageName,
.thumbnail(options)
})
}
-
-function removeFromBlacklist (video: VideoInstance) {
- // Find the blacklisted video
- return db.BlacklistedVideo.loadByVideoId(video.id).then(video => {
- // Not found the video, skip
- if (!video) {
- return null
- }
-
- // If we found the video, remove it from the blacklist
- return video.destroy()
- })
-}
expect(video.nsfw).to.be.ok
expect(video.description).to.equal('my super description for pod 1')
expect(video.podHost).to.equal('localhost:9001')
- expect(video.magnetUri).to.exist
expect(video.duration).to.equal(10)
expect(video.tags).to.deep.equal([ 'tag1p1', 'tag2p1' ])
expect(miscsUtils.dateIsValid(video.createdAt)).to.be.true
expect(miscsUtils.dateIsValid(video.updatedAt)).to.be.true
expect(video.author).to.equal('root')
+ expect(video.files).to.have.lengthOf(1)
+
+ const file = video.files[0]
+ const magnetUri = file.magnetUri
+ expect(file.magnetUri).to.exist
+ expect(file.resolution).to.equal(0)
+ expect(file.resolutionLabel).to.equal('original')
+ expect(file.size).to.equal(572456)
+
if (server.url !== 'http://localhost:9001') {
expect(video.isLocal).to.be.false
} else {
// All pods should have the same magnet Uri
if (baseMagnet === null) {
- baseMagnet = video.magnetUri
+ baseMagnet = magnetUri
} else {
- expect(video.magnetUri).to.equal.magnetUri
+ expect(baseMagnet).to.equal(magnetUri)
}
videosUtils.testVideoImage(server.url, 'video_short1.webm', video.thumbnailPath, function (err, test) {
expect(video.nsfw).to.be.true
expect(video.description).to.equal('my super description for pod 2')
expect(video.podHost).to.equal('localhost:9002')
- expect(video.magnetUri).to.exist
expect(video.duration).to.equal(5)
expect(video.tags).to.deep.equal([ 'tag1p2', 'tag2p2', 'tag3p2' ])
expect(miscsUtils.dateIsValid(video.createdAt)).to.be.true
expect(miscsUtils.dateIsValid(video.updatedAt)).to.be.true
expect(video.author).to.equal('root')
+ expect(video.files).to.have.lengthOf(1)
+
+ const file = video.files[0]
+ const magnetUri = file.magnetUri
+ expect(file.magnetUri).to.exist
+ expect(file.resolution).to.equal(0)
+ expect(file.resolutionLabel).to.equal('original')
+ expect(file.size).to.equal(942961)
+
if (server.url !== 'http://localhost:9002') {
expect(video.isLocal).to.be.false
} else {
// All pods should have the same magnet Uri
if (baseMagnet === null) {
- baseMagnet = video.magnetUri
+ baseMagnet = magnetUri
} else {
- expect(video.magnetUri).to.equal.magnetUri
+ expect(baseMagnet).to.equal(magnetUri)
}
videosUtils.testVideoImage(server.url, 'video_short2.webm', video.thumbnailPath, function (err, test) {
expect(video1.nsfw).to.be.ok
expect(video1.description).to.equal('my super description for pod 3')
expect(video1.podHost).to.equal('localhost:9003')
- expect(video1.magnetUri).to.exist
expect(video1.duration).to.equal(5)
expect(video1.tags).to.deep.equal([ 'tag1p3' ])
expect(video1.author).to.equal('root')
expect(miscsUtils.dateIsValid(video1.createdAt)).to.be.true
expect(miscsUtils.dateIsValid(video1.updatedAt)).to.be.true
+ expect(video1.files).to.have.lengthOf(1)
+
+ const file1 = video1.files[0]
+ const magnetUri1 = file1.magnetUri
+ expect(file1.magnetUri).to.exist
+ expect(file1.resolution).to.equal(0)
+ expect(file1.resolutionLabel).to.equal('original')
+ expect(file1.size).to.equal(292677)
+
expect(video2.name).to.equal('my super name for pod 3-2')
expect(video2.category).to.equal(7)
expect(video2.categoryLabel).to.equal('Gaming')
expect(video2.nsfw).to.be.false
expect(video2.description).to.equal('my super description for pod 3-2')
expect(video2.podHost).to.equal('localhost:9003')
- expect(video2.magnetUri).to.exist
expect(video2.duration).to.equal(5)
expect(video2.tags).to.deep.equal([ 'tag2p3', 'tag3p3', 'tag4p3' ])
expect(video2.author).to.equal('root')
expect(miscsUtils.dateIsValid(video2.createdAt)).to.be.true
expect(miscsUtils.dateIsValid(video2.updatedAt)).to.be.true
+ expect(video2.files).to.have.lengthOf(1)
+
+ const file2 = video2.files[0]
+ const magnetUri2 = file2.magnetUri
+ expect(file2.magnetUri).to.exist
+ expect(file2.resolution).to.equal(0)
+ expect(file2.resolutionLabel).to.equal('original')
+ expect(file2.size).to.equal(218910)
+
if (server.url !== 'http://localhost:9003') {
expect(video1.isLocal).to.be.false
expect(video2.isLocal).to.be.false
// All pods should have the same magnet Uri
if (baseMagnet === null) {
- baseMagnet = video2.magnetUri
+ baseMagnet = magnetUri2
} else {
- expect(video2.magnetUri).to.equal.magnetUri
+ expect(baseMagnet).to.equal(magnetUri2)
}
videosUtils.testVideoImage(server.url, 'video_short3.webm', video1.thumbnailPath, function (err, test) {
toRemove.push(res.body.data[2])
toRemove.push(res.body.data[3])
- webtorrent.add(video.magnetUri, function (torrent) {
+ webtorrent.add(video.files[0].magnetUri, function (torrent) {
expect(torrent.files).to.exist
expect(torrent.files.length).to.equal(1)
expect(torrent.files[0].path).to.exist.and.to.not.equal('')
const video = res.body.data[1]
- webtorrent.add(video.magnetUri, function (torrent) {
+ webtorrent.add(video.files[0].magnetUri, function (torrent) {
expect(torrent.files).to.exist
expect(torrent.files.length).to.equal(1)
expect(torrent.files[0].path).to.exist.and.to.not.equal('')
const video = res.body.data[2]
- webtorrent.add(video.magnetUri, function (torrent) {
+ webtorrent.add(video.files[0].magnetUri, function (torrent) {
expect(torrent.files).to.exist
expect(torrent.files.length).to.equal(1)
expect(torrent.files[0].path).to.exist.and.to.not.equal('')
const video = res.body.data[3]
- webtorrent.add(video.magnetUri, function (torrent) {
+ webtorrent.add(video.files[0].magnetUri, function (torrent) {
expect(torrent.files).to.exist
expect(torrent.files.length).to.equal(1)
expect(torrent.files[0].path).to.exist.and.to.not.equal('')
expect(videoUpdated.tags).to.deep.equal([ 'tagup1', 'tagup2' ])
expect(miscsUtils.dateIsValid(videoUpdated.updatedAt, 20000)).to.be.true
+ const file = videoUpdated.files[0]
+ const magnetUri = file.magnetUri
+ expect(file.magnetUri).to.exist
+ expect(file.resolution).to.equal(0)
+ expect(file.resolutionLabel).to.equal('original')
+ expect(file.size).to.equal(292677)
+
videosUtils.testVideoImage(server.url, 'video_short3.webm', videoUpdated.thumbnailPath, function (err, test) {
if (err) throw err
expect(test).to.equal(true)
- webtorrent.add(videoUpdated.magnetUri, function (torrent) {
+ webtorrent.add(videoUpdated.files[0].magnetUri, function (torrent) {
expect(torrent.files).to.exist
expect(torrent.files.length).to.equal(1)
expect(torrent.files[0].path).to.exist.and.to.not.equal('')
expect(video.nsfw).to.be.ok
expect(video.description).to.equal('my super description')
expect(video.podHost).to.equal('localhost:9001')
- expect(video.magnetUri).to.exist
expect(video.author).to.equal('root')
expect(video.isLocal).to.be.true
expect(video.tags).to.deep.equal([ 'tag1', 'tag2', 'tag3' ])
expect(miscsUtils.dateIsValid(video.createdAt)).to.be.true
expect(miscsUtils.dateIsValid(video.updatedAt)).to.be.true
+ expect(video.files).to.have.lengthOf(1)
+
+ const file = video.files[0]
+ const magnetUri = file.magnetUri
+ expect(file.magnetUri).to.exist
+ expect(file.resolution).to.equal(0)
+ expect(file.resolutionLabel).to.equal('original')
+ expect(file.size).to.equal(218910)
+
videosUtils.testVideoImage(server.url, 'video_short.webm', video.thumbnailPath, function (err, test) {
if (err) throw err
expect(test).to.equal(true)
videoId = video.id
videoUUID = video.uuid
- webtorrent.add(video.magnetUri, function (torrent) {
+ webtorrent.add(magnetUri, function (torrent) {
expect(torrent.files).to.exist
expect(torrent.files.length).to.equal(1)
expect(torrent.files[0].path).to.exist.and.to.not.equal('')
expect(video.nsfw).to.be.ok
expect(video.description).to.equal('my super description')
expect(video.podHost).to.equal('localhost:9001')
- expect(video.magnetUri).to.exist
expect(video.author).to.equal('root')
expect(video.isLocal).to.be.true
expect(video.tags).to.deep.equal([ 'tag1', 'tag2', 'tag3' ])
expect(miscsUtils.dateIsValid(video.createdAt)).to.be.true
expect(miscsUtils.dateIsValid(video.updatedAt)).to.be.true
+ expect(video.files).to.have.lengthOf(1)
+
+ const file = video.files[0]
+ const magnetUri = file.magnetUri
+ expect(file.magnetUri).to.exist
+ expect(file.resolution).to.equal(0)
+ expect(file.resolutionLabel).to.equal('original')
+ expect(file.size).to.equal(218910)
+
videosUtils.testVideoImage(server.url, 'video_short.webm', video.thumbnailPath, function (err, test) {
if (err) throw err
expect(test).to.equal(true)
expect(miscsUtils.dateIsValid(video.createdAt)).to.be.true
expect(miscsUtils.dateIsValid(video.updatedAt)).to.be.true
+ expect(video.files).to.have.lengthOf(1)
+
+ const file = video.files[0]
+ const magnetUri = file.magnetUri
+ expect(file.magnetUri).to.exist
+ expect(file.resolution).to.equal(0)
+ expect(file.resolutionLabel).to.equal('original')
+ expect(file.size).to.equal(218910)
+
videosUtils.testVideoImage(server.url, 'video_short.webm', video.thumbnailPath, function (err, test) {
if (err) throw err
expect(test).to.equal(true)
expect(miscsUtils.dateIsValid(video.createdAt)).to.be.true
expect(miscsUtils.dateIsValid(video.updatedAt)).to.be.true
+ expect(video.files).to.have.lengthOf(1)
+
+ const file = video.files[0]
+ const magnetUri = file.magnetUri
+ expect(file.magnetUri).to.exist
+ expect(file.resolution).to.equal(0)
+ expect(file.resolutionLabel).to.equal('original')
+ expect(file.size).to.equal(218910)
+
videosUtils.testVideoImage(server.url, 'video_short.webm', video.thumbnailPath, function (err, test) {
if (err) throw err
expect(test).to.equal(true)
it('Should search the right magnetUri video', function (done) {
const video = videosListBase[0]
- videosUtils.searchVideoWithPagination(server.url, encodeURIComponent(video.magnetUri), 'magnetUri', 0, 15, function (err, res) {
+ videosUtils.searchVideoWithPagination(server.url, encodeURIComponent(video.files[0].magnetUri), 'magnetUri', 0, 15, function (err, res) {
if (err) throw err
const videos = res.body.data
expect(miscsUtils.dateIsValid(video.createdAt)).to.be.true
expect(miscsUtils.dateIsValid(video.updatedAt)).to.be.true
+ expect(video.files).to.have.lengthOf(1)
+
+ const file = video.files[0]
+ const magnetUri = file.magnetUri
+ expect(file.magnetUri).to.exist
+ expect(file.resolution).to.equal(0)
+ expect(file.resolutionLabel).to.equal('original')
+ expect(file.size).to.equal(292677)
+
videosUtils.testVideoImage(server.url, 'video_short3.webm', video.thumbnailPath, function (err, test) {
if (err) throw err
expect(test).to.equal(true)
- webtorrent.add(video.magnetUri, function (torrent) {
+ webtorrent.add(magnetUri, function (torrent) {
expect(torrent.files).to.exist
expect(torrent.files.length).to.equal(1)
expect(torrent.files[0].path).to.exist.and.to.not.equal('')
expect(miscsUtils.dateIsValid(video.createdAt)).to.be.true
expect(miscsUtils.dateIsValid(video.updatedAt)).to.be.true
+ expect(video.files).to.have.lengthOf(1)
+
+ const file = video.files[0]
+ const magnetUri = file.magnetUri
+ expect(file.magnetUri).to.exist
+ expect(file.resolution).to.equal(0)
+ expect(file.resolutionLabel).to.equal('original')
+ expect(file.size).to.equal(292677)
+
done()
})
})
expect(miscsUtils.dateIsValid(video.createdAt)).to.be.true
expect(miscsUtils.dateIsValid(video.updatedAt)).to.be.true
+ expect(video.files).to.have.lengthOf(1)
+
+ const file = video.files[0]
+ const magnetUri = file.magnetUri
+ expect(file.magnetUri).to.exist
+ expect(file.resolution).to.equal(0)
+ expect(file.resolutionLabel).to.equal('original')
+ expect(file.size).to.equal(292677)
+
done()
})
})
if (err) throw err
const video = res.body.data[0]
- expect(video.magnetUri).to.match(/\.webm/)
+ const magnetUri = video.files[0].magnetUri
+ expect(magnetUri).to.match(/\.webm/)
- webtorrent.add(video.magnetUri, function (torrent) {
+ webtorrent.add(magnetUri, function (torrent) {
expect(torrent.files).to.exist
expect(torrent.files.length).to.equal(1)
expect(torrent.files[0].path).match(/\.webm$/)
if (err) throw err
const video = res.body.data[0]
- expect(video.magnetUri).to.match(/\.mp4/)
+ const magnetUri = video.files[0].magnetUri
+ expect(magnetUri).to.match(/\.mp4/)
- webtorrent.add(video.magnetUri, function (torrent) {
+ webtorrent.add(magnetUri, function (torrent) {
expect(torrent.files).to.exist
expect(torrent.files.length).to.equal(1)
expect(torrent.files[0].path).match(/\.mp4$/)
author: string
tags: string[]
name: string
- extname: string
- infoHash: string
category: number
licence: number
language: number
likes: number
dislikes: number
thumbnailData: string
+ files: {
+ infoHash: string
+ extname: string
+ resolution: number
+ size: number
+ }[]
}
export interface RemoteVideoCreateRequest extends RemoteVideoRequest {
views: number
likes: number
dislikes: number
+ files: {
+ infoHash: string
+ extname: string
+ resolution: number
+ size: number
+ }[]
}
export interface RemoteVideoUpdateRequest {
+export interface VideoFile {
+ magnetUri: string
+ resolution: number
+ resolutionLabel: string
+ size: number // Bytes
+}
+
export interface Video {
id: number
uuid: string
description: string
duration: number
isLocal: boolean
- magnetUri: string
name: string
podHost: string
tags: string[]
likes: number
dislikes: number
nsfw: boolean
+ files: VideoFile[]
}