--- /dev/null
+<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>
--- /dev/null
+import { Component, Input, ViewChild } from '@angular/core'
+
+import { ModalDirective } from 'ngx-bootstrap/modal'
+
+import { Video } from '../shared'
+
+@Component({
+ selector: 'my-video-download',
+ templateUrl: './video-download.component.html',
+ styles: [ '.resolution-block { margin-top: 20px; }' ]
+})
+export class VideoDownloadComponent {
+ @Input() video: Video = null
+
+ @ViewChild('modal') modal: ModalDirective
+
+ constructor () {
+ // empty
+ }
+
+ show () {
+ this.modal.show()
+ }
+
+ hide () {
+ this.modal.hide()
+ }
+}
+++ /dev/null
-<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>
+++ /dev/null
-import { Component, Input, ViewChild } from '@angular/core'
-
-import { ModalDirective } from 'ngx-bootstrap/modal'
-
-import { Video } from '../shared'
-
-@Component({
- selector: 'my-video-magnet',
- templateUrl: './video-magnet.component.html'
-})
-export class VideoMagnetComponent {
- @Input() video: Video = null
-
- @ViewChild('modal') modal: ModalDirective
-
- constructor () {
- // empty
- }
-
- show () {
- this.modal.show()
- }
-
- hide () {
- this.modal.hide()
- }
-}
</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>
<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>
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'
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
this.videoShareModal.show()
}
- showMagnetUriModal (event: Event) {
+ showDownloadModal (event: Event) {
event.preventDefault()
- this.videoMagnetModal.show()
+ this.videoDownloadModal.show()
}
isUserLoggedIn () {
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: [
declarations: [
VideoWatchComponent,
- VideoMagnetComponent,
+ VideoDownloadComponent,
VideoShareComponent,
VideoReportComponent
],
})
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')
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({
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
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>
createThumbnail: VideoMethods.CreateThumbnail
createTorrentAndSetInfoHash: VideoMethods.CreateTorrentAndSetInfoHash
getOriginalFile: VideoMethods.GetOriginalFile
- generateMagnetUri: VideoMethods.GenerateMagnetUri
getPreviewName: VideoMethods.GetPreviewName
getPreviewPath: VideoMethods.GetPreviewPath
getThumbnailName: VideoMethods.GetThumbnailName
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
createPreview,
createThumbnail,
createTorrentAndSetInfoHash,
- generateMagnetUri,
getPreviewName,
getPreviewPath,
getThumbnailName,
})
}
-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
}
}
// Format and sort video files
+ const { baseUrlHttp, baseUrlWs } = getBaseUrls(this)
json.files = this.VideoFiles
.map(videoFile => {
let resolutionLabel = videoFile.resolution + 'p'
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
}
}
}
+
+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)
+}
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)
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)
resolution: number
resolutionLabel: string
size: number // Bytes
+ torrentUrl: string
+ fileUrl: string
}
export interface Video {