Move video file metadata in their own table

Will be used for user video quotas and multiple video resolutions
pull/87/head
Chocobozzz 2017-08-25 11:36:23 +02:00
parent 69f224587e
commit 93e1258c7c
30 changed files with 818 additions and 340 deletions

1
.gitignore vendored
View File

@ -19,3 +19,4 @@
/*.sublime-workspace /*.sublime-workspace
/dist /dist
/.idea /.idea
/PeerTube.iml

View File

@ -1,4 +1,4 @@
import { Video as VideoServerModel } from '../../../../../shared' import { Video as VideoServerModel, VideoFile } from '../../../../../shared'
import { User } from '../../shared' import { User } from '../../shared'
export class Video implements VideoServerModel { export class Video implements VideoServerModel {
@ -17,7 +17,6 @@ export class Video implements VideoServerModel {
id: number id: number
uuid: string uuid: string
isLocal: boolean isLocal: boolean
magnetUri: string
name: string name: string
podHost: string podHost: string
tags: string[] tags: string[]
@ -29,6 +28,7 @@ export class Video implements VideoServerModel {
likes: number likes: number
dislikes: number dislikes: number
nsfw: boolean nsfw: boolean
files: VideoFile[]
private static createByString (author: string, podHost: string) { private static createByString (author: string, podHost: string) {
return author + '@' + podHost return author + '@' + podHost
@ -57,7 +57,6 @@ export class Video implements VideoServerModel {
id: number, id: number,
uuid: string, uuid: string,
isLocal: boolean, isLocal: boolean,
magnetUri: string,
name: string, name: string,
podHost: string, podHost: string,
tags: string[], tags: string[],
@ -66,7 +65,8 @@ export class Video implements VideoServerModel {
views: number, views: number,
likes: number, likes: number,
dislikes: number, dislikes: number,
nsfw: boolean nsfw: boolean,
files: VideoFile[]
}) { }) {
this.author = hash.author this.author = hash.author
this.createdAt = new Date(hash.createdAt) this.createdAt = new Date(hash.createdAt)
@ -82,7 +82,6 @@ export class Video implements VideoServerModel {
this.id = hash.id this.id = hash.id
this.uuid = hash.uuid this.uuid = hash.uuid
this.isLocal = hash.isLocal this.isLocal = hash.isLocal
this.magnetUri = hash.magnetUri
this.name = hash.name this.name = hash.name
this.podHost = hash.podHost this.podHost = hash.podHost
this.tags = hash.tags this.tags = hash.tags
@ -94,6 +93,7 @@ export class Video implements VideoServerModel {
this.likes = hash.likes this.likes = hash.likes
this.dislikes = hash.dislikes this.dislikes = hash.dislikes
this.nsfw = hash.nsfw this.nsfw = hash.nsfw
this.files = hash.files
this.by = Video.createByString(hash.author, hash.podHost) this.by = Video.createByString(hash.author, hash.podHost)
} }
@ -115,6 +115,13 @@ export class Video implements VideoServerModel {
return (this.nsfw && (!user || user.displayNSFW === false)) 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) { patch (values: Object) {
Object.keys(values).forEach((key) => { Object.keys(values).forEach((key) => {
this[key] = values[key] this[key] = values[key]
@ -132,7 +139,6 @@ export class Video implements VideoServerModel {
duration: this.duration, duration: this.duration,
id: this.id, id: this.id,
isLocal: this.isLocal, isLocal: this.isLocal,
magnetUri: this.magnetUri,
name: this.name, name: this.name,
podHost: this.podHost, podHost: this.podHost,
tags: this.tags, tags: this.tags,
@ -140,7 +146,8 @@ export class Video implements VideoServerModel {
views: this.views, views: this.views,
likes: this.likes, likes: this.likes,
dislikes: this.dislikes, dislikes: this.dislikes,
nsfw: this.nsfw nsfw: this.nsfw,
files: this.files
} }
} }
} }

View File

@ -10,7 +10,7 @@
</div> </div>
<div class="modal-body"> <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> </div>
</div> </div>

View File

@ -90,8 +90,8 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
window.clearInterval(this.torrentInfosInterval) window.clearInterval(this.torrentInfosInterval)
window.clearTimeout(this.errorTimer) window.clearTimeout(this.errorTimer)
if (this.video !== null && this.webTorrentService.has(this.video.magnetUri)) { if (this.video !== null && this.webTorrentService.has(this.video.getDefaultMagnetUri())) {
this.webTorrentService.remove(this.video.magnetUri) this.webTorrentService.remove(this.video.getDefaultMagnetUri())
} }
// Remove player // Remove player
@ -108,13 +108,13 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
// We are loading the video // We are loading the video
this.loading = true 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 // The callback might never return if there are network issues
// So we create a timer to inform the user the load is abnormally long // So we create a timer to inform the user the load is abnormally long
this.errorTimer = window.setTimeout(() => this.loadTooLong(), VideoWatchComponent.LOADTIME_TOO_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 // Clear the error timer
window.clearTimeout(this.errorTimer) window.clearTimeout(this.errorTimer)
// Maybe the error was fired by the timer, so reset it // Maybe the error was fired by the timer, so reset it
@ -123,7 +123,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
// We are not loading the video anymore // We are not loading the video anymore
this.loading = false this.loading = false
console.log('Added ' + this.video.magnetUri + '.') console.log('Added ' + this.video.getDefaultMagnetUri() + '.')
torrent.files[0].renderTo(this.playerElement, (err) => { torrent.files[0].renderTo(this.playerElement, (err) => {
if (err) { if (err) {
this.notificationsService.error('Error', 'Cannot append the file in the video element.') this.notificationsService.error('Error', 'Cannot append the file in the video element.')

View File

@ -57,7 +57,11 @@ loadVideoInfos(videoId, (err, videoInfos) => {
return 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 videoContainer = document.getElementById('video-container') as HTMLVideoElement
const previewUrl = window.location.origin + videoInfos.previewPath const previewUrl = window.location.origin + videoInfos.previewPath
videoContainer.poster = previewUrl videoContainer.poster = previewUrl

View File

@ -30,6 +30,7 @@
"danger:clean:modules": "scripty", "danger:clean:modules": "scripty",
"reset-password": "ts-node ./scripts/reset-password.ts", "reset-password": "ts-node ./scripts/reset-password.ts",
"play": "scripty", "play": "scripty",
"dev": "scripty",
"dev:server": "scripty", "dev:server": "scripty",
"dev:client": "scripty", "dev:client": "scripty",
"start": "node dist/server", "start": "node dist/server",

5
scripts/dev/index.sh Executable file
View File

@ -0,0 +1,5 @@
#!/usr/bin/env sh
NODE_ENV=test concurrently -k \
"npm run watch:client" \
"npm run watch:server"

View File

@ -1,4 +1,5 @@
import { readFileSync, writeFileSync } from 'fs' import { readFileSync, writeFileSync } from 'fs'
import { join } from 'path'
import * as parseTorrent from 'parse-torrent' import * as parseTorrent from 'parse-torrent'
import { CONFIG, STATIC_PATHS } from '../server/initializers/constants' import { CONFIG, STATIC_PATHS } from '../server/initializers/constants'
@ -19,17 +20,10 @@ db.init(true)
return db.Video.list() return db.Video.list()
}) })
.then(videos => { .then(videos => {
videos.forEach(function (video) { videos.forEach(video => {
const torrentName = video.id + '.torrent' video.VideoFiles.forEach(file => {
const torrentPath = CONFIG.STORAGE.TORRENTS_DIR + torrentName video.createTorrentAndSetInfoHash(file)
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)
}) })
process.exit(0) process.exit(0)

View File

@ -26,7 +26,7 @@ const app = express()
// ----------- Database ----------- // ----------- Database -----------
// Do not use barrels because we don't want to load all modules here (we need to initialize database first) // 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 { 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 // Initialize database and models
import { database as db } from './server/initializers/database' import { database as db } from './server/initializers/database'
db.init(false).then(() => onDatabaseInitDone()) db.init(false).then(() => onDatabaseInitDone())
@ -57,10 +57,20 @@ import { apiRouter, clientsRouter, staticRouter } from './server/controllers'
// Enable CORS for develop // Enable CORS for develop
if (isTestInstance()) { if (isTestInstance()) {
app.use(cors({ app.use((req, res, next) => {
origin: 'http://localhost:3000', // These routes have already cors
credentials: true 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 // For the logger

View File

@ -258,8 +258,6 @@ function addRemoteVideo (videoToCreateData: RemoteVideoCreateData, fromPod: PodI
const videoData = { const videoData = {
name: videoToCreateData.name, name: videoToCreateData.name,
uuid: videoToCreateData.uuid, uuid: videoToCreateData.uuid,
extname: videoToCreateData.extname,
infoHash: videoToCreateData.infoHash,
category: videoToCreateData.category, category: videoToCreateData.category,
licence: videoToCreateData.licence, licence: videoToCreateData.licence,
language: videoToCreateData.language, language: videoToCreateData.language,
@ -289,6 +287,26 @@ function addRemoteVideo (videoToCreateData: RemoteVideoCreateData, fromPod: PodI
return video.save(options).then(videoCreated => ({ tagInstances, videoCreated })) 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 }) => { .then(({ tagInstances, videoCreated }) => {
const options = { const options = {
transaction: t transaction: t
@ -344,6 +362,26 @@ function updateRemoteVideo (videoAttributesToUpdate: RemoteVideoUpdateData, from
return videoInstance.save(options).then(() => ({ videoInstance, tagInstances })) 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 }) => { .then(({ videoInstance, tagInstances }) => {
const options = { transaction: t } const options = { transaction: t }

View File

@ -1,7 +1,7 @@
import * as express from 'express' import * as express from 'express'
import * as Promise from 'bluebird' import * as Promise from 'bluebird'
import * as multer from 'multer' import * as multer from 'multer'
import * as path from 'path' import { extname, join } from 'path'
import { database as db } from '../../../initializers/database' import { database as db } from '../../../initializers/database'
import { import {
@ -16,7 +16,8 @@ import {
addEventToRemoteVideo, addEventToRemoteVideo,
quickAndDirtyUpdateVideoToFriends, quickAndDirtyUpdateVideoToFriends,
addVideoToFriends, addVideoToFriends,
updateVideoToFriends updateVideoToFriends,
JobScheduler
} from '../../../lib' } from '../../../lib'
import { import {
authenticate, authenticate,
@ -155,7 +156,7 @@ function addVideoRetryWrapper (req: express.Request, res: express.Response, next
.catch(err => next(err)) .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 const videoInfos: VideoCreate = req.body
return db.sequelize.transaction(t => { return db.sequelize.transaction(t => {
@ -177,13 +178,13 @@ function addVideo (req: express.Request, res: express.Response, videoFile: Expre
const videoData = { const videoData = {
name: videoInfos.name, name: videoInfos.name,
remote: false, remote: false,
extname: path.extname(videoFile.filename), extname: extname(videoPhysicalFile.filename),
category: videoInfos.category, category: videoInfos.category,
licence: videoInfos.licence, licence: videoInfos.licence,
language: videoInfos.language, language: videoInfos.language,
nsfw: videoInfos.nsfw, nsfw: videoInfos.nsfw,
description: videoInfos.description, 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 authorId: author.id
} }
@ -191,18 +192,50 @@ function addVideo (req: express.Request, res: express.Response, videoFile: Expre
return { author, tagInstances, video } return { author, tagInstances, video }
}) })
.then(({ 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 videoDir = CONFIG.STORAGE.VIDEOS_DIR
const source = path.join(videoDir, videoFile.filename) const source = join(videoDir, videoPhysicalFile.filename)
const destination = path.join(videoDir, video.getVideoFilename()) const destination = join(videoDir, video.getVideoFilename(videoFile))
return renamePromise(source, destination) return renamePromise(source, destination)
.then(() => { .then(() => {
// This is important in case if there is another attempt in the retry process // This is important in case if there is another attempt in the retry process
videoFile.filename = video.getVideoFilename() videoPhysicalFile.filename = video.getVideoFilename(videoFile)
return { author, tagInstances, video } 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 } const options = { transaction: t }
return video.save(options) return video.save(options)
@ -210,9 +243,17 @@ function addVideo (req: express.Request, res: express.Response, videoFile: Expre
// Do not forget to add Author informations to the created video // Do not forget to add Author informations to the created video
videoCreated.Author = author 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 }) => { .then(({ tagInstances, video }) => {
if (!tagInstances) return video if (!tagInstances) return video
@ -236,7 +277,7 @@ function addVideo (req: express.Request, res: express.Response, videoFile: Expre
}) })
.then(() => logger.info('Video with name %s created.', videoInfos.name)) .then(() => logger.info('Video with name %s created.', videoInfos.name))
.catch((err: Error) => { .catch((err: Error) => {
logger.debug('Cannot insert the video.', { error: err.stack }) logger.debug('Cannot insert the video.', err)
throw err throw err
}) })
} }

View File

@ -23,10 +23,11 @@ import {
isVideoNSFWValid, isVideoNSFWValid,
isVideoDescriptionValid, isVideoDescriptionValid,
isVideoDurationValid, isVideoDurationValid,
isVideoInfoHashValid, isVideoFileInfoHashValid,
isVideoNameValid, isVideoNameValid,
isVideoTagsValid, isVideoTagsValid,
isVideoExtnameValid isVideoFileExtnameValid,
isVideoFileResolutionValid
} from '../videos' } from '../videos'
const ENDPOINT_ACTIONS = REQUEST_ENDPOINT_ACTIONS[REQUEST_ENDPOINTS.VIDEOS] const ENDPOINT_ACTIONS = REQUEST_ENDPOINT_ACTIONS[REQUEST_ENDPOINTS.VIDEOS]
@ -121,14 +122,22 @@ function isCommonVideoAttributesValid (video: any) {
isVideoNSFWValid(video.nsfw) && isVideoNSFWValid(video.nsfw) &&
isVideoDescriptionValid(video.description) && isVideoDescriptionValid(video.description) &&
isVideoDurationValid(video.duration) && isVideoDurationValid(video.duration) &&
isVideoInfoHashValid(video.infoHash) &&
isVideoNameValid(video.name) && isVideoNameValid(video.name) &&
isVideoTagsValid(video.tags) && isVideoTagsValid(video.tags) &&
isVideoUUIDValid(video.uuid) && isVideoUUIDValid(video.uuid) &&
isVideoExtnameValid(video.extname) &&
isVideoViewsValid(video.views) && isVideoViewsValid(video.views) &&
isVideoLikesValid(video.likes) && 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) { function isRequestTypeAddValid (value: string) {

View File

@ -7,7 +7,8 @@ import {
VIDEO_CATEGORIES, VIDEO_CATEGORIES,
VIDEO_LICENCES, VIDEO_LICENCES,
VIDEO_LANGUAGES, VIDEO_LANGUAGES,
VIDEO_RATE_TYPES VIDEO_RATE_TYPES,
VIDEO_FILE_RESOLUTIONS
} from '../../initializers' } from '../../initializers'
import { isUserUsernameValid } from './users' import { isUserUsernameValid } from './users'
import { isArray, exists } from './misc' import { isArray, exists } from './misc'
@ -53,14 +54,6 @@ function isVideoDurationValid (value: string) {
return exists(value) && validator.isInt(value + '', VIDEOS_CONSTRAINTS_FIELDS.DURATION) 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) { function isVideoNameValid (value: string) {
return exists(value) && validator.isLength(value, VIDEOS_CONSTRAINTS_FIELDS.NAME) return exists(value) && validator.isLength(value, VIDEOS_CONSTRAINTS_FIELDS.NAME)
} }
@ -128,6 +121,22 @@ function isVideoFile (value: string, files: { [ fieldname: string ]: Express.Mul
return new RegExp('^video/(webm|mp4|ogg)$', 'i').test(file.mimetype) 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 { export {
@ -140,12 +149,12 @@ export {
isVideoNSFWValid, isVideoNSFWValid,
isVideoDescriptionValid, isVideoDescriptionValid,
isVideoDurationValid, isVideoDurationValid,
isVideoInfoHashValid, isVideoFileInfoHashValid,
isVideoNameValid, isVideoNameValid,
isVideoTagsValid, isVideoTagsValid,
isVideoThumbnailValid, isVideoThumbnailValid,
isVideoThumbnailDataValid, isVideoThumbnailDataValid,
isVideoExtnameValid, isVideoFileExtnameValid,
isVideoUUIDValid, isVideoUUIDValid,
isVideoAbuseReasonValid, isVideoAbuseReasonValid,
isVideoAbuseReporterUsernameValid, isVideoAbuseReporterUsernameValid,
@ -154,7 +163,9 @@ export {
isVideoLikesValid, isVideoLikesValid,
isVideoRatingTypeValid, isVideoRatingTypeValid,
isVideoDislikesValid, isVideoDislikesValid,
isVideoEventCountValid isVideoEventCountValid,
isVideoFileSizeValid,
isVideoFileResolutionValid
} }
declare global { declare global {
@ -183,7 +194,9 @@ declare global {
isVideoLikesValid, isVideoLikesValid,
isVideoRatingTypeValid, isVideoRatingTypeValid,
isVideoDislikesValid, isVideoDislikesValid,
isVideoEventCountValid isVideoEventCountValid,
isVideoFileSizeValid,
isVideoFileResolutionValid
} }
} }
} }

View File

@ -15,7 +15,7 @@ import {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
const LAST_MIGRATION_VERSION = 55 const LAST_MIGRATION_VERSION = 65
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -114,7 +114,8 @@ const CONSTRAINTS_FIELDS = {
THUMBNAIL_DATA: { min: 0, max: 20000 }, // Bytes THUMBNAIL_DATA: { min: 0, max: 20000 }, // Bytes
VIEWS: { min: 0 }, VIEWS: { min: 0 },
LIKES: { min: 0 }, LIKES: { min: 0 },
DISLIKES: { min: 0 } DISLIKES: { min: 0 },
FILE_SIZE: { min: 10, max: 1024 * 1024 * 1024 * 3 /* 3Go */ }
}, },
VIDEO_EVENTS: { VIDEO_EVENTS: {
COUNT: { min: 0 } COUNT: { min: 0 }
@ -176,6 +177,14 @@ const VIDEO_LANGUAGES = {
14: 'Italien' 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 // Score a pod has when we create it as a friend
@ -362,6 +371,7 @@ export {
THUMBNAILS_SIZE, THUMBNAILS_SIZE,
USER_ROLES, USER_ROLES,
VIDEO_CATEGORIES, VIDEO_CATEGORIES,
VIDEO_FILE_RESOLUTIONS,
VIDEO_LANGUAGES, VIDEO_LANGUAGES,
VIDEO_LICENCES, VIDEO_LICENCES,
VIDEO_RATE_TYPES VIDEO_RATE_TYPES

View File

@ -23,6 +23,7 @@ import {
UserVideoRateModel, UserVideoRateModel,
VideoAbuseModel, VideoAbuseModel,
BlacklistedVideoModel, BlacklistedVideoModel,
VideoFileModel,
VideoTagModel, VideoTagModel,
VideoModel VideoModel
} from '../models' } from '../models'
@ -49,6 +50,7 @@ const database: {
UserVideoRate?: UserVideoRateModel, UserVideoRate?: UserVideoRateModel,
User?: UserModel, User?: UserModel,
VideoAbuse?: VideoAbuseModel, VideoAbuse?: VideoAbuseModel,
VideoFile?: VideoFileModel,
BlacklistedVideo?: BlacklistedVideoModel, BlacklistedVideo?: BlacklistedVideoModel,
VideoTag?: VideoTagModel, VideoTag?: VideoTagModel,
Video?: VideoModel Video?: VideoModel

View File

@ -0,0 +1,34 @@
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
}

View File

@ -0,0 +1,46 @@
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
}

View File

@ -64,14 +64,16 @@ function getMigrationScripts () {
script: string script: string
}[] = [] }[] = []
files.forEach(file => { files
// Filename is something like 'version-blabla.js' .filter(file => file.endsWith('.js.map') === false)
const version = file.split('-')[0] .forEach(file => {
filesToMigrate.push({ // Filename is something like 'version-blabla.js'
version, const version = file.split('-')[0]
script: file filesToMigrate.push({
version,
script: file
})
}) })
})
return filesToMigrate return filesToMigrate
}) })
@ -93,7 +95,8 @@ function executeMigration (actualVersion: number, entity: { version: string, scr
const options = { const options = {
transaction: t, transaction: t,
queryInterface: db.sequelize.getQueryInterface(), queryInterface: db.sequelize.getQueryInterface(),
sequelize: db.sequelize sequelize: db.sequelize,
db
} }
return migrationScript.up(options) return migrationScript.up(options)

View File

@ -5,7 +5,9 @@ import { VideoInstance } from '../../../models'
function process (data: { videoUUID: string }) { function process (data: { videoUUID: string }) {
return db.Video.loadByUUIDAndPopulateAuthorAndPodAndTags(data.videoUUID).then(video => { 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)
}) })
} }

