From a96aed15188174c50885dda0df3164a67295e11f Mon Sep 17 00:00:00 2001 From: Chocobozzz <florian.bigard@gmail.com> Date: Thu, 19 Oct 2017 14:58:28 +0200 Subject: [PATCH] Add ability to download a video from direct link or torrent file --- .../video-download.component.html | 30 ++++++++ ...mponent.ts => video-download.component.ts} | 7 +- .../+video-watch/video-magnet.component.html | 20 ----- .../+video-watch/video-watch.component.html | 6 +- .../+video-watch/video-watch.component.ts | 8 +- .../videos/+video-watch/video-watch.module.ts | 4 +- .../assets/player/peertube-videojs-plugin.ts | 7 +- server.ts | 44 ++++++----- server/models/video/video-interface.ts | 2 - server/models/video/video.ts | 75 +++++++++++-------- server/tests/api/multiple-pods.ts | 2 + server/tests/api/single-pod.ts | 2 + shared/models/videos/video.model.ts | 2 + 13 files changed, 123 insertions(+), 86 deletions(-) create mode 100644 client/src/app/videos/+video-watch/video-download.component.html rename client/src/app/videos/+video-watch/{video-magnet.component.ts => video-download.component.ts} (66%) delete mode 100644 client/src/app/videos/+video-watch/video-magnet.component.html diff --git a/client/src/app/videos/+video-watch/video-download.component.html b/client/src/app/videos/+video-watch/video-download.component.html new file mode 100644 index 0000000000..ddc57e999b --- /dev/null +++ b/client/src/app/videos/+video-watch/video-download.component.html @@ -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> diff --git a/client/src/app/videos/+video-watch/video-magnet.component.ts b/client/src/app/videos/+video-watch/video-download.component.ts similarity index 66% rename from client/src/app/videos/+video-watch/video-magnet.component.ts rename to client/src/app/videos/+video-watch/video-download.component.ts index f9432e92cc..22149aa6be 100644 --- a/client/src/app/videos/+video-watch/video-magnet.component.ts +++ b/client/src/app/videos/+video-watch/video-download.component.ts @@ -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 diff --git a/client/src/app/videos/+video-watch/video-magnet.component.html b/client/src/app/videos/+video-watch/video-magnet.component.html deleted file mode 100644 index 484280c454..0000000000 --- a/client/src/app/videos/+video-watch/video-magnet.component.html +++ /dev/null @@ -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> diff --git a/client/src/app/videos/+video-watch/video-watch.component.html b/client/src/app/videos/+video-watch/video-watch.component.html index 88863131af..5d58273446 100644 --- a/client/src/app/videos/+video-watch/video-watch.component.html +++ b/client/src/app/videos/+video-watch/video-watch.component.html @@ -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> diff --git a/client/src/app/videos/+video-watch/video-watch.component.ts b/client/src/app/videos/+video-watch/video-watch.component.ts index bd98e877ca..651298c140 100644 --- a/client/src/app/videos/+video-watch/video-watch.component.ts +++ b/client/src/app/videos/+video-watch/video-watch.component.ts @@ -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 () { diff --git a/client/src/app/videos/+video-watch/video-watch.module.ts b/client/src/app/videos/+video-watch/video-watch.module.ts index 5f20b171e6..c6c1344ce2 100644 --- a/client/src/app/videos/+video-watch/video-watch.module.ts +++ b/client/src/app/videos/+video-watch/video-watch.module.ts @@ -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 ], diff --git a/client/src/assets/player/peertube-videojs-plugin.ts b/client/src/assets/player/peertube-videojs-plugin.ts index 7cf3ea6ccb..19490baf2b 100644 --- a/client/src/assets/player/peertube-videojs-plugin.ts +++ b/client/src/assets/player/peertube-videojs-plugin.ts @@ -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') diff --git a/server.ts b/server.ts index 72bb11e747..f50e5bad4b 100644 --- a/server.ts +++ b/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 diff --git a/server/models/video/video-interface.ts b/server/models/video/video-interface.ts index 1402df26a2..86ce84dd99 100644 --- a/server/models/video/video-interface.ts +++ b/server/models/video/video-interface.ts @@ -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 diff --git a/server/models/video/video.ts b/server/models/video/video.ts index 4bd8eb98f5..0b1af4d21e 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts @@ -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) +} diff --git a/server/tests/api/multiple-pods.ts b/server/tests/api/multiple-pods.ts index 6c11aace5f..e0ccb30582 100644 --- a/server/tests/api/multiple-pods.ts +++ b/server/tests/api/multiple-pods.ts @@ -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) diff --git a/server/tests/api/single-pod.ts b/server/tests/api/single-pod.ts index 82bc51a3eb..71017b2b30 100644 --- a/server/tests/api/single-pod.ts +++ b/server/tests/api/single-pod.ts @@ -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) diff --git a/shared/models/videos/video.model.ts b/shared/models/videos/video.model.ts index bbcada8454..8e47ac0696 100644 --- a/shared/models/videos/video.model.ts +++ b/shared/models/videos/video.model.ts @@ -3,6 +3,8 @@ export interface VideoFile { resolution: number resolutionLabel: string size: number // Bytes + torrentUrl: string + fileUrl: string } export interface Video { -- GitLab