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'
@Component({
selector: 'my-video-magnet',
templateUrl: './video-magnet.component.html'
selector: 'my-video-download',
templateUrl: './video-download.component.html',
styles: [ '.resolution-block { margin-top: 20px; }' ]
})
export class VideoMagnetComponent {
export class VideoDownloadComponent {
@Input() video: Video = null
@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 role="menuitem">
<a class="dropdown-item" title="Get magnet URI" href="#" (click)="showMagnetUriModal($event)">
<span class="glyphicon glyphicon-magnet"></span> Magnet
<a class="dropdown-item" title="Download the video" href="#" (click)="showDownloadModal($event)">
<span class="glyphicon glyphicon-download-alt"></span> Download
</a>
</li>
@ -179,6 +179,6 @@
<ng-template [ngIf]="video !== null">
<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>
</ng-template>

View File

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

View File

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

View File

@ -158,7 +158,12 @@ const peertubePlugin = function (options: PeertubePluginOptions) {
})
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')

View File

@ -79,26 +79,6 @@ app.use(morgan('combined', {
app.use(bodyParser.json({ limit: '500kb' }))
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 -----------
const trackerServer = new TrackerServer({
@ -122,6 +102,30 @@ wss.on('connection', function (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 -----------
// Catch 404 and forward to error handler

View File

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

View File

@ -52,7 +52,6 @@ import { PREVIEWS_SIZE } from '../../initializers/constants'
let Video: Sequelize.Model<VideoInstance, VideoAttributes>
let getOriginalFile: VideoMethods.GetOriginalFile
let generateMagnetUri: VideoMethods.GenerateMagnetUri
let getVideoFilename: VideoMethods.GetVideoFilename
let getThumbnailName: VideoMethods.GetThumbnailName
let getThumbnailPath: VideoMethods.GetThumbnailPath
@ -254,7 +253,6 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
createPreview,
createThumbnail,
createTorrentAndSetInfoHash,
generateMagnetUri,
getPreviewName,
getPreviewPath,
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) {
return '/videos/embed/' + this.uuid
}
@ -516,6 +487,7 @@ toFormattedJSON = function (this: VideoInstance) {
}
// Format and sort video files
const { baseUrlHttp, baseUrlWs } = getBaseUrls(this)
json.files = this.VideoFiles
.map(videoFile => {
let resolutionLabel = videoFile.resolution + 'p'
@ -523,8 +495,10 @@ toFormattedJSON = function (this: VideoInstance) {
const videoFileJson = {
resolution: videoFile.resolution,
resolutionLabel,
magnetUri: this.generateMagnetUri(videoFile),
size: videoFile.size
magnetUri: generateMagnetUri(this, videoFile, baseUrlHttp, baseUrlWs),
size: videoFile.size,
torrentUrl: getTorrentUrl(this, videoFile, baseUrlHttp),
fileUrl: getVideoFileUrl(this, videoFile, baseUrlHttp)
}
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 magnetUri = file.magnetUri
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.resolutionLabel).to.equal('720p')
expect(file.size).to.equal(572456)

View File

@ -127,6 +127,8 @@ describe('Test a single pod', function () {
const file = video.files[0]
const magnetUri = file.magnetUri
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.resolutionLabel).to.equal('720p')
expect(file.size).to.equal(218910)

View File

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