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
/dist
/.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'
export class Video implements VideoServerModel {
@ -17,7 +17,6 @@ export class Video implements VideoServerModel {
id: number
uuid: string
isLocal: boolean
magnetUri: string
name: string
podHost: string
tags: string[]
@ -29,6 +28,7 @@ export class Video implements VideoServerModel {
likes: number
dislikes: number
nsfw: boolean
files: VideoFile[]
private static createByString (author: string, podHost: string) {
return author + '@' + podHost
@ -57,7 +57,6 @@ export class Video implements VideoServerModel {
id: number,
uuid: string,
isLocal: boolean,
magnetUri: string,
name: string,
podHost: string,
tags: string[],
@ -66,7 +65,8 @@ export class Video implements VideoServerModel {
views: number,
likes: number,
dislikes: number,
nsfw: boolean
nsfw: boolean,
files: VideoFile[]
}) {
this.author = hash.author
this.createdAt = new Date(hash.createdAt)
@ -82,7 +82,6 @@ export class Video implements VideoServerModel {
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
@ -94,6 +93,7 @@ export class Video implements VideoServerModel {
this.likes = hash.likes
this.dislikes = hash.dislikes
this.nsfw = hash.nsfw
this.files = hash.files
this.by = Video.createByString(hash.author, hash.podHost)
}
@ -115,6 +115,13 @@ export class Video implements VideoServerModel {
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]
@ -132,7 +139,6 @@ export class Video implements VideoServerModel {
duration: this.duration,
id: this.id,
isLocal: this.isLocal,
magnetUri: this.magnetUri,
name: this.name,
podHost: this.podHost,
tags: this.tags,
@ -140,7 +146,8 @@ export class Video implements VideoServerModel {
views: this.views,
likes: this.likes,
dislikes: this.dislikes,
nsfw: this.nsfw
nsfw: this.nsfw,
files: this.files
}
}
}

View File

@ -10,7 +10,7 @@
</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>

View File

@ -90,8 +90,8 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
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
@ -108,13 +108,13 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
// 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
@ -123,7 +123,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
// 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.')

View File

@ -57,7 +57,11 @@ loadVideoInfos(videoId, (err, videoInfos) => {
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

View File

@ -30,6 +30,7 @@
"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",

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 { join } from 'path'
import * as parseTorrent from 'parse-torrent'
import { CONFIG, STATIC_PATHS } from '../server/initializers/constants'
@ -19,17 +20,10 @@ db.init(true)
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)

View File

@ -26,7 +26,7 @@ const app = express()
// ----------- 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())
@ -57,10 +57,20 @@ import { apiRouter, clientsRouter, staticRouter } from './server/controllers'
// 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

View File

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

View File

@ -1,7 +1,7 @@
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 {
@ -16,7 +16,8 @@ import {
addEventToRemoteVideo,
quickAndDirtyUpdateVideoToFriends,
addVideoToFriends,
updateVideoToFriends
updateVideoToFriends,
JobScheduler
} from '../../../lib'
import {
authenticate,
@ -155,7 +156,7 @@ function addVideoRetryWrapper (req: express.Request, res: express.Response, next
.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 => {
@ -177,13 +178,13 @@ function addVideo (req: express.Request, res: express.Response, videoFile: Expre
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
}
@ -191,18 +192,50 @@ function addVideo (req: express.Request, res: express.Response, videoFile: Expre
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)
@ -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
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
@ -236,7 +277,7 @@ function addVideo (req: express.Request, res: express.Response, videoFile: Expre
})
.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
})
}

View File

@ -23,10 +23,11 @@ import {
isVideoNSFWValid,
isVideoDescriptionValid,
isVideoDurationValid,
isVideoInfoHashValid,
isVideoFileInfoHashValid,
isVideoNameValid,
isVideoTagsValid,
isVideoExtnameValid
isVideoFileExtnameValid,
isVideoFileResolutionValid
} from '../videos'
const ENDPOINT_ACTIONS = REQUEST_ENDPOINT_ACTIONS[REQUEST_ENDPOINTS.VIDEOS]
@ -121,14 +122,22 @@ function isCommonVideoAttributesValid (video: any) {
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) {

View File

@ -7,7 +7,8 @@ import {
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'
@ -53,14 +54,6 @@ function isVideoDurationValid (value: string) {
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)
}
@ -128,6 +121,22 @@ function isVideoFile (value: string, files: { [ fieldname: string ]: Express.Mul
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 {
@ -140,12 +149,12 @@ export {
isVideoNSFWValid,
isVideoDescriptionValid,
isVideoDurationValid,
isVideoInfoHashValid,
isVideoFileInfoHashValid,
isVideoNameValid,
isVideoTagsValid,
isVideoThumbnailValid,
isVideoThumbnailDataValid,
isVideoExtnameValid,
isVideoFileExtnameValid,
isVideoUUIDValid,
isVideoAbuseReasonValid,
isVideoAbuseReporterUsernameValid,
@ -154,7 +163,9 @@ export {
isVideoLikesValid,
isVideoRatingTypeValid,
isVideoDislikesValid,
isVideoEventCountValid
isVideoEventCountValid,
isVideoFileSizeValid,
isVideoFileResolutionValid
}
declare global {
@ -183,7 +194,9 @@ declare global {
isVideoLikesValid,
isVideoRatingTypeValid,
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
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 }
@ -176,6 +177,14 @@ const VIDEO_LANGUAGES = {
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
@ -362,6 +371,7 @@ export {
THUMBNAILS_SIZE,
USER_ROLES,
VIDEO_CATEGORIES,
VIDEO_FILE_RESOLUTIONS,
VIDEO_LANGUAGES,
VIDEO_LICENCES,
VIDEO_RATE_TYPES

View File

@ -23,6 +23,7 @@ import {
UserVideoRateModel,
VideoAbuseModel,
BlacklistedVideoModel,
VideoFileModel,
VideoTagModel,
VideoModel
} from '../models'
@ -49,6 +50,7 @@ const database: {
UserVideoRate?: UserVideoRateModel,
User?: UserModel,
VideoAbuse?: VideoAbuseModel,
VideoFile?: VideoFileModel,
BlacklistedVideo?: BlacklistedVideoModel,
VideoTag?: VideoTagModel,
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
}[] = []
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
})
@ -93,7 +95,8 @@ function executeMigration (actualVersion: number, entity: { version: string, scr
const options = {
transaction: t,
queryInterface: db.sequelize.getQueryInterface(),
sequelize: db.sequelize
sequelize: db.sequelize,
db
}
return migrationScript.up(options)

View File

@ -5,7 +5,9 @@ import { VideoInstance } from '../../../models'
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)
})
}

View File

@ -3,4 +3,5 @@ export * from './tag-interface'
export * from './video-abuse-interface'
export * from './video-blacklist-interface'
export * from './video-tag-interface'
export * from './video-file-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 { 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
@ -16,17 +24,16 @@ export type FormatedAddRemoteVideo = {
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 = {
@ -37,31 +44,35 @@ 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>
@ -86,31 +97,25 @@ export namespace VideoMethods {
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
}
@ -118,13 +123,11 @@ export interface VideoClass {
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
@ -133,6 +136,7 @@ export interface VideoAttributes {
Author?: AuthorInstance
Tags?: TagInstance[]
VideoFiles?: VideoFileInstance[]
}
export interface VideoInstance extends VideoClass, VideoAttributes, Sequelize.Instance<VideoAttributes> {
@ -140,18 +144,27 @@ export interface VideoInstance extends VideoClass, VideoAttributes, Sequelize.In
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> {}

View File

@ -2,13 +2,12 @@ import * as safeBuffer from 'safe-buffer'
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,
@ -18,7 +17,6 @@ import {
isVideoLanguageValid,
isVideoNSFWValid,
isVideoDescriptionValid,
isVideoInfoHashValid,
isVideoDurationValid,
readFileBufferPromise,
unlinkPromise,
@ -27,16 +25,17 @@ import {
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 {
@ -51,12 +50,16 @@ let generateMagnetUri: VideoMethods.GenerateMagnetUri
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
@ -71,6 +74,10 @@ let loadAndPopulateAuthor: VideoMethods.LoadAndPopulateAuthor
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',
@ -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: {
type: DataTypes.INTEGER,
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: {
type: DataTypes.INTEGER,
allowNull: false,
@ -215,9 +208,6 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
{
fields: [ 'duration' ]
},
{
fields: [ 'infoHash' ]
},
{
fields: [ 'views' ]
},
@ -229,8 +219,6 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
}
],
hooks: {
beforeValidate,
beforeCreate,
afterDestroy
}
}
@ -246,23 +234,30 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
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
]
@ -271,65 +266,6 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
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) {
@ -354,37 +290,46 @@ 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()
)
if (video.isOwned()) {
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()
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)
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) {
@ -398,8 +343,9 @@ getPreviewName = 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
}
@ -407,6 +353,67 @@ isOwned = function (this: VideoInstance) {
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
@ -443,7 +450,6 @@ toFormatedJSON = function (this: VideoInstance) {
description: this.description,
podHost,
isLocal: this.isOwned(),
magnetUri: this.generateMagnetUri(),
author: this.Author.name,
duration: this.duration,
views: this.views,
@ -453,9 +459,24 @@ toFormatedJSON = function (this: VideoInstance) {
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
}
@ -472,19 +493,27 @@ toAddRemoteJSON = function (this: VideoInstance) {
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
})
}
@ -498,28 +527,34 @@ toUpdateRemoteJSON = function (this: VideoInstance) {
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)
@ -533,24 +568,22 @@ transcodeVideofile = function (this: VideoInstance) {
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)
})
@ -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 ------------------------------
generateThumbnailFromData = function (video: VideoInstance, thumbnailData: string) {
@ -582,7 +635,11 @@ getDurationFromFile = function (videoPath: 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) {
@ -597,8 +654,8 @@ 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()
}
@ -617,6 +674,9 @@ loadByHostAndUUID = function (fromHost: string, uuid: string) {
uuid
},
include: [
{
model: Video['sequelize'].models.VideoFile
},
{
model: Video['sequelize'].models.Author,
include: [
@ -640,7 +700,11 @@ listOwnedAndPopulateAuthorAndTags = function () {
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)
@ -652,6 +716,9 @@ listOwnedByAuthor = function (author: string) {
remote: false
},
include: [
{
model: Video['sequelize'].models.VideoFile
},
{
model: Video['sequelize'].models.Author,
where: {
@ -672,14 +739,15 @@ loadByUUID = function (uuid: string) {
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)
@ -692,7 +760,8 @@ loadAndPopulateAuthorAndPodAndTags = function (id: number) {
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
]
}
@ -709,7 +778,8 @@ loadByUUIDAndPopulateAuthorAndPodAndTags = function (uuid: 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
]
}
@ -733,6 +803,10 @@ searchAndPopulateAuthorAndPodAndTags = function (value: string, field: string, s
model: Video['sequelize'].models.Tag
}
const videoFileInclude: Sequelize.IncludeOptions = {
model: Video['sequelize'].models.VideoFile
}
const query: Sequelize.FindOptions = {
distinct: true,
where: createBaseVideosWhere(),
@ -743,8 +817,9 @@ searchAndPopulateAuthorAndPodAndTags = function (value: string, field: string, s
// 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(
@ -777,7 +852,7 @@ searchAndPopulateAuthorAndPodAndTags = function (value: string, field: string, s
}
query.include = [
authorInclude, tagInclude
authorInclude, tagInclude, videoFileInclude
]
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) {
const options = {
filename: imageName,
@ -868,16 +893,3 @@ function generateImage (video: VideoInstance, videoPath: string, folder: string,
.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.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 {
@ -136,9 +144,9 @@ describe('Test multiple pods', function () {
// 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) {
@ -198,13 +206,21 @@ describe('Test multiple pods', function () {
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 {
@ -213,9 +229,9 @@ describe('Test multiple pods', function () {
// 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) {
@ -297,13 +313,21 @@ describe('Test multiple pods', function () {
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')
@ -314,13 +338,21 @@ describe('Test multiple pods', function () {
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
@ -331,9 +363,9 @@ describe('Test multiple pods', function () {
// 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) {
@ -366,7 +398,7 @@ describe('Test multiple pods', function () {
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('')
@ -385,7 +417,7 @@ describe('Test multiple pods', function () {
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('')
@ -404,7 +436,7 @@ describe('Test multiple pods', function () {
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('')
@ -423,7 +455,7 @@ describe('Test multiple pods', function () {
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('')
@ -700,11 +732,18 @@ describe('Test multiple pods', function () {
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('')

View File

@ -129,13 +129,21 @@ describe('Test a single pod', function () {
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)
@ -143,7 +151,7 @@ describe('Test a single pod', function () {
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('')
@ -172,13 +180,21 @@ describe('Test a single pod', function () {
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)
@ -240,6 +256,15 @@ describe('Test a single pod', function () {
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)
@ -302,6 +327,15 @@ describe('Test a single pod', function () {
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)
@ -564,7 +598,7 @@ describe('Test a single pod', function () {
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
@ -650,11 +684,20 @@ describe('Test a single pod', function () {
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('')
@ -694,6 +737,15 @@ describe('Test a single pod', function () {
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()
})
})
@ -728,6 +780,15 @@ describe('Test a single pod', function () {
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()
})
})

View File

@ -56,9 +56,10 @@ describe('Test video transcoding', function () {
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$/)
@ -86,9 +87,10 @@ describe('Test video transcoding', function () {
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$/)

View File

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

View File

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

View File

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