View File

@ -3,4 +3,5 @@ export * from './tag-interface'
export * from './video-abuse-interface' export * from './video-abuse-interface'
export * from './video-blacklist-interface' export * from './video-blacklist-interface'
export * from './video-tag-interface' export * from './video-tag-interface'
export * from './video-file-interface'
export * from './video-interface' export * from './video-interface'

View File

@ -0,0 +1,24 @@
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> {}

View File

@ -0,0 +1,89 @@
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 ------------------------------

View File

@ -3,11 +3,19 @@ import * as Promise from 'bluebird'
import { AuthorInstance } from './author-interface' import { AuthorInstance } from './author-interface'
import { TagAttributes, TagInstance } from './tag-interface' import { TagAttributes, TagInstance } from './tag-interface'
import { VideoFileAttributes, VideoFileInstance } from './video-file-interface'
// Don't use barrel, import just what we need // Don't use barrel, import just what we need
import { Video as FormatedVideo } from '../../../shared/models/videos/video.model' import { Video as FormatedVideo } from '../../../shared/models/videos/video.model'
import { ResultList } from '../../../shared/models/result-list.model' import { ResultList } from '../../../shared/models/result-list.model'
export type FormatedRemoteVideoFile = {
infoHash: string
resolution: number
extname: string
size: number
}
export type FormatedAddRemoteVideo = { export type FormatedAddRemoteVideo = {
uuid: string uuid: string
name: string name: string
@ -16,17 +24,16 @@ export type FormatedAddRemoteVideo = {
language: number language: number
nsfw: boolean nsfw: boolean
description: string description: string
infoHash: string
author: string author: string
duration: number duration: number
thumbnailData: string thumbnailData: string
tags: string[] tags: string[]
createdAt: Date createdAt: Date
updatedAt: Date updatedAt: Date
extname: string
views: number views: number
likes: number likes: number
dislikes: number dislikes: number
files: FormatedRemoteVideoFile[]
} }
export type FormatedUpdateRemoteVideo = { export type FormatedUpdateRemoteVideo = {
@ -37,31 +44,35 @@ export type FormatedUpdateRemoteVideo = {
language: number language: number
nsfw: boolean nsfw: boolean
description: string description: string
infoHash: string
author: string author: string
duration: number duration: number
tags: string[] tags: string[]
createdAt: Date createdAt: Date
updatedAt: Date updatedAt: Date
extname: string
views: number views: number
likes: number likes: number
dislikes: number dislikes: number
files: FormatedRemoteVideoFile[]
} }
export namespace VideoMethods { export namespace VideoMethods {
export type GenerateMagnetUri = (this: VideoInstance) => string
export type GetVideoFilename = (this: VideoInstance) => string
export type GetThumbnailName = (this: VideoInstance) => string export type GetThumbnailName = (this: VideoInstance) => string
export type GetPreviewName = (this: VideoInstance) => string export type GetPreviewName = (this: VideoInstance) => string
export type GetTorrentName = (this: VideoInstance) => string
export type IsOwned = (this: VideoInstance) => boolean export type IsOwned = (this: VideoInstance) => boolean
export type ToFormatedJSON = (this: VideoInstance) => FormatedVideo 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 ToAddRemoteJSON = (this: VideoInstance) => Promise<FormatedAddRemoteVideo>
export type ToUpdateRemoteJSON = (this: VideoInstance) => FormatedUpdateRemoteVideo 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 // Return thumbnail name
export type GenerateThumbnailFromData = (video: VideoInstance, thumbnailData: string) => Promise<string> export type GenerateThumbnailFromData = (video: VideoInstance, thumbnailData: string) => Promise<string>
@ -86,31 +97,25 @@ export namespace VideoMethods {
export type LoadAndPopulateAuthor = (id: number) => Promise<VideoInstance> export type LoadAndPopulateAuthor = (id: number) => Promise<VideoInstance>
export type LoadAndPopulateAuthorAndPodAndTags = (id: number) => Promise<VideoInstance> export type LoadAndPopulateAuthorAndPodAndTags = (id: number) => Promise<VideoInstance>
export type LoadByUUIDAndPopulateAuthorAndPodAndTags = (uuid: string) => 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 { 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 generateThumbnailFromData: VideoMethods.GenerateThumbnailFromData
getDurationFromFile: VideoMethods.GetDurationFromFile getDurationFromFile: VideoMethods.GetDurationFromFile
list: VideoMethods.List list: VideoMethods.List
listForApi: VideoMethods.ListForApi listForApi: VideoMethods.ListForApi
loadByHostAndUUID: VideoMethods.LoadByHostAndUUID
listOwnedAndPopulateAuthorAndTags: VideoMethods.ListOwnedAndPopulateAuthorAndTags listOwnedAndPopulateAuthorAndTags: VideoMethods.ListOwnedAndPopulateAuthorAndTags
listOwnedByAuthor: VideoMethods.ListOwnedByAuthor listOwnedByAuthor: VideoMethods.ListOwnedByAuthor
load: VideoMethods.Load load: VideoMethods.Load
loadByUUID: VideoMethods.LoadByUUID
loadAndPopulateAuthor: VideoMethods.LoadAndPopulateAuthor loadAndPopulateAuthor: VideoMethods.LoadAndPopulateAuthor
loadAndPopulateAuthorAndPodAndTags: VideoMethods.LoadAndPopulateAuthorAndPodAndTags loadAndPopulateAuthorAndPodAndTags: VideoMethods.LoadAndPopulateAuthorAndPodAndTags
loadByHostAndUUID: VideoMethods.LoadByHostAndUUID
loadByUUID: VideoMethods.LoadByUUID
loadByUUIDAndPopulateAuthorAndPodAndTags: VideoMethods.LoadByUUIDAndPopulateAuthorAndPodAndTags loadByUUIDAndPopulateAuthorAndPodAndTags: VideoMethods.LoadByUUIDAndPopulateAuthorAndPodAndTags
searchAndPopulateAuthorAndPodAndTags: VideoMethods.SearchAndPopulateAuthorAndPodAndTags searchAndPopulateAuthorAndPodAndTags: VideoMethods.SearchAndPopulateAuthorAndPodAndTags
} }
@ -118,13 +123,11 @@ export interface VideoClass {
export interface VideoAttributes { export interface VideoAttributes {
uuid?: string uuid?: string
name: string name: string
extname: string
category: number category: number
licence: number licence: number
language: number language: number
nsfw: boolean nsfw: boolean
description: string description: string
infoHash?: string
duration: number duration: number
views?: number views?: number
likes?: number likes?: number
@ -133,6 +136,7 @@ export interface VideoAttributes {
Author?: AuthorInstance Author?: AuthorInstance
Tags?: TagInstance[] Tags?: TagInstance[]
VideoFiles?: VideoFileInstance[]
} }
export interface VideoInstance extends VideoClass, VideoAttributes, Sequelize.Instance<VideoAttributes> { export interface VideoInstance extends VideoClass, VideoAttributes, Sequelize.Instance<VideoAttributes> {
@ -140,18 +144,27 @@ export interface VideoInstance extends VideoClass, VideoAttributes, Sequelize.In
createdAt: Date createdAt: Date
updatedAt: Date updatedAt: Date
createPreview: VideoMethods.CreatePreview
createThumbnail: VideoMethods.CreateThumbnail
createTorrentAndSetInfoHash: VideoMethods.CreateTorrentAndSetInfoHash
generateMagnetUri: VideoMethods.GenerateMagnetUri generateMagnetUri: VideoMethods.GenerateMagnetUri
getVideoFilename: VideoMethods.GetVideoFilename
getThumbnailName: VideoMethods.GetThumbnailName
getPreviewName: VideoMethods.GetPreviewName getPreviewName: VideoMethods.GetPreviewName
getTorrentName: VideoMethods.GetTorrentName getThumbnailName: VideoMethods.GetThumbnailName
getTorrentFileName: VideoMethods.GetTorrentFileName
getVideoFilename: VideoMethods.GetVideoFilename
getVideoFilePath: VideoMethods.GetVideoFilePath
isOwned: VideoMethods.IsOwned isOwned: VideoMethods.IsOwned
toFormatedJSON: VideoMethods.ToFormatedJSON removeFile: VideoMethods.RemoveFile
removePreview: VideoMethods.RemovePreview
removeThumbnail: VideoMethods.RemoveThumbnail
removeTorrent: VideoMethods.RemoveTorrent
toAddRemoteJSON: VideoMethods.ToAddRemoteJSON toAddRemoteJSON: VideoMethods.ToAddRemoteJSON
toFormatedJSON: VideoMethods.ToFormatedJSON
toUpdateRemoteJSON: VideoMethods.ToUpdateRemoteJSON toUpdateRemoteJSON: VideoMethods.ToUpdateRemoteJSON
transcodeVideofile: VideoMethods.TranscodeVideofile transcodeVideofile: VideoMethods.TranscodeVideofile
setTags: Sequelize.HasManySetAssociationsMixin<TagAttributes, string> setTags: Sequelize.HasManySetAssociationsMixin<TagAttributes, string>
setVideoFiles: Sequelize.HasManySetAssociationsMixin<VideoFileAttributes, string>
} }
export interface VideoModel extends VideoClass, Sequelize.Model<VideoInstance, VideoAttributes> {} export interface VideoModel extends VideoClass, Sequelize.Model<VideoInstance, VideoAttributes> {}

View File

@ -2,13 +2,12 @@ import * as safeBuffer from 'safe-buffer'
const Buffer = safeBuffer.Buffer const Buffer = safeBuffer.Buffer
import * as ffmpeg from 'fluent-ffmpeg' import * as ffmpeg from 'fluent-ffmpeg'
import * as magnetUtil from 'magnet-uri' import * as magnetUtil from 'magnet-uri'
import { map, values } from 'lodash' import { map } from 'lodash'
import * as parseTorrent from 'parse-torrent' import * as parseTorrent from 'parse-torrent'
import { join } from 'path' import { join } from 'path'
import * as Sequelize from 'sequelize' import * as Sequelize from 'sequelize'
import * as Promise from 'bluebird' import * as Promise from 'bluebird'
import { database as db } from '../../initializers/database'
import { TagInstance } from './tag-interface' import { TagInstance } from './tag-interface'
import { import {
logger, logger,
@ -18,7 +17,6 @@ import {
isVideoLanguageValid, isVideoLanguageValid,
isVideoNSFWValid, isVideoNSFWValid,
isVideoDescriptionValid, isVideoDescriptionValid,
isVideoInfoHashValid,
isVideoDurationValid, isVideoDurationValid,
readFileBufferPromise, readFileBufferPromise,
unlinkPromise, unlinkPromise,
@ -27,16 +25,17 @@ import {
createTorrentPromise createTorrentPromise
} from '../../helpers' } from '../../helpers'
import { import {
CONSTRAINTS_FIELDS,
CONFIG, CONFIG,
REMOTE_SCHEME, REMOTE_SCHEME,
STATIC_PATHS, STATIC_PATHS,
VIDEO_CATEGORIES, VIDEO_CATEGORIES,
VIDEO_LICENCES, VIDEO_LICENCES,
VIDEO_LANGUAGES, VIDEO_LANGUAGES,
THUMBNAILS_SIZE THUMBNAILS_SIZE,
VIDEO_FILE_RESOLUTIONS
} from '../../initializers' } from '../../initializers'
import { JobScheduler, removeVideoToFriends } from '../../lib' import { removeVideoToFriends } from '../../lib'
import { VideoFileInstance } from './video-file-interface'
import { addMethodsToModel, getSort } from '../utils' import { addMethodsToModel, getSort } from '../utils'
import { import {
@ -51,12 +50,16 @@ let generateMagnetUri: VideoMethods.GenerateMagnetUri
let getVideoFilename: VideoMethods.GetVideoFilename let getVideoFilename: VideoMethods.GetVideoFilename
let getThumbnailName: VideoMethods.GetThumbnailName let getThumbnailName: VideoMethods.GetThumbnailName
let getPreviewName: VideoMethods.GetPreviewName let getPreviewName: VideoMethods.GetPreviewName
let getTorrentName: VideoMethods.GetTorrentName let getTorrentFileName: VideoMethods.GetTorrentFileName
let isOwned: VideoMethods.IsOwned let isOwned: VideoMethods.IsOwned
let toFormatedJSON: VideoMethods.ToFormatedJSON let toFormatedJSON: VideoMethods.ToFormatedJSON
let toAddRemoteJSON: VideoMethods.ToAddRemoteJSON let toAddRemoteJSON: VideoMethods.ToAddRemoteJSON
let toUpdateRemoteJSON: VideoMethods.ToUpdateRemoteJSON let toUpdateRemoteJSON: VideoMethods.ToUpdateRemoteJSON
let transcodeVideofile: VideoMethods.TranscodeVideofile 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 generateThumbnailFromData: VideoMethods.GenerateThumbnailFromData
let getDurationFromFile: VideoMethods.GetDurationFromFile let getDurationFromFile: VideoMethods.GetDurationFromFile
@ -71,6 +74,10 @@ let loadAndPopulateAuthor: VideoMethods.LoadAndPopulateAuthor
let loadAndPopulateAuthorAndPodAndTags: VideoMethods.LoadAndPopulateAuthorAndPodAndTags let loadAndPopulateAuthorAndPodAndTags: VideoMethods.LoadAndPopulateAuthorAndPodAndTags
let loadByUUIDAndPopulateAuthorAndPodAndTags: VideoMethods.LoadByUUIDAndPopulateAuthorAndPodAndTags let loadByUUIDAndPopulateAuthorAndPodAndTags: VideoMethods.LoadByUUIDAndPopulateAuthorAndPodAndTags
let searchAndPopulateAuthorAndPodAndTags: VideoMethods.SearchAndPopulateAuthorAndPodAndTags 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) { export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) {
Video = sequelize.define<VideoInstance, VideoAttributes>('Video', Video = sequelize.define<VideoInstance, VideoAttributes>('Video',
@ -93,10 +100,6 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
} }
} }
}, },
extname: {
type: DataTypes.ENUM(values(CONSTRAINTS_FIELDS.VIDEOS.EXTNAME)),
allowNull: false
},
category: { category: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
allowNull: false, allowNull: false,
@ -148,16 +151,6 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
} }
} }
}, },
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: { duration: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
allowNull: false, allowNull: false,
@ -215,9 +208,6 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
{ {
fields: [ 'duration' ] fields: [ 'duration' ]
}, },
{
fields: [ 'infoHash' ]
},
{ {
fields: [ 'views' ] fields: [ 'views' ]
}, },
@ -229,8 +219,6 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
} }
], ],
hooks: { hooks: {
beforeValidate,
beforeCreate,
afterDestroy afterDestroy
} }
} }
@ -246,23 +234,30 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
listOwnedAndPopulateAuthorAndTags, listOwnedAndPopulateAuthorAndTags,
listOwnedByAuthor, listOwnedByAuthor,
load, load,
loadByUUID,
loadByHostAndUUID,
loadAndPopulateAuthor, loadAndPopulateAuthor,
loadAndPopulateAuthorAndPodAndTags, loadAndPopulateAuthorAndPodAndTags,
loadByHostAndUUID,
loadByUUID,
loadByUUIDAndPopulateAuthorAndPodAndTags, loadByUUIDAndPopulateAuthorAndPodAndTags,
searchAndPopulateAuthorAndPodAndTags, searchAndPopulateAuthorAndPodAndTags
removeFromBlacklist
] ]
const instanceMethods = [ const instanceMethods = [
createPreview,
createThumbnail,
createTorrentAndSetInfoHash,
generateMagnetUri, generateMagnetUri,
getVideoFilename,
getThumbnailName,
getPreviewName, getPreviewName,
getTorrentName, getThumbnailName,
getTorrentFileName,
getVideoFilename,
getVideoFilePath,
isOwned, isOwned,
toFormatedJSON, removeFile,
removePreview,
removeThumbnail,
removeTorrent,
toAddRemoteJSON, toAddRemoteJSON,
toFormatedJSON,
toUpdateRemoteJSON, toUpdateRemoteJSON,
transcodeVideofile transcodeVideofile
] ]
@ -271,65 +266,6 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
return Video 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 ------------------------------ // ------------------------------ METHODS ------------------------------
function associate (models) { function associate (models) {
@ -354,37 +290,46 @@ function associate (models) {
}, },
onDelete: 'cascade' onDelete: 'cascade'
}) })
Video.hasMany(models.VideoFile, {
foreignKey: {
name: 'videoId',
allowNull: false
},
onDelete: 'cascade'
})
} }
generateMagnetUri = function (this: VideoInstance) { function afterDestroy (video: VideoInstance) {
let baseUrlHttp const tasks = []
let baseUrlWs
if (this.isOwned()) { tasks.push(
baseUrlHttp = CONFIG.WEBSERVER.URL video.removeThumbnail()
baseUrlWs = CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT )
} else {
baseUrlHttp = REMOTE_SCHEME.HTTP + '://' + this.Author.Pod.host if (video.isOwned()) {
baseUrlWs = REMOTE_SCHEME.WS + '://' + this.Author.Pod.host const removeVideoToFriendsParams = {
uuid: video.uuid
}
tasks.push(
video.removePreview(),
removeVideoToFriends(removeVideoToFriendsParams)
)
// TODO: check files is populated
video.VideoFiles.forEach(file => {
video.removeFile(file),
video.removeTorrent(file)
})
} }
const xs = baseUrlHttp + STATIC_PATHS.TORRENTS + this.getTorrentName() return Promise.all(tasks)
const announce = [ baseUrlWs + '/tracker/socket' ]
const urlList = [ baseUrlHttp + STATIC_PATHS.WEBSEED + this.getVideoFilename() ]
const magnetHash = {
xs,
announce,
urlList,
infoHash: this.infoHash,
name: this.name
}
return magnetUtil.encode(magnetHash)
} }
getVideoFilename = function (this: VideoInstance) { getVideoFilename = function (this: VideoInstance, videoFile: VideoFileInstance) {
return this.uuid + this.extname // return this.uuid + '-' + VIDEO_FILE_RESOLUTIONS[videoFile.resolution] + videoFile.extname
return this.uuid + videoFile.extname
} }
getThumbnailName = function (this: VideoInstance) { getThumbnailName = function (this: VideoInstance) {
@ -398,8 +343,9 @@ getPreviewName = function (this: VideoInstance) {
return this.uuid + extension return this.uuid + extension
} }
getTorrentName = function (this: VideoInstance) { getTorrentFileName = function (this: VideoInstance, videoFile: VideoFileInstance) {
const extension = '.torrent' const extension = '.torrent'
// return this.uuid + '-' + VIDEO_FILE_RESOLUTIONS[videoFile.resolution] + extension
return this.uuid + extension return this.uuid + extension
} }
@ -407,6 +353,67 @@ isOwned = function (this: VideoInstance) {
return this.remote === false 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) { toFormatedJSON = function (this: VideoInstance) {
let podHost let podHost
@ -443,7 +450,6 @@ toFormatedJSON = function (this: VideoInstance) {
description: this.description, description: this.description,
podHost, podHost,
isLocal: this.isOwned(), isLocal: this.isOwned(),
magnetUri: this.generateMagnetUri(),
author: this.Author.name, author: this.Author.name,
duration: this.duration, duration: this.duration,
views: this.views, views: this.views,
@ -453,9 +459,24 @@ toFormatedJSON = function (this: VideoInstance) {
thumbnailPath: join(STATIC_PATHS.THUMBNAILS, this.getThumbnailName()), thumbnailPath: join(STATIC_PATHS.THUMBNAILS, this.getThumbnailName()),
previewPath: join(STATIC_PATHS.PREVIEWS, this.getPreviewName()), previewPath: join(STATIC_PATHS.PREVIEWS, this.getPreviewName()),
createdAt: this.createdAt, 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 return json
} }
@ -472,19 +493,27 @@ toAddRemoteJSON = function (this: VideoInstance) {
language: this.language, language: this.language,
nsfw: this.nsfw, nsfw: this.nsfw,
description: this.description, description: this.description,
infoHash: this.infoHash,
author: this.Author.name, author: this.Author.name,
duration: this.duration, duration: this.duration,
thumbnailData: thumbnailData.toString('binary'), thumbnailData: thumbnailData.toString('binary'),
tags: map<TagInstance, string>(this.Tags, 'name'), tags: map<TagInstance, string>(this.Tags, 'name'),
createdAt: this.createdAt, createdAt: this.createdAt,
updatedAt: this.updatedAt, updatedAt: this.updatedAt,
extname: this.extname,
views: this.views, views: this.views,
likes: this.likes, 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 return remoteVideo
}) })
} }
@ -498,28 +527,34 @@ toUpdateRemoteJSON = function (this: VideoInstance) {
language: this.language, language: this.language,
nsfw: this.nsfw, nsfw: this.nsfw,
description: this.description, description: this.description,
infoHash: this.infoHash,
author: this.Author.name, author: this.Author.name,
duration: this.duration, duration: this.duration,
tags: map<TagInstance, string>(this.Tags, 'name'), tags: map<TagInstance, string>(this.Tags, 'name'),
createdAt: this.createdAt, createdAt: this.createdAt,
updatedAt: this.updatedAt, updatedAt: this.updatedAt,
extname: this.extname,
views: this.views, views: this.views,
likes: this.likes, 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 return json
} }
transcodeVideofile = function (this: VideoInstance) { transcodeVideofile = function (this: VideoInstance, inputVideoFile: VideoFileInstance) {
const video = this
const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR
const newExtname = '.mp4' const newExtname = '.mp4'
const videoInputPath = join(videosDirectory, video.getVideoFilename()) const videoInputPath = join(videosDirectory, this.getVideoFilename(inputVideoFile))
const videoOutputPath = join(videosDirectory, video.id + '-transcoded' + newExtname) const videoOutputPath = join(videosDirectory, this.id + '-transcoded' + newExtname)
return new Promise<void>((res, rej) => { return new Promise<void>((res, rej) => {
ffmpeg(videoInputPath) ffmpeg(videoInputPath)
@ -533,24 +568,22 @@ transcodeVideofile = function (this: VideoInstance) {
return unlinkPromise(videoInputPath) return unlinkPromise(videoInputPath)
.then(() => { .then(() => {
// Important to do this before getVideoFilename() to take in account the new file extension // 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, this.getVideoFilePath(inputVideoFile))
return renamePromise(videoOutputPath, newVideoPath)
}) })
.then(() => { .then(() => {
const newVideoPath = join(videosDirectory, video.getVideoFilename()) return this.createTorrentAndSetInfoHash(inputVideoFile)
return createTorrentFromVideo(video, newVideoPath)
}) })
.then(() => { .then(() => {
return video.save() return inputVideoFile.save()
}) })
.then(() => { .then(() => {
return res() return res()
}) })
.catch(err => { .catch(err => {
// Autodesctruction... // Autodestruction...
video.destroy().catch(err => logger.error('Cannot destruct video after transcoding failure.', err)) this.destroy().catch(err => logger.error('Cannot destruct video after transcoding failure.', err))
return rej(err) return rej(err)
}) })
@ -559,6 +592,26 @@ transcodeVideofile = function (this: VideoInstance) {
}) })
} }
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 ------------------------------ // ------------------------------ STATICS ------------------------------
generateThumbnailFromData = function (video: VideoInstance, thumbnailData: string) { generateThumbnailFromData = function (video: VideoInstance, thumbnailData: string) {
@ -582,7 +635,11 @@ getDurationFromFile = function (videoPath: string) {
} }
list = function () { list = function () {
return Video.findAll() const query = {
include: [ Video['sequelize'].models.VideoFile ]
}
return Video.findAll(query)
} }
listForApi = function (start: number, count: number, sort: string) { listForApi = function (start: number, count: number, sort: string) {
@ -597,8 +654,8 @@ listForApi = function (start: number, count: number, sort: string) {
model: Video['sequelize'].models.Author, model: Video['sequelize'].models.Author,
include: [ { model: Video['sequelize'].models.Pod, required: false } ] include: [ { model: Video['sequelize'].models.Pod, required: false } ]
}, },
Video['sequelize'].models.Tag,
Video['sequelize'].models.Tag Video['sequelize'].models.VideoFile
], ],
where: createBaseVideosWhere() where: createBaseVideosWhere()
} }
@ -617,6 +674,9 @@ loadByHostAndUUID = function (fromHost: string, uuid: string) {
uuid uuid
}, },
include: [ include: [
{
model: Video['sequelize'].models.VideoFile
},
{ {
model: Video['sequelize'].models.Author, model: Video['sequelize'].models.Author,
include: [ include: [
@ -640,7 +700,11 @@ listOwnedAndPopulateAuthorAndTags = function () {
where: { where: {
remote: false 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) return Video.findAll(query)
@ -652,6 +716,9 @@ listOwnedByAuthor = function (author: string) {
remote: false remote: false
}, },
include: [ include: [
{
model: Video['sequelize'].models.VideoFile
},
{ {
model: Video['sequelize'].models.Author, model: Video['sequelize'].models.Author,
where: { where: {
@ -672,14 +739,15 @@ loadByUUID = function (uuid: string) {
const query = { const query = {
where: { where: {
uuid uuid
} },
include: [ Video['sequelize'].models.VideoFile ]
} }
return Video.findOne(query) return Video.findOne(query)
} }
loadAndPopulateAuthor = function (id: number) { loadAndPopulateAuthor = function (id: number) {
const options = { const options = {
include: [ Video['sequelize'].models.Author ] include: [ Video['sequelize'].models.VideoFile, Video['sequelize'].models.Author ]
} }
return Video.findById(id, options) return Video.findById(id, options)
@ -692,7 +760,8 @@ loadAndPopulateAuthorAndPodAndTags = function (id: number) {
model: Video['sequelize'].models.Author, model: Video['sequelize'].models.Author,
include: [ { model: Video['sequelize'].models.Pod, required: false } ] include: [ { model: Video['sequelize'].models.Pod, required: false } ]
}, },
Video['sequelize'].models.Tag Video['sequelize'].models.Tag,
Video['sequelize'].models.VideoFile
] ]
} }
@ -709,7 +778,8 @@ loadByUUIDAndPopulateAuthorAndPodAndTags = function (uuid: string) {
model: Video['sequelize'].models.Author, model: Video['sequelize'].models.Author,
include: [ { model: Video['sequelize'].models.Pod, required: false } ] include: [ { model: Video['sequelize'].models.Pod, required: false } ]
}, },
Video['sequelize'].models.Tag Video['sequelize'].models.Tag,
Video['sequelize'].models.VideoFile
] ]
} }
@ -733,6 +803,10 @@ searchAndPopulateAuthorAndPodAndTags = function (value: string, field: string, s
model: Video['sequelize'].models.Tag model: Video['sequelize'].models.Tag
} }
const videoFileInclude: Sequelize.IncludeOptions = {
model: Video['sequelize'].models.VideoFile
}
const query: Sequelize.FindOptions = { const query: Sequelize.FindOptions = {
distinct: true, distinct: true,
where: createBaseVideosWhere(), where: createBaseVideosWhere(),
@ -743,8 +817,9 @@ searchAndPopulateAuthorAndPodAndTags = function (value: string, field: string, s
// Make an exact search with the magnet // Make an exact search with the magnet
if (field === 'magnetUri') { if (field === 'magnetUri') {
const infoHash = magnetUtil.decode(value).infoHash videoFileInclude.where = {
query.where['infoHash'] = infoHash infoHash: magnetUtil.decode(value).infoHash
}
} else if (field === 'tags') { } else if (field === 'tags') {
const escapedValue = Video['sequelize'].escape('%' + value + '%') const escapedValue = Video['sequelize'].escape('%' + value + '%')
query.where['id'].$in = Video['sequelize'].literal( query.where['id'].$in = Video['sequelize'].literal(
@ -777,7 +852,7 @@ searchAndPopulateAuthorAndPodAndTags = function (value: string, field: string, s
} }
query.include = [ query.include = [
authorInclude, tagInclude authorInclude, tagInclude, videoFileInclude
] ]
return Video.findAndCountAll(query).then(({ rows, count }) => { return Video.findAndCountAll(query).then(({ rows, count }) => {
@ -800,56 +875,6 @@ function createBaseVideosWhere () {
} }
} }
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) { function generateImage (video: VideoInstance, videoPath: string, folder: string, imageName: string, size: string) {
const options = { const options = {
filename: imageName, filename: imageName,
@ -868,16 +893,3 @@ function generateImage (video: VideoInstance, videoPath: string, folder: string,
.thumbnail(options) .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()
})
}

View File

@ -121,13 +121,21 @@ describe('Test multiple pods', function () {
expect(video.nsfw).to.be.ok expect(video.nsfw).to.be.ok
expect(video.description).to.equal('my super description for pod 1') expect(video.description).to.equal('my super description for pod 1')
expect(video.podHost).to.equal('localhost:9001') expect(video.podHost).to.equal('localhost:9001')
expect(video.magnetUri).to.exist
expect(video.duration).to.equal(10) expect(video.duration).to.equal(10)
expect(video.tags).to.deep.equal([ 'tag1p1', 'tag2p1' ]) expect(video.tags).to.deep.equal([ 'tag1p1', 'tag2p1' ])
expect(miscsUtils.dateIsValid(video.createdAt)).to.be.true expect(miscsUtils.dateIsValid(video.createdAt)).to.be.true
expect(miscsUtils.dateIsValid(video.updatedAt)).to.be.true expect(miscsUtils.dateIsValid(video.updatedAt)).to.be.true
expect(video.author).to.equal('root') 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') { if (server.url !== 'http://localhost:9001') {
expect(video.isLocal).to.be.false expect(video.isLocal).to.be.false
} else { } else {
@ -136,9 +144,9 @@ describe('Test multiple pods', function () {
// All pods should have the same magnet Uri // All pods should have the same magnet Uri
if (baseMagnet === null) { if (baseMagnet === null) {
baseMagnet = video.magnetUri baseMagnet = magnetUri
} else { } else {
expect(video.magnetUri).to.equal.magnetUri expect(baseMagnet).to.equal(magnetUri)
} }
videosUtils.testVideoImage(server.url, 'video_short1.webm', video.thumbnailPath, function (err, test) { videosUtils.testVideoImage(server.url, 'video_short1.webm', video.thumbnailPath, function (err, test) {
@ -198,13 +206,21 @@ describe('Test multiple pods', function () {
expect(video.nsfw).to.be.true expect(video.nsfw).to.be.true
expect(video.description).to.equal('my super description for pod 2') expect(video.description).to.equal('my super description for pod 2')
expect(video.podHost).to.equal('localhost:9002') expect(video.podHost).to.equal('localhost:9002')
expect(video.magnetUri).to.exist
expect(video.duration).to.equal(5) expect(video.duration).to.equal(5)
expect(video.tags).to.deep.equal([ 'tag1p2', 'tag2p2', 'tag3p2' ]) expect(video.tags).to.deep.equal([ 'tag1p2', 'tag2p2', 'tag3p2' ])
expect(miscsUtils.dateIsValid(video.createdAt)).to.be.true expect(miscsUtils.dateIsValid(video.createdAt)).to.be.true
expect(miscsUtils.dateIsValid(video.updatedAt)).to.be.true expect(miscsUtils.dateIsValid(video.updatedAt)).to.be.true
expect(video.author).to.equal('root') 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') { if (server.url !== 'http://localhost:9002') {
expect(video.isLocal).to.be.false expect(video.isLocal).to.be.false
} else { } else {
@ -213,9 +229,9 @@ describe('Test multiple pods', function () {
// All pods should have the same magnet Uri // All pods should have the same magnet Uri
if (baseMagnet === null) { if (baseMagnet === null) {
baseMagnet = video.magnetUri baseMagnet = magnetUri
} else { } else {
expect(video.magnetUri).to.equal.magnetUri expect(baseMagnet).to.equal(magnetUri)
} }
videosUtils.testVideoImage(server.url, 'video_short2.webm', video.thumbnailPath, function (err, test) { videosUtils.testVideoImage(server.url, 'video_short2.webm', video.thumbnailPath, function (err, test) {
@ -297,13 +313,21 @@ describe('Test multiple pods', function () {
expect(video1.nsfw).to.be.ok expect(video1.nsfw).to.be.ok
expect(video1.description).to.equal('my super description for pod 3') expect(video1.description).to.equal('my super description for pod 3')
expect(video1.podHost).to.equal('localhost:9003') expect(video1.podHost).to.equal('localhost:9003')
expect(video1.magnetUri).to.exist
expect(video1.duration).to.equal(5) expect(video1.duration).to.equal(5)
expect(video1.tags).to.deep.equal([ 'tag1p3' ]) expect(video1.tags).to.deep.equal([ 'tag1p3' ])
expect(video1.author).to.equal('root') expect(video1.author).to.equal('root')
expect(miscsUtils.dateIsValid(video1.createdAt)).to.be.true expect(miscsUtils.dateIsValid(video1.createdAt)).to.be.true
expect(miscsUtils.dateIsValid(video1.updatedAt)).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.name).to.equal('my super name for pod 3-2')
expect(video2.category).to.equal(7) expect(video2.category).to.equal(7)
expect(video2.categoryLabel).to.equal('Gaming') expect(video2.categoryLabel).to.equal('Gaming')
@ -314,13 +338,21 @@ describe('Test multiple pods', function () {
expect(video2.nsfw).to.be.false expect(video2.nsfw).to.be.false
expect(video2.description).to.equal('my super description for pod 3-2') expect(video2.description).to.equal('my super description for pod 3-2')
expect(video2.podHost).to.equal('localhost:9003') expect(video2.podHost).to.equal('localhost:9003')
expect(video2.magnetUri).to.exist
expect(video2.duration).to.equal(5) expect(video2.duration).to.equal(5)
expect(video2.tags).to.deep.equal([ 'tag2p3', 'tag3p3', 'tag4p3' ]) expect(video2.tags).to.deep.equal([ 'tag2p3', 'tag3p3', 'tag4p3' ])
expect(video2.author).to.equal('root') expect(video2.author).to.equal('root')
expect(miscsUtils.dateIsValid(video2.createdAt)).to.be.true expect(miscsUtils.dateIsValid(video2.createdAt)).to.be.true
expect(miscsUtils.dateIsValid(video2.updatedAt)).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') { if (server.url !== 'http://localhost:9003') {
expect(video1.isLocal).to.be.false expect(video1.isLocal).to.be.false
expect(video2.isLocal).to.be.false expect(video2.isLocal).to.be.false
@ -331,9 +363,9 @@ describe('Test multiple pods', function () {
// All pods should have the same magnet Uri // All pods should have the same magnet Uri
if (baseMagnet === null) { if (baseMagnet === null) {
baseMagnet = video2.magnetUri baseMagnet = magnetUri2
} else { } else {
expect(video2.magnetUri).to.equal.magnetUri expect(baseMagnet).to.equal(magnetUri2)
} }
videosUtils.testVideoImage(server.url, 'video_short3.webm', video1.thumbnailPath, function (err, test) { videosUtils.testVideoImage(server.url, 'video_short3.webm', video1.thumbnailPath, function (err, test) {
@ -366,7 +398,7 @@ describe('Test multiple pods', function () {
toRemove.push(res.body.data[2]) toRemove.push(res.body.data[2])
toRemove.push(res.body.data[3]) 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).to.exist
expect(torrent.files.length).to.equal(1) expect(torrent.files.length).to.equal(1)
expect(torrent.files[0].path).to.exist.and.to.not.equal('') expect(torrent.files[0].path).to.exist.and.to.not.equal('')
@ -385,7 +417,7 @@ describe('Test multiple pods', function () {
const video = res.body.data[1] 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).to.exist
expect(torrent.files.length).to.equal(1) expect(torrent.files.length).to.equal(1)
expect(torrent.files[0].path).to.exist.and.to.not.equal('') expect(torrent.files[0].path).to.exist.and.to.not.equal('')
@ -404,7 +436,7 @@ describe('Test multiple pods', function () {
const video = res.body.data[2] 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).to.exist
expect(torrent.files.length).to.equal(1) expect(torrent.files.length).to.equal(1)
expect(torrent.files[0].path).to.exist.and.to.not.equal('') expect(torrent.files[0].path).to.exist.and.to.not.equal('')
@ -423,7 +455,7 @@ describe('Test multiple pods', function () {
const video = res.body.data[3] 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).to.exist
expect(torrent.files.length).to.equal(1) expect(torrent.files.length).to.equal(1)
expect(torrent.files[0].path).to.exist.and.to.not.equal('') expect(torrent.files[0].path).to.exist.and.to.not.equal('')
@ -700,11 +732,18 @@ describe('Test multiple pods', function () {
expect(videoUpdated.tags).to.deep.equal([ 'tagup1', 'tagup2' ]) expect(videoUpdated.tags).to.deep.equal([ 'tagup1', 'tagup2' ])
expect(miscsUtils.dateIsValid(videoUpdated.updatedAt, 20000)).to.be.true 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) { videosUtils.testVideoImage(server.url, 'video_short3.webm', videoUpdated.thumbnailPath, function (err, test) {
if (err) throw err if (err) throw err
expect(test).to.equal(true) 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).to.exist
expect(torrent.files.length).to.equal(1) expect(torrent.files.length).to.equal(1)
expect(torrent.files[0].path).to.exist.and.to.not.equal('') expect(torrent.files[0].path).to.exist.and.to.not.equal('')

View File

@ -129,13 +129,21 @@ describe('Test a single pod', function () {
expect(video.nsfw).to.be.ok expect(video.nsfw).to.be.ok
expect(video.description).to.equal('my super description') expect(video.description).to.equal('my super description')
expect(video.podHost).to.equal('localhost:9001') expect(video.podHost).to.equal('localhost:9001')
expect(video.magnetUri).to.exist
expect(video.author).to.equal('root') expect(video.author).to.equal('root')
expect(video.isLocal).to.be.true expect(video.isLocal).to.be.true
expect(video.tags).to.deep.equal([ 'tag1', 'tag2', 'tag3' ]) expect(video.tags).to.deep.equal([ 'tag1', 'tag2', 'tag3' ])
expect(miscsUtils.dateIsValid(video.createdAt)).to.be.true expect(miscsUtils.dateIsValid(video.createdAt)).to.be.true
expect(miscsUtils.dateIsValid(video.updatedAt)).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) { videosUtils.testVideoImage(server.url, 'video_short.webm', video.thumbnailPath, function (err, test) {
if (err) throw err if (err) throw err
expect(test).to.equal(true) expect(test).to.equal(true)
@ -143,7 +151,7 @@ describe('Test a single pod', function () {
videoId = video.id videoId = video.id
videoUUID = video.uuid videoUUID = video.uuid
webtorrent.add(video.magnetUri, function (torrent) { webtorrent.add(magnetUri, function (torrent) {
expect(torrent.files).to.exist expect(torrent.files).to.exist
expect(torrent.files.length).to.equal(1) expect(torrent.files.length).to.equal(1)
expect(torrent.files[0].path).to.exist.and.to.not.equal('') expect(torrent.files[0].path).to.exist.and.to.not.equal('')
@ -172,13 +180,21 @@ describe('Test a single pod', function () {
expect(video.nsfw).to.be.ok expect(video.nsfw).to.be.ok
expect(video.description).to.equal('my super description') expect(video.description).to.equal('my super description')
expect(video.podHost).to.equal('localhost:9001') expect(video.podHost).to.equal('localhost:9001')
expect(video.magnetUri).to.exist
expect(video.author).to.equal('root') expect(video.author).to.equal('root')
expect(video.isLocal).to.be.true expect(video.isLocal).to.be.true
expect(video.tags).to.deep.equal([ 'tag1', 'tag2', 'tag3' ]) expect(video.tags).to.deep.equal([ 'tag1', 'tag2', 'tag3' ])
expect(miscsUtils.dateIsValid(video.createdAt)).to.be.true expect(miscsUtils.dateIsValid(video.createdAt)).to.be.true
expect(miscsUtils.dateIsValid(video.updatedAt)).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) { videosUtils.testVideoImage(server.url, 'video_short.webm', video.thumbnailPath, function (err, test) {
if (err) throw err if (err) throw err
expect(test).to.equal(true) expect(test).to.equal(true)
@ -240,6 +256,15 @@ describe('Test a single pod', function () {
expect(miscsUtils.dateIsValid(video.createdAt)).to.be.true expect(miscsUtils.dateIsValid(video.createdAt)).to.be.true
expect(miscsUtils.dateIsValid(video.updatedAt)).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) { videosUtils.testVideoImage(server.url, 'video_short.webm', video.thumbnailPath, function (err, test) {
if (err) throw err if (err) throw err
expect(test).to.equal(true) expect(test).to.equal(true)
@ -302,6 +327,15 @@ describe('Test a single pod', function () {
expect(miscsUtils.dateIsValid(video.createdAt)).to.be.true expect(miscsUtils.dateIsValid(video.createdAt)).to.be.true
expect(miscsUtils.dateIsValid(video.updatedAt)).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) { videosUtils.testVideoImage(server.url, 'video_short.webm', video.thumbnailPath, function (err, test) {
if (err) throw err if (err) throw err
expect(test).to.equal(true) expect(test).to.equal(true)
@ -564,7 +598,7 @@ describe('Test a single pod', function () {
it('Should search the right magnetUri video', function (done) { it('Should search the right magnetUri video', function (done) {
const video = videosListBase[0] 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 if (err) throw err
const videos = res.body.data const videos = res.body.data
@ -650,11 +684,20 @@ describe('Test a single pod', function () {
expect(miscsUtils.dateIsValid(video.createdAt)).to.be.true expect(miscsUtils.dateIsValid(video.createdAt)).to.be.true
expect(miscsUtils.dateIsValid(video.updatedAt)).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) { videosUtils.testVideoImage(server.url, 'video_short3.webm', video.thumbnailPath, function (err, test) {
if (err) throw err if (err) throw err
expect(test).to.equal(true) expect(test).to.equal(true)
webtorrent.add(video.magnetUri, function (torrent) { webtorrent.add(magnetUri, function (torrent) {
expect(torrent.files).to.exist expect(torrent.files).to.exist
expect(torrent.files.length).to.equal(1) expect(torrent.files.length).to.equal(1)
expect(torrent.files[0].path).to.exist.and.to.not.equal('') expect(torrent.files[0].path).to.exist.and.to.not.equal('')
@ -694,6 +737,15 @@ describe('Test a single pod', function () {
expect(miscsUtils.dateIsValid(video.createdAt)).to.be.true expect(miscsUtils.dateIsValid(video.createdAt)).to.be.true
expect(miscsUtils.dateIsValid(video.updatedAt)).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() done()
}) })
}) })
@ -728,6 +780,15 @@ describe('Test a single pod', function () {
expect(miscsUtils.dateIsValid(video.createdAt)).to.be.true expect(miscsUtils.dateIsValid(video.createdAt)).to.be.true
expect(miscsUtils.dateIsValid(video.updatedAt)).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() done()
}) })
}) })

View File

@ -56,9 +56,10 @@ describe('Test video transcoding', function () {
if (err) throw err if (err) throw err
const video = res.body.data[0] 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).to.exist
expect(torrent.files.length).to.equal(1) expect(torrent.files.length).to.equal(1)
expect(torrent.files[0].path).match(/\.webm$/) expect(torrent.files[0].path).match(/\.webm$/)
@ -86,9 +87,10 @@ describe('Test video transcoding', function () {
if (err) throw err if (err) throw err
const video = res.body.data[0] 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).to.exist
expect(torrent.files.length).to.equal(1) expect(torrent.files.length).to.equal(1)
expect(torrent.files[0].path).match(/\.mp4$/) expect(torrent.files[0].path).match(/\.mp4$/)

View File

@ -5,8 +5,6 @@ export interface RemoteVideoCreateData {
author: string author: string
tags: string[] tags: string[]
name: string name: string
extname: string
infoHash: string
category: number category: number
licence: number licence: number
language: number language: number
@ -19,6 +17,12 @@ export interface RemoteVideoCreateData {
likes: number likes: number
dislikes: number dislikes: number
thumbnailData: string thumbnailData: string
files: {
infoHash: string
extname: string
resolution: number
size: number
}[]
} }
export interface RemoteVideoCreateRequest extends RemoteVideoRequest { export interface RemoteVideoCreateRequest extends RemoteVideoRequest {

View File

@ -15,6 +15,12 @@ export interface RemoteVideoUpdateData {
views: number views: number
likes: number likes: number
dislikes: number dislikes: number
files: {
infoHash: string
extname: string
resolution: number
size: number
}[]
} }
export interface RemoteVideoUpdateRequest { export interface RemoteVideoUpdateRequest {

View File

@ -1,3 +1,10 @@
export interface VideoFile {
magnetUri: string
resolution: number
resolutionLabel: string
size: number // Bytes
}
export interface Video { export interface Video {
id: number id: number
uuid: string uuid: string
@ -12,7 +19,6 @@ export interface Video {
description: string description: string
duration: number duration: number
isLocal: boolean isLocal: boolean
magnetUri: string
name: string name: string
podHost: string podHost: string
tags: string[] tags: string[]
@ -22,4 +28,5 @@ export interface Video {
likes: number likes: number
dislikes: number dislikes: number
nsfw: boolean nsfw: boolean
files: VideoFile[]
} }