Add ability to download a video from direct link or torrent file
This commit is contained in:
parent
bda65bdc9f
commit
a96aed1518
|
@ -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">×</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>
|
|
@ -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
|
|
@ -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">×</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>
|
|
@ -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>
|
||||
|
|
|
@ -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 () {
|
||||
|
|
|
@ -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
|
||||
],
|
||||
|
|
|
@ -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')
|
||||
|
||||
|
|
44
server.ts
44
server.ts
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -3,6 +3,8 @@ export interface VideoFile {
|
|||
resolution: number
|
||||
resolutionLabel: string
|
||||
size: number // Bytes
|
||||
torrentUrl: string
|
||||
fileUrl: string
|
||||
}
|
||||
|
||||
export interface Video {
|
||||
|
|
Loading…
Reference in New Issue