Import torrents with webtorrent
This commit is contained in:
parent
ce33919c24
commit
990b6a0b0c
|
@ -5,7 +5,7 @@
|
||||||
<ng-template pTemplate="header">
|
<ng-template pTemplate="header">
|
||||||
<tr>
|
<tr>
|
||||||
<th style="width: 40px;"></th>
|
<th style="width: 40px;"></th>
|
||||||
<th i18n>URL</th>
|
<th i18n>Target</th>
|
||||||
<th i18n>Video</th>
|
<th i18n>Video</th>
|
||||||
<th i18n style="width: 150px">State</th>
|
<th i18n style="width: 150px">State</th>
|
||||||
<th i18n pSortableColumn="createdAt">Created <p-sortIcon field="createdAt"></p-sortIcon></th>
|
<th i18n pSortableColumn="createdAt">Created <p-sortIcon field="createdAt"></p-sortIcon></th>
|
||||||
|
@ -22,7 +22,10 @@
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td>
|
<td>
|
||||||
<a [href]="videoImport.targetUrl" target="_blank" rel="noopener noreferrer">{{ videoImport.targetUrl }}</a>
|
<a *ngIf="videoImport.targetUrl; else torrent" [href]="videoImport.targetUrl" target="_blank" rel="noopener noreferrer">{{ videoImport.targetUrl }}</a>
|
||||||
|
<ng-template #torrent>
|
||||||
|
<span [title]="videoImport.torrentName || videoImport.magnetUri">{{ videoImport.torrentName || videoImport.magnetUri }}</span>
|
||||||
|
</ng-template>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td *ngIf="isVideoImportPending(videoImport)">
|
<td *ngIf="isVideoImportPending(videoImport)">
|
||||||
|
|
|
@ -2,8 +2,16 @@
|
||||||
<div class="import-video-torrent">
|
<div class="import-video-torrent">
|
||||||
<div class="icon icon-upload"></div>
|
<div class="icon icon-upload"></div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="button-file">
|
||||||
<label i18n for="magnetUri">Magnet URI</label>
|
<span i18n>Select the torrent to import</span>
|
||||||
|
<input #torrentfileInput type="file" name="torrentfile" id="torrentfile" accept=".torrent" (change)="fileChange()" />
|
||||||
|
</div>
|
||||||
|
<span class="button-file-extension">(.torrent)</span>
|
||||||
|
|
||||||
|
<div class="torrent-or-magnet">Or</div>
|
||||||
|
|
||||||
|
<div class="form-group form-group-magnet-uri">
|
||||||
|
<label i18n for="magnetUri">Paste magnet URI</label>
|
||||||
<my-help
|
<my-help
|
||||||
helpType="custom" i18n-customHtml
|
helpType="custom" i18n-customHtml
|
||||||
customHtml="You can import any torrent file that points to a mp4 file. You should make sure you have diffusion rights over the content it points to, otherwise it could cause legal trouble to yourself and your instance."
|
customHtml="You can import any torrent file that points to a mp4 file. You should make sure you have diffusion rights over the content it points to, otherwise it could cause legal trouble to yourself and your instance."
|
||||||
|
|
|
@ -20,6 +20,26 @@ $width-size: 190px;
|
||||||
background-image: url('../../../../assets/images/video/upload.svg');
|
background-image: url('../../../../assets/images/video/upload.svg');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.button-file {
|
||||||
|
@include peertube-button-file(auto);
|
||||||
|
|
||||||
|
min-width: 190px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-file-extension {
|
||||||
|
display: block;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.torrent-or-magnet {
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group-magnet-uri {
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
input[type=text] {
|
input[type=text] {
|
||||||
@include peertube-input-text($width-size);
|
@include peertube-input-text($width-size);
|
||||||
display: block;
|
display: block;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Component, EventEmitter, OnInit, Output } from '@angular/core'
|
import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core'
|
||||||
import { Router } from '@angular/router'
|
import { Router } from '@angular/router'
|
||||||
import { NotificationsService } from 'angular2-notifications'
|
import { NotificationsService } from 'angular2-notifications'
|
||||||
import { VideoPrivacy, VideoUpdate } from '../../../../../../shared/models/videos'
|
import { VideoPrivacy, VideoUpdate } from '../../../../../../shared/models/videos'
|
||||||
|
@ -23,6 +23,7 @@ import { VideoImportService } from '@app/shared/video-import'
|
||||||
})
|
})
|
||||||
export class VideoImportTorrentComponent extends VideoSend implements OnInit, CanComponentDeactivate {
|
export class VideoImportTorrentComponent extends VideoSend implements OnInit, CanComponentDeactivate {
|
||||||
@Output() firstStepDone = new EventEmitter<string>()
|
@Output() firstStepDone = new EventEmitter<string>()
|
||||||
|
@ViewChild('torrentfileInput') torrentfileInput
|
||||||
|
|
||||||
videoFileName: string
|
videoFileName: string
|
||||||
magnetUri = ''
|
magnetUri = ''
|
||||||
|
@ -33,7 +34,7 @@ export class VideoImportTorrentComponent extends VideoSend implements OnInit, Ca
|
||||||
|
|
||||||
video: VideoEdit
|
video: VideoEdit
|
||||||
|
|
||||||
protected readonly DEFAULT_VIDEO_PRIVACY = VideoPrivacy.PRIVATE
|
protected readonly DEFAULT_VIDEO_PRIVACY = VideoPrivacy.PUBLIC
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
protected formValidatorService: FormValidatorService,
|
protected formValidatorService: FormValidatorService,
|
||||||
|
@ -62,7 +63,14 @@ export class VideoImportTorrentComponent extends VideoSend implements OnInit, Ca
|
||||||
return !!this.magnetUri
|
return !!this.magnetUri
|
||||||
}
|
}
|
||||||
|
|
||||||
importVideo () {
|
fileChange () {
|
||||||
|
const torrentfile = this.torrentfileInput.nativeElement.files[0] as File
|
||||||
|
if (!torrentfile) return
|
||||||
|
|
||||||
|
this.importVideo(torrentfile)
|
||||||
|
}
|
||||||
|
|
||||||
|
importVideo (torrentfile?: Blob) {
|
||||||
this.isImportingVideo = true
|
this.isImportingVideo = true
|
||||||
|
|
||||||
const videoUpdate: VideoUpdate = {
|
const videoUpdate: VideoUpdate = {
|
||||||
|
@ -74,7 +82,7 @@ export class VideoImportTorrentComponent extends VideoSend implements OnInit, Ca
|
||||||
|
|
||||||
this.loadingBar.start()
|
this.loadingBar.start()
|
||||||
|
|
||||||
this.videoImportService.importVideoTorrent(this.magnetUri, videoUpdate).subscribe(
|
this.videoImportService.importVideoTorrent(torrentfile || this.magnetUri, videoUpdate).subscribe(
|
||||||
res => {
|
res => {
|
||||||
this.loadingBar.complete()
|
this.loadingBar.complete()
|
||||||
this.firstStepDone.emit(res.video.name)
|
this.firstStepDone.emit(res.video.name)
|
||||||
|
|
|
@ -33,7 +33,7 @@ export class VideoImportUrlComponent extends VideoSend implements OnInit, CanCom
|
||||||
|
|
||||||
video: VideoEdit
|
video: VideoEdit
|
||||||
|
|
||||||
protected readonly DEFAULT_VIDEO_PRIVACY = VideoPrivacy.PRIVATE
|
protected readonly DEFAULT_VIDEO_PRIVACY = VideoPrivacy.PUBLIC
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
protected formValidatorService: FormValidatorService,
|
protected formValidatorService: FormValidatorService,
|
||||||
|
|
|
@ -49,7 +49,7 @@ $background-color: #F7F7F7;
|
||||||
background-color: $background-color;
|
background-color: $background-color;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 440px;
|
min-height: 440px;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
|
@ -1,8 +1,16 @@
|
||||||
import * as magnetUtil from 'magnet-uri'
|
|
||||||
import * as express from 'express'
|
import * as express from 'express'
|
||||||
|
import * as magnetUtil from 'magnet-uri'
|
||||||
|
import 'multer'
|
||||||
import { auditLoggerFactory, VideoImportAuditView } from '../../../helpers/audit-logger'
|
import { auditLoggerFactory, VideoImportAuditView } from '../../../helpers/audit-logger'
|
||||||
import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, videoImportAddValidator } from '../../../middlewares'
|
import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, videoImportAddValidator } from '../../../middlewares'
|
||||||
import { CONFIG, IMAGE_MIMETYPE_EXT, PREVIEWS_SIZE, sequelizeTypescript, THUMBNAILS_SIZE } from '../../../initializers'
|
import {
|
||||||
|
CONFIG,
|
||||||
|
IMAGE_MIMETYPE_EXT,
|
||||||
|
PREVIEWS_SIZE,
|
||||||
|
sequelizeTypescript,
|
||||||
|
THUMBNAILS_SIZE,
|
||||||
|
TORRENT_MIMETYPE_EXT
|
||||||
|
} from '../../../initializers'
|
||||||
import { getYoutubeDLInfo, YoutubeDLInfo } from '../../../helpers/youtube-dl'
|
import { getYoutubeDLInfo, YoutubeDLInfo } from '../../../helpers/youtube-dl'
|
||||||
import { createReqFiles } from '../../../helpers/express-utils'
|
import { createReqFiles } from '../../../helpers/express-utils'
|
||||||
import { logger } from '../../../helpers/logger'
|
import { logger } from '../../../helpers/logger'
|
||||||
|
@ -18,16 +26,20 @@ import { isArray } from '../../../helpers/custom-validators/misc'
|
||||||
import { FilteredModelAttributes } from 'sequelize-typescript/lib/models/Model'
|
import { FilteredModelAttributes } from 'sequelize-typescript/lib/models/Model'
|
||||||
import { VideoChannelModel } from '../../../models/video/video-channel'
|
import { VideoChannelModel } from '../../../models/video/video-channel'
|
||||||
import * as Bluebird from 'bluebird'
|
import * as Bluebird from 'bluebird'
|
||||||
|
import * as parseTorrent from 'parse-torrent'
|
||||||
|
import { readFileBufferPromise, renamePromise } from '../../../helpers/core-utils'
|
||||||
|
import { getSecureTorrentName } from '../../../helpers/utils'
|
||||||
|
|
||||||
const auditLogger = auditLoggerFactory('video-imports')
|
const auditLogger = auditLoggerFactory('video-imports')
|
||||||
const videoImportsRouter = express.Router()
|
const videoImportsRouter = express.Router()
|
||||||
|
|
||||||
const reqVideoFileImport = createReqFiles(
|
const reqVideoFileImport = createReqFiles(
|
||||||
[ 'thumbnailfile', 'previewfile' ],
|
[ 'thumbnailfile', 'previewfile', 'torrentfile' ],
|
||||||
IMAGE_MIMETYPE_EXT,
|
Object.assign({}, TORRENT_MIMETYPE_EXT, IMAGE_MIMETYPE_EXT),
|
||||||
{
|
{
|
||||||
thumbnailfile: CONFIG.STORAGE.THUMBNAILS_DIR,
|
thumbnailfile: CONFIG.STORAGE.THUMBNAILS_DIR,
|
||||||
previewfile: CONFIG.STORAGE.PREVIEWS_DIR
|
previewfile: CONFIG.STORAGE.PREVIEWS_DIR,
|
||||||
|
torrentfile: CONFIG.STORAGE.TORRENTS_DIR
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -49,17 +61,37 @@ export {
|
||||||
function addVideoImport (req: express.Request, res: express.Response) {
|
function addVideoImport (req: express.Request, res: express.Response) {
|
||||||
if (req.body.targetUrl) return addYoutubeDLImport(req, res)
|
if (req.body.targetUrl) return addYoutubeDLImport(req, res)
|
||||||
|
|
||||||
if (req.body.magnetUri) return addTorrentImport(req, res)
|
const file = req.files['torrentfile'][0]
|
||||||
|
if (req.body.magnetUri || file) return addTorrentImport(req, res, file)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function addTorrentImport (req: express.Request, res: express.Response) {
|
async function addTorrentImport (req: express.Request, res: express.Response, torrentfile: Express.Multer.File) {
|
||||||
const body: VideoImportCreate = req.body
|
const body: VideoImportCreate = req.body
|
||||||
const magnetUri = body.magnetUri
|
|
||||||
|
|
||||||
const parsed = magnetUtil.decode(magnetUri)
|
let videoName: string
|
||||||
const magnetName = isArray(parsed.name) ? parsed.name[0] : parsed.name as string
|
let torrentName: string
|
||||||
|
let magnetUri: string
|
||||||
|
|
||||||
const video = buildVideo(res.locals.videoChannel.id, body, { name: magnetName })
|
if (torrentfile) {
|
||||||
|
torrentName = torrentfile.originalname
|
||||||
|
|
||||||
|
// Rename the torrent to a secured name
|
||||||
|
const newTorrentPath = join(CONFIG.STORAGE.TORRENTS_DIR, getSecureTorrentName(torrentName))
|
||||||
|
await renamePromise(torrentfile.path, newTorrentPath)
|
||||||
|
torrentfile.path = newTorrentPath
|
||||||
|
|
||||||
|
const buf = await readFileBufferPromise(torrentfile.path)
|
||||||
|
const parsedTorrent = parseTorrent(buf)
|
||||||
|
|
||||||
|
videoName = isArray(parsedTorrent.name) ? parsedTorrent.name[ 0 ] : parsedTorrent.name as string
|
||||||
|
} else {
|
||||||
|
magnetUri = body.magnetUri
|
||||||
|
|
||||||
|
const parsed = magnetUtil.decode(magnetUri)
|
||||||
|
videoName = isArray(parsed.name) ? parsed.name[ 0 ] : parsed.name as string
|
||||||
|
}
|
||||||
|
|
||||||
|
const video = buildVideo(res.locals.videoChannel.id, body, { name: videoName })
|
||||||
|
|
||||||
await processThumbnail(req, video)
|
await processThumbnail(req, video)
|
||||||
await processPreview(req, video)
|
await processPreview(req, video)
|
||||||
|
@ -67,13 +99,14 @@ async function addTorrentImport (req: express.Request, res: express.Response) {
|
||||||
const tags = null
|
const tags = null
|
||||||
const videoImportAttributes = {
|
const videoImportAttributes = {
|
||||||
magnetUri,
|
magnetUri,
|
||||||
|
torrentName,
|
||||||
state: VideoImportState.PENDING
|
state: VideoImportState.PENDING
|
||||||
}
|
}
|
||||||
const videoImport: VideoImportModel = await insertIntoDB(video, res.locals.videoChannel, tags, videoImportAttributes)
|
const videoImport: VideoImportModel = await insertIntoDB(video, res.locals.videoChannel, tags, videoImportAttributes)
|
||||||
|
|
||||||
// Create job to import the video
|
// Create job to import the video
|
||||||
const payload = {
|
const payload = {
|
||||||
type: 'magnet-uri' as 'magnet-uri',
|
type: torrentfile ? 'torrent-file' as 'torrent-file' : 'magnet-uri' as 'magnet-uri',
|
||||||
videoImportId: videoImport.id,
|
videoImportId: videoImport.id,
|
||||||
magnetUri
|
magnetUri
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,7 @@ import * as pem from 'pem'
|
||||||
import * as rimraf from 'rimraf'
|
import * as rimraf from 'rimraf'
|
||||||
import { URL } from 'url'
|
import { URL } from 'url'
|
||||||
import { truncate } from 'lodash'
|
import { truncate } from 'lodash'
|
||||||
|
import * as crypto from 'crypto'
|
||||||
|
|
||||||
function sanitizeUrl (url: string) {
|
function sanitizeUrl (url: string) {
|
||||||
const urlObject = new URL(url)
|
const urlObject = new URL(url)
|
||||||
|
@ -95,6 +96,10 @@ function peertubeTruncate (str: string, maxLength: number) {
|
||||||
return truncate(str, options)
|
return truncate(str, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sha256 (str: string) {
|
||||||
|
return crypto.createHash('sha256').update(str).digest('hex')
|
||||||
|
}
|
||||||
|
|
||||||
function promisify0<A> (func: (cb: (err: any, result: A) => void) => void): () => Promise<A> {
|
function promisify0<A> (func: (cb: (err: any, result: A) => void) => void): () => Promise<A> {
|
||||||
return function promisified (): Promise<A> {
|
return function promisified (): Promise<A> {
|
||||||
return new Promise<A>((resolve: (arg: A) => void, reject: (err: any) => void) => {
|
return new Promise<A>((resolve: (arg: A) => void, reject: (err: any) => void) => {
|
||||||
|
@ -165,6 +170,7 @@ export {
|
||||||
sanitizeHost,
|
sanitizeHost,
|
||||||
buildPath,
|
buildPath,
|
||||||
peertubeTruncate,
|
peertubeTruncate,
|
||||||
|
sha256,
|
||||||
|
|
||||||
promisify0,
|
promisify0,
|
||||||
promisify1,
|
promisify1,
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
import 'express-validator'
|
import 'express-validator'
|
||||||
import 'multer'
|
import 'multer'
|
||||||
import * as validator from 'validator'
|
import * as validator from 'validator'
|
||||||
import { CONSTRAINTS_FIELDS, VIDEO_IMPORT_STATES } from '../../initializers'
|
import { CONSTRAINTS_FIELDS, TORRENT_MIMETYPE_EXT, VIDEO_IMPORT_STATES } from '../../initializers'
|
||||||
import { exists } from './misc'
|
import { exists, isFileValid } from './misc'
|
||||||
import * as express from 'express'
|
import * as express from 'express'
|
||||||
import { VideoChannelModel } from '../../models/video/video-channel'
|
|
||||||
import { VideoImportModel } from '../../models/video/video-import'
|
import { VideoImportModel } from '../../models/video/video-import'
|
||||||
|
|
||||||
function isVideoImportTargetUrlValid (url: string) {
|
function isVideoImportTargetUrlValid (url: string) {
|
||||||
|
@ -25,6 +24,12 @@ function isVideoImportStateValid (value: any) {
|
||||||
return exists(value) && VIDEO_IMPORT_STATES[ value ] !== undefined
|
return exists(value) && VIDEO_IMPORT_STATES[ value ] !== undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const videoTorrentImportTypes = Object.keys(TORRENT_MIMETYPE_EXT).map(m => `(${m})`)
|
||||||
|
const videoTorrentImportRegex = videoTorrentImportTypes.join('|')
|
||||||
|
function isVideoImportTorrentFile (files: { [ fieldname: string ]: Express.Multer.File[] } | Express.Multer.File[]) {
|
||||||
|
return isFileValid(files, videoTorrentImportRegex, 'torrentfile', CONSTRAINTS_FIELDS.VIDEO_IMPORTS.TORRENT_FILE.FILE_SIZE.max, true)
|
||||||
|
}
|
||||||
|
|
||||||
async function isVideoImportExist (id: number, res: express.Response) {
|
async function isVideoImportExist (id: number, res: express.Response) {
|
||||||
const videoImport = await VideoImportModel.loadAndPopulateVideo(id)
|
const videoImport = await VideoImportModel.loadAndPopulateVideo(id)
|
||||||
|
|
||||||
|
@ -45,5 +50,6 @@ async function isVideoImportExist (id: number, res: express.Response) {
|
||||||
export {
|
export {
|
||||||
isVideoImportStateValid,
|
isVideoImportStateValid,
|
||||||
isVideoImportTargetUrlValid,
|
isVideoImportTargetUrlValid,
|
||||||
isVideoImportExist
|
isVideoImportExist,
|
||||||
|
isVideoImportTorrentFile
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,11 +6,12 @@ import { CONFIG } from '../initializers'
|
||||||
import { UserModel } from '../models/account/user'
|
import { UserModel } from '../models/account/user'
|
||||||
import { ActorModel } from '../models/activitypub/actor'
|
import { ActorModel } from '../models/activitypub/actor'
|
||||||
import { ApplicationModel } from '../models/application/application'
|
import { ApplicationModel } from '../models/application/application'
|
||||||
import { pseudoRandomBytesPromise, unlinkPromise } from './core-utils'
|
import { pseudoRandomBytesPromise, sha256, unlinkPromise } from './core-utils'
|
||||||
import { logger } from './logger'
|
import { logger } from './logger'
|
||||||
import { isArray } from './custom-validators/misc'
|
import { isArray } from './custom-validators/misc'
|
||||||
import * as crypto from "crypto"
|
import * as crypto from "crypto"
|
||||||
import { join } from "path"
|
import { join } from "path"
|
||||||
|
import { Instance as ParseTorrent } from 'parse-torrent'
|
||||||
|
|
||||||
const isCidr = require('is-cidr')
|
const isCidr = require('is-cidr')
|
||||||
|
|
||||||
|
@ -183,13 +184,18 @@ async function getServerActor () {
|
||||||
return Promise.resolve(serverActor)
|
return Promise.resolve(serverActor)
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateVideoTmpPath (id: string) {
|
function generateVideoTmpPath (target: string | ParseTorrent) {
|
||||||
const hash = crypto.createHash('sha256').update(id).digest('hex')
|
const id = typeof target === 'string' ? target : target.infoHash
|
||||||
|
|
||||||
|
const hash = sha256(id)
|
||||||
return join(CONFIG.STORAGE.VIDEOS_DIR, hash + '-import.mp4')
|
return join(CONFIG.STORAGE.VIDEOS_DIR, hash + '-import.mp4')
|
||||||
}
|
}
|
||||||
|
|
||||||
type SortType = { sortModel: any, sortValue: string }
|
function getSecureTorrentName (originalName: string) {
|
||||||
|
return sha256(originalName) + '.torrent'
|
||||||
|
}
|
||||||
|
|
||||||
|
type SortType = { sortModel: any, sortValue: string }
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@ -199,6 +205,7 @@ export {
|
||||||
generateRandomString,
|
generateRandomString,
|
||||||
getFormattedObjects,
|
getFormattedObjects,
|
||||||
isSignupAllowed,
|
isSignupAllowed,
|
||||||
|
getSecureTorrentName,
|
||||||
isSignupAllowedForCurrentIP,
|
isSignupAllowedForCurrentIP,
|
||||||
computeResolutionsToTranscode,
|
computeResolutionsToTranscode,
|
||||||
resetSequelizeInstance,
|
resetSequelizeInstance,
|
||||||
|
|
|
@ -2,17 +2,22 @@ import { logger } from './logger'
|
||||||
import { generateVideoTmpPath } from './utils'
|
import { generateVideoTmpPath } from './utils'
|
||||||
import * as WebTorrent from 'webtorrent'
|
import * as WebTorrent from 'webtorrent'
|
||||||
import { createWriteStream } from 'fs'
|
import { createWriteStream } from 'fs'
|
||||||
|
import { Instance as ParseTorrent } from 'parse-torrent'
|
||||||
|
import { CONFIG } from '../initializers'
|
||||||
|
import { join } from 'path'
|
||||||
|
|
||||||
function downloadWebTorrentVideo (target: string) {
|
function downloadWebTorrentVideo (target: { magnetUri: string, torrentName: string }) {
|
||||||
const path = generateVideoTmpPath(target)
|
const id = target.magnetUri || target.torrentName
|
||||||
|
|
||||||
logger.info('Importing torrent video %s', target)
|
const path = generateVideoTmpPath(id)
|
||||||
|
logger.info('Importing torrent video %s', id)
|
||||||
|
|
||||||
return new Promise<string>((res, rej) => {
|
return new Promise<string>((res, rej) => {
|
||||||
const webtorrent = new WebTorrent()
|
const webtorrent = new WebTorrent()
|
||||||
|
|
||||||
const torrent = webtorrent.add(target, torrent => {
|
const torrentId = target.magnetUri || join(CONFIG.STORAGE.TORRENTS_DIR, target.torrentName)
|
||||||
if (torrent.files.length !== 1) throw new Error('The number of files is not equal to 1 for ' + target)
|
const torrent = webtorrent.add(torrentId, torrent => {
|
||||||
|
if (torrent.files.length !== 1) return rej(new Error('The number of files is not equal to 1 for ' + torrentId))
|
||||||
|
|
||||||
const file = torrent.files[ 0 ]
|
const file = torrent.files[ 0 ]
|
||||||
file.createReadStream().pipe(createWriteStream(path))
|
file.createReadStream().pipe(createWriteStream(path))
|
||||||
|
|
|
@ -273,6 +273,12 @@ const CONSTRAINTS_FIELDS = {
|
||||||
VIDEO_IMPORTS: {
|
VIDEO_IMPORTS: {
|
||||||
URL: { min: 3, max: 2000 }, // Length
|
URL: { min: 3, max: 2000 }, // Length
|
||||||
TORRENT_NAME: { min: 3, max: 255 }, // Length
|
TORRENT_NAME: { min: 3, max: 255 }, // Length
|
||||||
|
TORRENT_FILE: {
|
||||||
|
EXTNAME: [ '.torrent' ],
|
||||||
|
FILE_SIZE: {
|
||||||
|
max: 1024 * 200 // 200 KB
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
VIDEOS: {
|
VIDEOS: {
|
||||||
NAME: { min: 3, max: 120 }, // Length
|
NAME: { min: 3, max: 120 }, // Length
|
||||||
|
@ -417,6 +423,10 @@ const VIDEO_CAPTIONS_MIMETYPE_EXT = {
|
||||||
'application/x-subrip': '.srt'
|
'application/x-subrip': '.srt'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const TORRENT_MIMETYPE_EXT = {
|
||||||
|
'application/x-bittorrent': '.torrent'
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
const SERVER_ACTOR_NAME = 'peertube'
|
const SERVER_ACTOR_NAME = 'peertube'
|
||||||
|
@ -596,6 +606,7 @@ export {
|
||||||
FEEDS,
|
FEEDS,
|
||||||
JOB_TTL,
|
JOB_TTL,
|
||||||
NSFW_POLICY_TYPES,
|
NSFW_POLICY_TYPES,
|
||||||
|
TORRENT_MIMETYPE_EXT,
|
||||||
STATIC_MAX_AGE,
|
STATIC_MAX_AGE,
|
||||||
STATIC_PATHS,
|
STATIC_PATHS,
|
||||||
ACTIVITY_PUB,
|
ACTIVITY_PUB,
|
||||||
|
|
|
@ -14,6 +14,7 @@ import { JobQueue } from '../index'
|
||||||
import { federateVideoIfNeeded } from '../../activitypub'
|
import { federateVideoIfNeeded } from '../../activitypub'
|
||||||
import { VideoModel } from '../../../models/video/video'
|
import { VideoModel } from '../../../models/video/video'
|
||||||
import { downloadWebTorrentVideo } from '../../../helpers/webtorrent'
|
import { downloadWebTorrentVideo } from '../../../helpers/webtorrent'
|
||||||
|
import { getSecureTorrentName } from '../../../helpers/utils'
|
||||||
|
|
||||||
type VideoImportYoutubeDLPayload = {
|
type VideoImportYoutubeDLPayload = {
|
||||||
type: 'youtube-dl'
|
type: 'youtube-dl'
|
||||||
|
@ -25,7 +26,7 @@ type VideoImportYoutubeDLPayload = {
|
||||||
}
|
}
|
||||||
|
|
||||||
type VideoImportTorrentPayload = {
|
type VideoImportTorrentPayload = {
|
||||||
type: 'magnet-uri'
|
type: 'magnet-uri' | 'torrent-file'
|
||||||
videoImportId: number
|
videoImportId: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,7 +36,7 @@ async function processVideoImport (job: Bull.Job) {
|
||||||
const payload = job.data as VideoImportPayload
|
const payload = job.data as VideoImportPayload
|
||||||
|
|
||||||
if (payload.type === 'youtube-dl') return processYoutubeDLImport(job, payload)
|
if (payload.type === 'youtube-dl') return processYoutubeDLImport(job, payload)
|
||||||
if (payload.type === 'magnet-uri') return processTorrentImport(job, payload)
|
if (payload.type === 'magnet-uri' || payload.type === 'torrent-file') return processTorrentImport(job, payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
@ -50,6 +51,7 @@ async function processTorrentImport (job: Bull.Job, payload: VideoImportTorrentP
|
||||||
logger.info('Processing torrent video import in job %d.', job.id)
|
logger.info('Processing torrent video import in job %d.', job.id)
|
||||||
|
|
||||||
const videoImport = await getVideoImportOrDie(payload.videoImportId)
|
const videoImport = await getVideoImportOrDie(payload.videoImportId)
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
videoImportId: payload.videoImportId,
|
videoImportId: payload.videoImportId,
|
||||||
|
|
||||||
|
@ -59,7 +61,11 @@ async function processTorrentImport (job: Bull.Job, payload: VideoImportTorrentP
|
||||||
generateThumbnail: true,
|
generateThumbnail: true,
|
||||||
generatePreview: true
|
generatePreview: true
|
||||||
}
|
}
|
||||||
return processFile(() => downloadWebTorrentVideo(videoImport.magnetUri), videoImport, options)
|
const target = {
|
||||||
|
torrentName: videoImport.torrentName ? getSecureTorrentName(videoImport.torrentName) : undefined,
|
||||||
|
magnetUri: videoImport.magnetUri
|
||||||
|
}
|
||||||
|
return processFile(() => downloadWebTorrentVideo(target), videoImport, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function processYoutubeDLImport (job: Bull.Job, payload: VideoImportYoutubeDLPayload) {
|
async function processYoutubeDLImport (job: Bull.Job, payload: VideoImportYoutubeDLPayload) {
|
||||||
|
|
|
@ -4,10 +4,11 @@ import { isIdValid } from '../../helpers/custom-validators/misc'
|
||||||
import { logger } from '../../helpers/logger'
|
import { logger } from '../../helpers/logger'
|
||||||
import { areValidationErrors } from './utils'
|
import { areValidationErrors } from './utils'
|
||||||
import { getCommonVideoAttributes } from './videos'
|
import { getCommonVideoAttributes } from './videos'
|
||||||
import { isVideoImportTargetUrlValid } from '../../helpers/custom-validators/video-imports'
|
import { isVideoImportTargetUrlValid, isVideoImportTorrentFile } from '../../helpers/custom-validators/video-imports'
|
||||||
import { cleanUpReqFiles } from '../../helpers/utils'
|
import { cleanUpReqFiles } from '../../helpers/utils'
|
||||||
import { isVideoChannelOfAccountExist, isVideoMagnetUriValid, isVideoNameValid } from '../../helpers/custom-validators/videos'
|
import { isVideoChannelOfAccountExist, isVideoMagnetUriValid, isVideoNameValid } from '../../helpers/custom-validators/videos'
|
||||||
import { CONFIG } from '../../initializers/constants'
|
import { CONFIG } from '../../initializers/constants'
|
||||||
|
import { CONSTRAINTS_FIELDS } from '../../initializers'
|
||||||
|
|
||||||
const videoImportAddValidator = getCommonVideoAttributes().concat([
|
const videoImportAddValidator = getCommonVideoAttributes().concat([
|
||||||
body('channelId')
|
body('channelId')
|
||||||
|
@ -19,6 +20,11 @@ const videoImportAddValidator = getCommonVideoAttributes().concat([
|
||||||
body('magnetUri')
|
body('magnetUri')
|
||||||
.optional()
|
.optional()
|
||||||
.custom(isVideoMagnetUriValid).withMessage('Should have a valid video magnet URI'),
|
.custom(isVideoMagnetUriValid).withMessage('Should have a valid video magnet URI'),
|
||||||
|
body('torrentfile')
|
||||||
|
.custom((value, { req }) => isVideoImportTorrentFile(req.files)).withMessage(
|
||||||
|
'This torrent file is not supported or too large. Please, make sure it is of the following type: '
|
||||||
|
+ CONSTRAINTS_FIELDS.VIDEO_IMPORTS.TORRENT_FILE.EXTNAME.join(', ')
|
||||||
|
),
|
||||||
body('name')
|
body('name')
|
||||||
.optional()
|
.optional()
|
||||||
.custom(isVideoNameValid).withMessage('Should have a valid name'),
|
.custom(isVideoNameValid).withMessage('Should have a valid name'),
|
||||||
|
@ -40,11 +46,12 @@ const videoImportAddValidator = getCommonVideoAttributes().concat([
|
||||||
if (!await isVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req)
|
if (!await isVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req)
|
||||||
|
|
||||||
// Check we have at least 1 required param
|
// Check we have at least 1 required param
|
||||||
if (!req.body.targetUrl && !req.body.magnetUri) {
|
const file = req.files['torrentfile'][0]
|
||||||
|
if (!req.body.targetUrl && !req.body.magnetUri && !file) {
|
||||||
cleanUpReqFiles(req)
|
cleanUpReqFiles(req)
|
||||||
|
|
||||||
return res.status(400)
|
return res.status(400)
|
||||||
.json({ error: 'Should have a magnetUri or a targetUrl.' })
|
.json({ error: 'Should have a magnetUri or a targetUrl or a torrent file.' })
|
||||||
.end()
|
.end()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -171,7 +171,11 @@ export class VideoImportModel extends Model<VideoImportModel> {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: this.id,
|
id: this.id,
|
||||||
|
|
||||||
targetUrl: this.targetUrl,
|
targetUrl: this.targetUrl,
|
||||||
|
magnetUri: this.magnetUri,
|
||||||
|
torrentName: this.torrentName,
|
||||||
|
|
||||||
state: {
|
state: {
|
||||||
id: this.state,
|
id: this.state,
|
||||||
label: VideoImportModel.getStateLabel(this.state)
|
label: VideoImportModel.getStateLabel(this.state)
|
||||||
|
|
|
@ -4,7 +4,11 @@ import { VideoImportState } from './video-import-state.enum'
|
||||||
|
|
||||||
export interface VideoImport {
|
export interface VideoImport {
|
||||||
id: number
|
id: number
|
||||||
|
|
||||||
targetUrl: string
|
targetUrl: string
|
||||||
|
magnetUri: string
|
||||||
|
torrentName: string
|
||||||
|
|
||||||
createdAt: string
|
createdAt: string
|
||||||
updatedAt: string
|
updatedAt: string
|
||||||
state: VideoConstant<VideoImportState>
|
state: VideoConstant<VideoImportState>
|
||||||
|
|
Loading…
Reference in New Issue