Add ability to download a video from direct link or torrent file

This commit is contained in:
Chocobozzz 2017-10-19 14:58:28 +02:00
parent bda65bdc9f
commit a96aed1518
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
13 changed files with 123 additions and 86 deletions

View File

@ -0,0 +1,30 @@
<div bsModal #modal="bs-modal" class="modal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content modal-lg">
<div class="modal-header">
<button type="button" class="close" aria-label="Close" (click)="hide()">
<span aria-hidden="true">&times;</span>
</button>
<h4 class="modal-title">Download</h4>
</div>
<div class="modal-body">
<div *ngFor="let file of video.files" class="resolution-block">
<label>{{ file.resolutionLabel }}</label>
<a class="btn btn-default " target="_blank" [href]="file.torrentUrl">
<span class="glyphicon glyphicon-download"></span>
Torrent file
</a>
<a class="btn btn-default" target="_blank" [href]="file.fileUrl">
<span class="glyphicon glyphicon-download"></span>
Download
</a>
<!-- Don't display magnet URI for now, this is not compatible with most torrent clients -->
<!--<input #magnetUriInput (click)="magnetUriInput.select()" type="text" class="form-control input-sm readonly" readonly [value]="file.magnetUri" />-->
</div>
</div>
</div>
</div>
</div>

View File

