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 000000000..ddc57e999 --- /dev/null +++ b/client/src/app/videos/+video-watch/video-download.component.html @@ -0,0 +1,30 @@ + 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 f9432e92c..22149aa6b 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 484280c45..000000000 --- a/client/src/app/videos/+video-watch/video-magnet.component.html +++ /dev/null @@ -1,20 +0,0 @@ - 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 88863131a..5d5827344 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 @@
  • - - Magnet + + Download
  • @@ -179,6 +179,6 @@ - + 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 bd98e877c..651298c14 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 5f20b171e..c6c1344ce 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 7cf3ea6cc..19490baf2 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 72bb11e74..f50e5bad4 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 1402df26a..86ce84dd9 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 @@ -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 4bd8eb98f..0b1af4d21 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 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 6c11aace5..e0ccb3058 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 82bc51a3e..71017b2b3 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 bbcada845..8e47ac069 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 {