@ -5,10 +5,11 @@ import { ModalDirective } from 'ngx-bootstrap/modal'
import { Video } from '../shared' import { Video } from '../shared'
@Component({ @Component({
selector: 'my-video-magnet', selector: 'my-video-download',
templateUrl: './video-magnet.component.html' templateUrl: './video-download.component.html',
styles: [ '.resolution-block { margin-top: 20px; }' ]
}) })
export class VideoMagnetComponent { export class VideoDownloadComponent {
@Input() video: Video = null @Input() video: Video = null
@ViewChild('modal') modal: ModalDirective @ViewChild('modal') modal: ModalDirective

View File

@ -1,20 +0,0 @@
<div bsModal #modal="bs-modal" class="modal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content modal-lg">
<div class="modal-header">
<button type="button" class="close" aria-label="Close" (click)="hide()">
<span aria-hidden="true">&times;</span>
</button>
<h4 class="modal-title">Magnet Uri</h4>
</div>
<div class="modal-body">
<div *ngFor="let file of video.files">
<label>{{ file.resolutionLabel }}</label>
<input #magnetUriInput (click)="magnetUriInput.select()" type="text" class="form-control input-sm readonly" readonly [value]="file.magnetUri" />
</div>
</div>
</div>
</div>
</div>

View File

@ -71,8 +71,8 @@
</li> </li>
<li role="menuitem"> <li role="menuitem">
<a class="dropdown-item" title="Get magnet URI" href="#" (click)="showMagnetUriModal($event)"> <a class="dropdown-item" title="Download the video" href="#" (click)="showDownloadModal($event)">
<span class="glyphicon glyphicon-magnet"></span> Magnet <span class="glyphicon glyphicon-download-alt"></span> Download
</a> </a>
</li> </li>
@ -179,6 +179,6 @@
<ng-template [ngIf]="video !== null"> <ng-template [ngIf]="video !== null">
<my-video-share #videoShareModal [video]="video"></my-video-share> <my-video-share #videoShareModal [video]="video"></my-video-share>
<my-video-magnet #videoMagnetModal [video]="video"></my-video-magnet> <my-video-download #videoDownloadModal [video]="video"></my-video-download>
<my-video-report #videoReportModal [video]="video"></my-video-report> <my-video-report #videoReportModal [video]="video"></my-video-report>
</ng-template> </ng-template>

View File

@ -10,7 +10,7 @@ import { MetaService } from '@ngx-meta/core'
import { NotificationsService } from 'angular2-notifications' import { NotificationsService } from 'angular2-notifications'
import { AuthService, ConfirmService } from '../../core' import { AuthService, ConfirmService } from '../../core'
import { VideoMagnetComponent } from './video-magnet.component' import { VideoDownloadComponent } from './video-download.component'
import { VideoShareComponent } from './video-share.component' import { VideoShareComponent } from './video-share.component'
import { VideoReportComponent } from './video-report.component' import { VideoReportComponent } from './video-report.component'
import { Video, VideoService } from '../shared' import { Video, VideoService } from '../shared'
@ -23,7 +23,7 @@ import { UserVideoRateType, VideoRateType } from '../../../../../shared'
styleUrls: [ './video-watch.component.scss' ] styleUrls: [ './video-watch.component.scss' ]
}) })
export class VideoWatchComponent implements OnInit, OnDestroy { export class VideoWatchComponent implements OnInit, OnDestroy {
@ViewChild('videoMagnetModal') videoMagnetModal: VideoMagnetComponent @ViewChild('videoDownloadModal') videoDownloadModal: VideoDownloadComponent
@ViewChild('videoShareModal') videoShareModal: VideoShareComponent @ViewChild('videoShareModal') videoShareModal: VideoShareComponent
@ViewChild('videoReportModal') videoReportModal: VideoReportComponent @ViewChild('videoReportModal') videoReportModal: VideoReportComponent
@ -160,9 +160,9 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
this.videoShareModal.show() this.videoShareModal.show()
} }
showMagnetUriModal (event: Event) { showDownloadModal (event: Event) {
event.preventDefault() event.preventDefault()
this.videoMagnetModal.show() this.videoDownloadModal.show()
} }
isUserLoggedIn () { isUserLoggedIn () {

View File

@ -7,7 +7,7 @@ import { SharedModule } from '../../shared'
import { VideoWatchComponent } from './video-watch.component' import { VideoWatchComponent } from './video-watch.component'
import { VideoReportComponent } from './video-report.component' import { VideoReportComponent } from './video-report.component'
import { VideoShareComponent } from './video-share.component' import { VideoShareComponent } from './video-share.component'
import { VideoMagnetComponent } from './video-magnet.component' import { VideoDownloadComponent } from './video-download.component'
@NgModule({ @NgModule({
imports: [ imports: [
@ -18,7 +18,7 @@ import { VideoMagnetComponent } from './video-magnet.component'
declarations: [ declarations: [
VideoWatchComponent, VideoWatchComponent,
VideoMagnetComponent, VideoDownloadComponent,
VideoShareComponent, VideoShareComponent,
VideoReportComponent VideoReportComponent
], ],

View File

@ -158,7 +158,12 @@ const peertubePlugin = function (options: PeertubePluginOptions) {
}) })
player.torrent.on('error', err => handleError(err)) player.torrent.on('error', err => handleError(err))
player.torrent.on('warning', err => handleError(err)) player.torrent.on('warning', err => {
// We don't support HTTP tracker but we don't care -> we use the web socket tracker
if (err.message.indexOf('Unsupported tracker protocol: http://') !== -1) return
return handleError(err)
})
player.trigger('videoFileUpdate') player.trigger('videoFileUpdate')

View File

@ -79,26 +79,6 @@ app.use(morgan('combined', {
app.use(bodyParser.json({ limit: '500kb' })) app.use(bodyParser.json({ limit: '500kb' }))
app.use(bodyParser.urlencoded({ extended: false })) app.use(bodyParser.urlencoded({ extended: false }))
// ----------- Views, routes and static files -----------
// API
const apiRoute = '/api/' + API_VERSION
app.use(apiRoute, apiRouter)
// Services (oembed...)
app.use('/services', servicesRouter)
// Client files
app.use('/', clientsRouter)
// Static files
app.use('/', staticRouter)
// Always serve index client page (the client is a single page application, let it handle routing)
app.use('/*', function (req, res, next) {
res.sendFile(path.join(__dirname, '../client/dist/index.html'))
})
// ----------- Tracker ----------- // ----------- Tracker -----------
const trackerServer = new TrackerServer({ const trackerServer = new TrackerServer({
@ -122,6 +102,30 @@ wss.on('connection', function (ws) {
trackerServer.onWebSocketConnection(ws) trackerServer.onWebSocketConnection(ws)
}) })
const onHttpRequest = trackerServer.onHttpRequest.bind(trackerServer)
app.get('/tracker/announce', (req, res) => onHttpRequest(req, res, { action: 'announce' }))
app.get('/tracker/scrape', (req, res) => onHttpRequest(req, res, { action: 'scrape' }))
// ----------- Views, routes and static files -----------
// API
const apiRoute = '/api/' + API_VERSION
app.use(apiRoute, apiRouter)
// Services (oembed...)
app.use('/services', servicesRouter)
// Client files
app.use('/', clientsRouter)
// Static files
app.use('/', staticRouter)
// Always serve index client page (the client is a single page application, let it handle routing)
app.use('/*', function (req, res) {
res.sendFile(path.join(__dirname, '../client/dist/index.html'))
})
// ----------- Errors ----------- // ----------- Errors -----------
// Catch 404 and forward to error handler // Catch 404 and forward to error handler

View File

@ -18,7 +18,6 @@ export namespace VideoMethods {
export type ToFormattedJSON = (this: VideoInstance) => FormattedVideo export type ToFormattedJSON = (this: VideoInstance) => FormattedVideo
export type GetOriginalFile = (this: VideoInstance) => VideoFileInstance export type GetOriginalFile = (this: VideoInstance) => VideoFileInstance
export type GenerateMagnetUri = (this: VideoInstance, videoFile: VideoFileInstance) => string
export type GetTorrentFileName = (this: VideoInstance, videoFile: VideoFileInstance) => string export type GetTorrentFileName = (this: VideoInstance, videoFile: VideoFileInstance) => string
export type GetVideoFilename = (this: VideoInstance, videoFile: VideoFileInstance) => string export type GetVideoFilename = (this: VideoInstance, videoFile: VideoFileInstance) => string
export type CreatePreview = (this: VideoInstance, videoFile: VideoFileInstance) => Promise<string> export type CreatePreview = (this: VideoInstance, videoFile: VideoFileInstance) => Promise<string>
@ -108,7 +107,6 @@ export interface VideoInstance extends VideoClass, VideoAttributes, Sequelize.In
createThumbnail: VideoMethods.CreateThumbnail createThumbnail: VideoMethods.CreateThumbnail
createTorrentAndSetInfoHash: VideoMethods.CreateTorrentAndSetInfoHash createTorrentAndSetInfoHash: VideoMethods.CreateTorrentAndSetInfoHash
getOriginalFile: VideoMethods.GetOriginalFile getOriginalFile: VideoMethods.GetOriginalFile
generateMagnetUri: VideoMethods.GenerateMagnetUri
getPreviewName: VideoMethods.GetPreviewName getPreviewName: VideoMethods.GetPreviewName
getPreviewPath: VideoMethods.GetPreviewPath getPreviewPath: VideoMethods.GetPreviewPath
getThumbnailName: VideoMethods.GetThumbnailName getThumbnailName: VideoMethods.GetThumbnailName

View File

@ -52,7 +52,6 @@ import { PREVIEWS_SIZE } from '../../initializers/constants'
let Video: Sequelize.Model<VideoInstance, VideoAttributes> let Video: Sequelize.Model<VideoInstance, VideoAttributes>
let getOriginalFile: VideoMethods.GetOriginalFile let getOriginalFile: VideoMethods.GetOriginalFile
let generateMagnetUri: VideoMethods.GenerateMagnetUri
let getVideoFilename: VideoMethods.GetVideoFilename let getVideoFilename: VideoMethods.GetVideoFilename
let getThumbnailName: VideoMethods.GetThumbnailName let getThumbnailName: VideoMethods.GetThumbnailName
let getThumbnailPath: VideoMethods.GetThumbnailPath let getThumbnailPath: VideoMethods.GetThumbnailPath
@ -254,7 +253,6 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
createPreview, createPreview,
createThumbnail, createThumbnail,
createTorrentAndSetInfoHash, createTorrentAndSetInfoHash,
generateMagnetUri,
getPreviewName, getPreviewName,
getPreviewPath, getPreviewPath,
getThumbnailName, getThumbnailName,
@ -426,33 +424,6 @@ createTorrentAndSetInfoHash = function (this: VideoInstance, videoFile: VideoFil
}) })
} }
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)
}
getEmbedPath = function (this: VideoInstance) { getEmbedPath = function (this: VideoInstance) {
return '/videos/embed/' + this.uuid return '/videos/embed/' + this.uuid
} }
@ -516,6 +487,7 @@ toFormattedJSON = function (this: VideoInstance) {
} }
// Format and sort video files // Format and sort video files
const { baseUrlHttp, baseUrlWs } = getBaseUrls(this)
json.files = this.VideoFiles json.files = this.VideoFiles
.map(videoFile => { .map(videoFile => {
let resolutionLabel = videoFile.resolution + 'p' let resolutionLabel = videoFile.resolution + 'p'
@ -523,8 +495,10 @@ toFormattedJSON = function (this: VideoInstance) {
const videoFileJson = { const videoFileJson = {
resolution: videoFile.resolution, resolution: videoFile.resolution,
resolutionLabel, resolutionLabel,
magnetUri: this.generateMagnetUri(videoFile), magnetUri: generateMagnetUri(this, videoFile, baseUrlHttp, baseUrlWs),
size: videoFile.size size: videoFile.size,
torrentUrl: getTorrentUrl(this, videoFile, baseUrlHttp),
fileUrl: getVideoFileUrl(this, videoFile, baseUrlHttp)
} }
return videoFileJson return videoFileJson
@ -972,3 +946,42 @@ function createBaseVideosWhere () {
} }
} }
} }
function getBaseUrls (video: VideoInstance) {
let baseUrlHttp
let baseUrlWs
if (video.isOwned()) {
baseUrlHttp = CONFIG.WEBSERVER.URL
baseUrlWs = CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT
} else {
baseUrlHttp = REMOTE_SCHEME.HTTP + '://' + video.Author.Pod.host
baseUrlWs = REMOTE_SCHEME.WS + '://' + video.Author.Pod.host
}
return { baseUrlHttp, baseUrlWs }
}
function getTorrentUrl (video: VideoInstance, videoFile: VideoFileInstance, baseUrlHttp: string) {
return baseUrlHttp + STATIC_PATHS.TORRENTS + video.getTorrentFileName(videoFile)
}
function getVideoFileUrl (video: VideoInstance, videoFile: VideoFileInstance, baseUrlHttp: string) {
return baseUrlHttp + STATIC_PATHS.WEBSEED + video.getVideoFilename(videoFile)
}
function generateMagnetUri (video: VideoInstance, videoFile: VideoFileInstance, baseUrlHttp: string, baseUrlWs: string) {
const xs = getTorrentUrl(video, videoFile, baseUrlHttp)
const announce = [ baseUrlWs + '/tracker/socket', baseUrlHttp + '/tracker/announce' ]
const urlList = [ getVideoFileUrl(video, videoFile, baseUrlHttp) ]
const magnetHash = {
xs,
announce,
urlList,
infoHash: videoFile.infoHash,
name: video.name
}
return magnetUtil.encode(magnetHash)
}

View File

@ -106,6 +106,8 @@ describe('Test multiple pods', function () {
const file = video.files[0] const file = video.files[0]
const magnetUri = file.magnetUri const magnetUri = file.magnetUri
expect(file.magnetUri).to.have.lengthOf.above(2) expect(file.magnetUri).to.have.lengthOf.above(2)
expect(file.torrentUrl).to.equal(`http://${video.podHost}/static/torrents/${video.uuid}-${file.resolution}.torrent`)
expect(file.fileUrl).to.equal(`http://${video.podHost}/static/webseed/${video.uuid}-${file.resolution}.webm`)
expect(file.resolution).to.equal(720) expect(file.resolution).to.equal(720)
expect(file.resolutionLabel).to.equal('720p') expect(file.resolutionLabel).to.equal('720p')
expect(file.size).to.equal(572456) expect(file.size).to.equal(572456)

View File

@ -127,6 +127,8 @@ describe('Test a single pod', function () {
const file = video.files[0] const file = video.files[0]
const magnetUri = file.magnetUri const magnetUri = file.magnetUri
expect(file.magnetUri).to.have.lengthOf.above(2) expect(file.magnetUri).to.have.lengthOf.above(2)
expect(file.torrentUrl).to.equal(`${server.url}/static/torrents/${video.uuid}-${file.resolution}.torrent`)
expect(file.fileUrl).to.equal(`${server.url}/static/webseed/${video.uuid}-${file.resolution}.webm`)
expect(file.resolution).to.equal(720) expect(file.resolution).to.equal(720)
expect(file.resolutionLabel).to.equal('720p') expect(file.resolutionLabel).to.equal('720p')
expect(file.size).to.equal(218910) expect(file.size).to.equal(218910)

View File

@ -3,6 +3,8 @@ export interface VideoFile {
resolution: number resolution: number
resolutionLabel: string resolutionLabel: string
size: number // Bytes size: number // Bytes
torrentUrl: string
fileUrl: string
} }
export interface Video { export interface Video {