Compare commits

...

44 Commits

Author SHA1 Message Date
DC
c549fcfc27 0.15.0 2016-09-16 01:01:32 -07:00
DC
45e838d4c3 Telemetry: add torrent stats 2016-09-16 00:54:07 -07:00
DC
64f49e4d4f Auto launch: start minimized on MacOS 2016-09-13 19:49:47 -07:00
DC
61caa90901 Auto launch: don't open a terminal on MacOS 2016-09-13 19:49:47 -07:00
Noam Okman
3e85289318 Pref: start automatically on login 2016-09-13 19:49:47 -07:00
Mathias Rasmussen
3a4906079b External player clean up (#914)
* minor `addSubtitles` clean up

* external player clean up
2016-09-12 17:46:48 -07:00
Kai Curtis
3edf21f457 Update mouse moved time on header hover (#919)
Previously, moving the mouse into the player window from the sides or
bottom would bring up the HUD, but moving the mouse in from the top
would not. With this commit, moving the mouse in from the top of the
window will also bring up the HUD.

Fixes feross/webtorrent-desktop#241
2016-09-12 17:46:23 -07:00
DC
785c44cd2a Integration test: torrent list 2016-09-08 23:55:37 -07:00
DC
1ad8a5313b Integration test: save failed screenshot comparisons 2016-09-08 19:13:14 -07:00
DC
967e161288 Integration test: screenshots 2016-09-08 19:10:28 -07:00
DC
fe8c3b190c Integration test: tape + spectron hello world 2016-09-08 19:10:28 -07:00
Benjamin Tan
993e7d77ad Fix notification click not working. (#912)
This was changed incorrectly in 2a1e987.
2016-09-08 16:16:43 -07:00
DC
e0be052df4 Fix Open Torrent Address modal
Fixes a bug introduced in 0.14.0: cicking OK works, but hitting Enter doesn't do anything
2016-09-07 13:28:49 -07:00
Adam Gotlib
d331bae548 Move error definitions to errors.js (#898) 2016-09-07 13:21:59 -07:00
Adam Gotlib
d88229694a Disable playback controls while in external player (#909) 2016-09-07 13:13:50 -07:00
Adam Gotlib
8da5b955d6 Make git ignore npm-debug.log (#896) 2016-09-05 20:04:21 -07:00
DC
54882679c1 Dedupe cast.js status handlers, fix #889 2016-09-04 15:10:39 -07:00
DC
f2007be1b0 Fix selectFiles error, fixes #891 2016-09-04 14:56:17 -07:00
DC
7a757f9e05 More info in torrentFileModtimes, fix #892 2016-09-04 14:27:22 -07:00
DC
678f961110 Fix Heading.js 2016-09-04 11:09:08 -07:00
DC
f0464c44fd Changelog fixes 2016-09-04 10:31:01 -07:00
DC
b323ee24c4 Design: forward/back buttons, remove bad CSS 2016-09-04 09:51:50 -07:00
DC
38aad2ee9c authors 2016-09-03 18:49:49 -07:00
DC
b69ca93d20 0.14.0 2016-09-03 18:49:49 -07:00
DC
e21a039e70 changelog 2016-09-03 18:49:49 -07:00
Adam Gotlib
11f8e428a0 Restore playback state when reopening player (#877) 2016-09-03 15:25:19 -07:00
Mathias Rasmussen
704455c432 Rename App.js -> app.js (#882) 2016-09-03 13:24:09 -07:00
Noam Okman
c25bee755c make "npm run watch" work on windows (#878) 2016-09-03 20:13:11 +02:00
Dan Flettre
373d598c29 Merge pull request #880 from feross/dc/design
Torrent list design
2016-09-02 21:30:36 -05:00
DC
b8effffa96 Delete unused defaultPlayFileIndex 2016-09-02 14:13:30 -07:00
DC
ca6a7917ce Design: progress bar styling 2016-09-02 12:25:55 -07:00
DC
033bdf7908 Design: removed hover/select brightness
All those gradients and brighness filters toggling on and off were getting annoying
2016-09-02 12:08:57 -07:00
DC
6fe03aa325 Material UI: upgrade modals
Also clean up the Create Torrent page, delete some redundant CSS, prevent click-and-drag inside a TextField from moving the whole window, and make all label and input fonts  a consistent 14px size.
2016-09-02 02:30:37 -07:00
DC
b93f41f564 Telemetry: fix stacktrace redaction 2016-09-01 20:05:37 -07:00
DC
3f6cc97a02 Style: no more var 2016-09-01 19:52:37 -07:00
DC
0bda5358bd Style: extra linting 2016-09-01 19:41:50 -07:00
DC
e0af554caa Material UI: consistent JSX style 2016-09-01 19:38:21 -07:00
DC
b98f8476f5 Material UI: make file names consistent 2016-09-01 19:38:21 -07:00
Adam Gotlib
30732305ff Add playlists feature (#871)
* Open multi-file torrents as playlists

* Add `repeat` and `shuffle` options to the player

* Autoplay first file in torrent

* replaces `pickFileToPlay` feature
* when reopening player, restores the most recently viewed file

* Add playlist navigation buttons to Windows thumbar

* Remove `repeat` and `shuffle` options

This reverts commit 9284122461.

* Play files in order they appear in torrent

* Clean up playlists code
2016-09-01 19:18:48 -07:00
Adam Gotlib
14102ab3e6 Fix error on Windows caused by setBadge (#867) 2016-09-01 16:00:57 -07:00
Mathias Rasmussen
df16b14586 early telemetry (#870) 2016-09-01 15:58:02 -07:00
Mathias Rasmussen
9dcbc1b1f6 ensure that torrent file metadata is present (#869) 2016-09-01 15:54:37 -07:00
PurgingPanda
d806fd502f Added progress bar to the metadata on the overview (#844)
* Added progress bar to Torrent metadata on the overview.

* Added progress bar to Torrent metadata on the overview.

* Made progress bar smaller

As proposed by dcposh.
2016-09-01 14:43:28 -07:00
DC
db650caf18 changelog 2016-08-31 18:54:02 -07:00
86 changed files with 1901 additions and 1029 deletions

2
.gitignore vendored
View File

@@ -1,4 +1,4 @@
node_modules/ node_modules/
build/ build/
dist/ dist/
npm-debug.log.* npm-debug.log*

View File

@@ -1,4 +1,4 @@
language: node_js language: node_js
node_js: node_js:
- 'node' - 'node'
install: npm install standard depcheck install: npm install standard depcheck walk-sync

View File

@@ -31,5 +31,6 @@
- Jason Kurian (jasonk92@gmail.com) - Jason Kurian (jasonk92@gmail.com)
- Vamsi Krishna Avula (vamsi_ism@outlook.com) - Vamsi Krishna Avula (vamsi_ism@outlook.com)
- Noam Okman (noamokman@gmail.com) - Noam Okman (noamokman@gmail.com)
- PurgingPanda (t3ch0wn3r@gmail.com)
#### Generated by bin/update-authors.sh. #### Generated by bin/update-authors.sh.

View File

@@ -1,9 +1,29 @@
# WebTorrent Desktop Version History # WebTorrent Desktop Version History
## v0.14.0 - 2016-09-03
### Added
- Autoplay through all files in a torrent (#871)
- Torrents now have a progress bar (#844)
### Changed
- Modals now use Material UI
- Torrent list style improvements
### Fixed
- Fix App.js crash in Linux (#882)
- Fix error on Windows caused by `setBadge` (#867)
- Don't crash when restarting after adding a magnet link (#869)
- Restore playback state when reopening player (#877)
## v0.13.1 - 2016-08-31
### Fixed
- Fixed the Create Torrent page
## v0.13.0 - 2016-08-31 ## v0.13.0 - 2016-08-31
### Added ### Added
- Torrent progress bar
- Support .m4a audio - Support .m4a audio
- Better telemetry: log error versions, report more types of errors - Better telemetry: log error versions, report more types of errors

46
bin/extra-lint.js Executable file
View File

@@ -0,0 +1,46 @@
#!/usr/bin/env node
const walkSync = require('walk-sync')
const fs = require('fs')
const path = require('path')
let hasErrors = false
// Find all Javascript source files
var files = walkSync('src', {globs: ['**/*.js']})
console.log('Running extra-lint on ' + files.length + ' files...')
// Read each file, line by line
files.forEach(function (file) {
var filepath = path.join('src', file)
var lines = fs.readFileSync(filepath, 'utf8').split('\n')
lines.forEach(function (line, i) {
var error
// Consistent JSX tag closing
if (line.match(/' {2}\/> *$/) ||
line.match('[^ ]/> *$') ||
line.match(' > *$')) {
error = 'JSX tag spacing'
}
// No lines over 100 characters
if (line.length > 100) {
error = 'Line >100 chars'
}
if (line.match(/^var /) || line.match(/ var /)) {
error = 'Use const or let'
}
if (error) {
let name = path.basename(file)
console.log('%s:%d - %s:\n%s', name, i + 1, error, line)
hasErrors = true
}
})
})
if (hasErrors) process.exit(1)
else console.log('Looks good!')

View File

@@ -1,7 +1,7 @@
{ {
"name": "webtorrent-desktop", "name": "webtorrent-desktop",
"description": "WebTorrent, the streaming torrent client. For Mac, Windows, and Linux.", "description": "WebTorrent, the streaming torrent client. For Mac, Windows, and Linux.",
"version": "0.13.1", "version": "0.15.0",
"author": { "author": {
"name": "WebTorrent, LLC", "name": "WebTorrent, LLC",
"email": "feross@webtorrent.io", "email": "feross@webtorrent.io",
@@ -16,13 +16,15 @@
"dependencies": { "dependencies": {
"airplayer": "^2.0.0", "airplayer": "^2.0.0",
"application-config": "^1.0.0", "application-config": "^1.0.0",
"auto-launch": "^4.0.1",
"bitfield": "^1.0.2", "bitfield": "^1.0.2",
"chromecasts": "^1.8.0", "chromecasts": "^1.8.0",
"create-torrent": "^3.24.5", "create-torrent": "^3.24.5",
"deep-equal": "^1.0.1", "deep-equal": "^1.0.1",
"dlnacasts": "^0.1.0", "dlnacasts": "^0.1.0",
"drag-drop": "^2.12.1", "drag-drop": "^2.12.1",
"electron": "1.3.3", "electron": "1.3.5",
"es6-error": "^3.0.1",
"fs-extra": "^0.30.0", "fs-extra": "^0.30.0",
"iso-639-1": "^1.2.1", "iso-639-1": "^1.2.1",
"languagedetect": "^1.1.1", "languagedetect": "^1.1.1",
@@ -65,7 +67,10 @@
"plist": "^2.0.1", "plist": "^2.0.1",
"rimraf": "^2.5.2", "rimraf": "^2.5.2",
"run-series": "^1.1.4", "run-series": "^1.1.4",
"standard": "*" "spectron": "^3.3.0",
"standard": "*",
"tape": "^4.6.0",
"walk-sync": "^0.3.1"
}, },
"engines": { "engines": {
"node": ">=4.0.0" "node": ">=4.0.0"
@@ -98,9 +103,10 @@
"package": "node ./bin/package.js", "package": "node ./bin/package.js",
"prepublish": "npm run build", "prepublish": "npm run build",
"start": "npm run build && electron .", "start": "npm run build && electron .",
"test": "standard && depcheck --ignores=babel-cli,nodemon,gh-release --ignore-dirs=build,dist", "integration-test": "npm run build && node ./test",
"test": "standard && depcheck --ignores=babel-cli,nodemon,gh-release --ignore-dirs=build,dist && node ./bin/extra-lint.js",
"gh-release": "gh-release", "gh-release": "gh-release",
"update-authors": "./bin/update-authors.sh", "update-authors": "./bin/update-authors.sh",
"watch": "nodemon --exec 'npm run start' --ext js,pug,css" "watch": "nodemon --exec \"npm run start\" --ext js,pug,css"
} }
} }

View File

@@ -1,12 +1,22 @@
var appConfig = require('application-config')('WebTorrent') const appConfig = require('application-config')('WebTorrent')
var fs = require('fs') const fs = require('fs')
var path = require('path') const path = require('path')
const electron = require('electron')
var APP_NAME = 'WebTorrent' const APP_NAME = 'WebTorrent'
var APP_TEAM = 'WebTorrent, LLC' const APP_TEAM = 'WebTorrent, LLC'
var APP_VERSION = require('../package.json').version const APP_VERSION = require('../package.json').version
var PORTABLE_PATH = path.join(path.dirname(process.execPath), 'Portable Settings') const IS_TEST = isTest()
const PORTABLE_PATH = IS_TEST
? path.join(__dirname, '../test/tempTestData')
: path.join(path.dirname(process.execPath), 'Portable Settings')
const IS_PORTABLE = isPortable()
const IS_PRODUCTION = isProduction()
console.log('Production: %s portable: %s test: %s',
IS_PRODUCTION, IS_PORTABLE, IS_TEST)
if (IS_PORTABLE) console.log('Portable path: %s', PORTABLE_PATH)
module.exports = { module.exports = {
ANNOUNCEMENT_URL: 'https://webtorrent.io/desktop/announcement', ANNOUNCEMENT_URL: 'https://webtorrent.io/desktop/announcement',
@@ -26,26 +36,31 @@ module.exports = {
DEFAULT_TORRENTS: [ DEFAULT_TORRENTS: [
{ {
testID: 'bbb',
name: 'Big Buck Bunny', name: 'Big Buck Bunny',
posterFileName: 'bigBuckBunny.jpg', posterFileName: 'bigBuckBunny.jpg',
torrentFileName: 'bigBuckBunny.torrent' torrentFileName: 'bigBuckBunny.torrent'
}, },
{ {
testID: 'cosmos',
name: 'Cosmos Laundromat (Preview)', name: 'Cosmos Laundromat (Preview)',
posterFileName: 'cosmosLaundromat.jpg', posterFileName: 'cosmosLaundromat.jpg',
torrentFileName: 'cosmosLaundromat.torrent' torrentFileName: 'cosmosLaundromat.torrent'
}, },
{ {
testID: 'sintel',
name: 'Sintel', name: 'Sintel',
posterFileName: 'sintel.jpg', posterFileName: 'sintel.jpg',
torrentFileName: 'sintel.torrent' torrentFileName: 'sintel.torrent'
}, },
{ {
testID: 'tears',
name: 'Tears of Steel', name: 'Tears of Steel',
posterFileName: 'tearsOfSteel.jpg', posterFileName: 'tearsOfSteel.jpg',
torrentFileName: 'tearsOfSteel.torrent' torrentFileName: 'tearsOfSteel.torrent'
}, },
{ {
testID: 'wired',
name: 'The WIRED CD - Rip. Sample. Mash. Share.', name: 'The WIRED CD - Rip. Sample. Mash. Share.',
posterFileName: 'wiredCd.jpg', posterFileName: 'wiredCd.jpg',
torrentFileName: 'wiredCd.torrent' torrentFileName: 'wiredCd.torrent'
@@ -62,8 +77,9 @@ module.exports = {
HOME_PAGE_URL: 'https://webtorrent.io', HOME_PAGE_URL: 'https://webtorrent.io',
IS_PORTABLE: isPortable(), IS_PORTABLE: IS_PORTABLE,
IS_PRODUCTION: isProduction(), IS_PRODUCTION: IS_PRODUCTION,
IS_TEST: IS_TEST,
POSTER_PATH: path.join(getConfigPath(), 'Posters'), POSTER_PATH: path.join(getConfigPath(), 'Posters'),
ROOT_PATH: path.join(__dirname, '..'), ROOT_PATH: path.join(__dirname, '..'),
@@ -79,7 +95,7 @@ module.exports = {
} }
function getConfigPath () { function getConfigPath () {
if (isPortable()) { if (IS_PORTABLE) {
return PORTABLE_PATH return PORTABLE_PATH
} else { } else {
return path.dirname(appConfig.filePath) return path.dirname(appConfig.filePath)
@@ -89,22 +105,31 @@ function getConfigPath () {
function getDefaultDownloadPath () { function getDefaultDownloadPath () {
if (!process || !process.type) { if (!process || !process.type) {
return '' return ''
} } else if (IS_PORTABLE) {
if (isPortable()) {
return path.join(getConfigPath(), 'Downloads') return path.join(getConfigPath(), 'Downloads')
} else {
return getPath('downloads')
} }
}
var electron = require('electron') function getPath (key) {
if (process.type === 'renderer') {
return electron.remote.app.getPath(key)
} else {
return electron.app.getPath(key)
}
}
return process.type === 'renderer' function isTest () {
? electron.remote.app.getPath('downloads') return process.env.NODE_ENV === 'test'
: electron.app.getPath('downloads')
} }
function isPortable () { function isPortable () {
if (IS_TEST) {
return true
}
try { try {
return process.platform === 'win32' && isProduction() && !!fs.statSync(PORTABLE_PATH) return process.platform === 'win32' && IS_PRODUCTION && !!fs.statSync(PORTABLE_PATH)
} catch (err) { } catch (err) {
return false return false
} }

View File

@@ -2,8 +2,8 @@ module.exports = {
init init
} }
var config = require('./config') const config = require('./config')
var electron = require('electron') const electron = require('electron')
function init () { function init () {
electron.crashReporter.start({ electron.crashReporter.start({

View File

@@ -2,12 +2,12 @@ module.exports = {
init init
} }
var electron = require('electron') const electron = require('electron')
var config = require('../config') const config = require('../config')
var log = require('./log') const log = require('./log')
var ANNOUNCEMENT_URL = config.ANNOUNCEMENT_URL + const ANNOUNCEMENT_URL = config.ANNOUNCEMENT_URL +
'?version=' + config.APP_VERSION + '?version=' + config.APP_VERSION +
'&platform=' + process.platform '&platform=' + process.platform
@@ -26,7 +26,7 @@ var ANNOUNCEMENT_URL = config.ANNOUNCEMENT_URL +
* } * }
*/ */
function init () { function init () {
var get = require('simple-get') const get = require('simple-get')
get.concat(ANNOUNCEMENT_URL, onResponse) get.concat(ANNOUNCEMENT_URL, onResponse)
} }

View File

@@ -6,10 +6,10 @@ module.exports = {
openFiles openFiles
} }
var electron = require('electron') const electron = require('electron')
var log = require('./log') const log = require('./log')
var windows = require('./windows') const windows = require('./windows')
/** /**
* Show open dialog to create a single-file torrent. * Show open dialog to create a single-file torrent.
@@ -17,7 +17,7 @@ var windows = require('./windows')
function openSeedFile () { function openSeedFile () {
if (!windows.main.win) return if (!windows.main.win) return
log('openSeedFile') log('openSeedFile')
var opts = { const opts = {
title: 'Select a file for the torrent.', title: 'Select a file for the torrent.',
properties: [ 'openFile' ] properties: [ 'openFile' ]
} }
@@ -37,7 +37,7 @@ function openSeedFile () {
function openSeedDirectory () { function openSeedDirectory () {
if (!windows.main.win) return if (!windows.main.win) return
log('openSeedDirectory') log('openSeedDirectory')
var opts = process.platform === 'darwin' const opts = process.platform === 'darwin'
? { ? {
title: 'Select a file or folder for the torrent.', title: 'Select a file or folder for the torrent.',
properties: [ 'openFile', 'openDirectory' ] properties: [ 'openFile', 'openDirectory' ]
@@ -61,7 +61,7 @@ function openSeedDirectory () {
function openFiles () { function openFiles () {
if (!windows.main.win) return if (!windows.main.win) return
log('openFiles') log('openFiles')
var opts = process.platform === 'darwin' const opts = process.platform === 'darwin'
? { ? {
title: 'Select a file or folder to add.', title: 'Select a file or folder to add.',
properties: [ 'openFile', 'openDirectory' ] properties: [ 'openFile', 'openDirectory' ]
@@ -84,7 +84,7 @@ function openFiles () {
function openTorrentFile () { function openTorrentFile () {
if (!windows.main.win) return if (!windows.main.win) return
log('openTorrentFile') log('openTorrentFile')
var opts = { const opts = {
title: 'Select a .torrent file.', title: 'Select a .torrent file.',
filters: [{ name: 'Torrent Files', extensions: ['torrent'] }], filters: [{ name: 'Torrent Files', extensions: ['torrent'] }],
properties: [ 'openFile', 'multiSelections' ] properties: [ 'openFile', 'multiSelections' ]

View File

@@ -4,19 +4,17 @@ module.exports = {
setBadge setBadge
} }
var electron = require('electron') const {app, Menu} = require('electron')
var app = electron.app const dialog = require('./dialog')
const log = require('./log')
var dialog = require('./dialog')
var log = require('./log')
/** /**
* Add a right-click menu to the dock icon. (Mac) * Add a right-click menu to the dock icon. (Mac)
*/ */
function init () { function init () {
if (!app.dock) return if (!app.dock) return
var menu = electron.Menu.buildFromTemplate(getMenuTemplate()) const menu = Menu.buildFromTemplate(getMenuTemplate())
app.dock.setMenu(menu) app.dock.setMenu(menu)
} }
@@ -33,8 +31,11 @@ function downloadFinished (path) {
* Display a counter badge for the app. (Mac, Linux) * Display a counter badge for the app. (Mac, Linux)
*/ */
function setBadge (count) { function setBadge (count) {
log(`setBadge: ${count}`) if (process.platform === 'darwin' ||
app.setBadgeCount(Number(count)) process.platform === 'linux' && app.isUnityRunning()) {
log(`setBadge: ${count}`)
app.setBadgeCount(Number(count))
}
} }
function getMenuTemplate () { function getMenuTemplate () {

View File

@@ -4,29 +4,36 @@ module.exports = {
checkInstall checkInstall
} }
var cp = require('child_process') const cp = require('child_process')
var vlcCommand = require('vlc-command') const path = require('path')
const vlcCommand = require('vlc-command')
var log = require('./log') const log = require('./log')
var windows = require('./windows') const windows = require('./windows')
// holds a ChildProcess while we're playing a video in an external player, null otherwise // holds a ChildProcess while we're playing a video in an external player, null otherwise
var proc let proc = null
function checkInstall (path, cb) { function checkInstall (playerPath, cb) {
// check for VLC if external player has not been specified by the user // check for VLC if external player has not been specified by the user
// otherwise assume the player is installed // otherwise assume the player is installed
if (path == null) return vlcCommand((err) => cb(!err)) if (playerPath == null) return vlcCommand((err) => cb(!err))
process.nextTick(() => cb(true)) process.nextTick(() => cb(true))
} }
function spawn (path, url, title) { function spawn (playerPath, url, title) {
if (path != null) return spawnExternal(path, [url]) if (playerPath != null) return spawnExternal(playerPath, [url])
// Try to find and use VLC if external player is not specified // Try to find and use VLC if external player is not specified
vlcCommand(function (err, vlcPath) { vlcCommand(function (err, vlcPath) {
if (err) return windows.main.dispatch('externalPlayerNotFound') if (err) return windows.main.dispatch('externalPlayerNotFound')
var args = ['--play-and-exit', '--video-on-top', '--quiet', `--meta-title=${JSON.stringify(title)}`, url] const args = [
'--play-and-exit',
'--video-on-top',
'--quiet',
`--meta-title=${JSON.stringify(title)}`,
url
]
spawnExternal(vlcPath, args) spawnExternal(vlcPath, args)
}) })
} }
@@ -38,13 +45,18 @@ function kill () {
proc = null proc = null
} }
function spawnExternal (path, args) { function spawnExternal (playerPath, args) {
log('Running external media player:', path + ' ' + args.join(' ')) log('Running external media player:', playerPath + ' ' + args.join(' '))
proc = cp.spawn(path, args, {stdio: 'ignore'}) if (path.extname(playerPath) === '.app') {
// Mac: Use executable in packaged .app bundle
playerPath += '/Contents/MacOS/' + path.basename(playerPath, '.app')
}
proc = cp.spawn(playerPath, args, {stdio: 'ignore'})
// If it works, close the modal after a second // If it works, close the modal after a second
var closeModalTimeout = setTimeout(() => const closeModalTimeout = setTimeout(() =>
windows.main.dispatch('exitModal'), 1000) windows.main.dispatch('exitModal'), 1000)
proc.on('close', function (code) { proc.on('close', function (code) {

View File

@@ -3,8 +3,8 @@ module.exports = {
uninstall uninstall
} }
var config = require('../config') const config = require('../config')
var path = require('path') const path = require('path')
function install () { function install () {
if (process.platform === 'darwin') { if (process.platform === 'darwin') {
@@ -31,8 +31,8 @@ function uninstall () {
} }
function installDarwin () { function installDarwin () {
var electron = require('electron') const electron = require('electron')
var app = electron.app const app = electron.app
// On Mac, only protocols that are listed in `Info.plist` can be set as the // On Mac, only protocols that are listed in `Info.plist` can be set as the
// default handler at runtime. // default handler at runtime.
@@ -44,18 +44,18 @@ function installDarwin () {
function uninstallDarwin () {} function uninstallDarwin () {}
var EXEC_COMMAND = [ process.execPath ] const EXEC_COMMAND = [ process.execPath ]
if (!config.IS_PRODUCTION) { if (!config.IS_PRODUCTION) {
EXEC_COMMAND.push(config.ROOT_PATH) EXEC_COMMAND.push(config.ROOT_PATH)
} }
function installWin32 () { function installWin32 () {
var Registry = require('winreg') const Registry = require('winreg')
var log = require('./log') const log = require('./log')
var iconPath = path.join( const iconPath = path.join(
process.resourcesPath, 'app.asar.unpacked', 'static', 'WebTorrentFile.ico' process.resourcesPath, 'app.asar.unpacked', 'static', 'WebTorrentFile.ico'
) )
registerProtocolHandlerWin32( registerProtocolHandlerWin32(
@@ -100,7 +100,7 @@ function installWin32 () {
*/ */
function registerProtocolHandlerWin32 (protocol, name, icon, command) { function registerProtocolHandlerWin32 (protocol, name, icon, command) {
var protocolKey = new Registry({ const protocolKey = new Registry({
hive: Registry.HKCU, // HKEY_CURRENT_USER hive: Registry.HKCU, // HKEY_CURRENT_USER
key: '\\Software\\Classes\\' + protocol key: '\\Software\\Classes\\' + protocol
}) })
@@ -120,7 +120,7 @@ function installWin32 () {
function setIcon (err) { function setIcon (err) {
if (err) log.error(err.message) if (err) log.error(err.message)
var iconKey = new Registry({ const iconKey = new Registry({
hive: Registry.HKCU, hive: Registry.HKCU,
key: '\\Software\\Classes\\' + protocol + '\\DefaultIcon' key: '\\Software\\Classes\\' + protocol + '\\DefaultIcon'
}) })
@@ -130,7 +130,7 @@ function installWin32 () {
function setCommand (err) { function setCommand (err) {
if (err) log.error(err.message) if (err) log.error(err.message)
var commandKey = new Registry({ const commandKey = new Registry({
hive: Registry.HKCU, hive: Registry.HKCU,
key: '\\Software\\Classes\\' + protocol + '\\shell\\open\\command' key: '\\Software\\Classes\\' + protocol + '\\shell\\open\\command'
}) })
@@ -161,7 +161,7 @@ function installWin32 () {
setExt() setExt()
function setExt () { function setExt () {
var extKey = new Registry({ const extKey = new Registry({
hive: Registry.HKCU, // HKEY_CURRENT_USER hive: Registry.HKCU, // HKEY_CURRENT_USER
key: '\\Software\\Classes\\' + ext key: '\\Software\\Classes\\' + ext
}) })
@@ -171,7 +171,7 @@ function installWin32 () {
function setId (err) { function setId (err) {
if (err) log.error(err.message) if (err) log.error(err.message)
var idKey = new Registry({ const idKey = new Registry({
hive: Registry.HKCU, hive: Registry.HKCU,
key: '\\Software\\Classes\\' + id key: '\\Software\\Classes\\' + id
}) })
@@ -181,7 +181,7 @@ function installWin32 () {
function setIcon (err) { function setIcon (err) {
if (err) log.error(err.message) if (err) log.error(err.message)
var iconKey = new Registry({ const iconKey = new Registry({
hive: Registry.HKCU, hive: Registry.HKCU,
key: '\\Software\\Classes\\' + id + '\\DefaultIcon' key: '\\Software\\Classes\\' + id + '\\DefaultIcon'
}) })
@@ -191,7 +191,7 @@ function installWin32 () {
function setCommand (err) { function setCommand (err) {
if (err) log.error(err.message) if (err) log.error(err.message)
var commandKey = new Registry({ const commandKey = new Registry({
hive: Registry.HKCU, hive: Registry.HKCU,
key: '\\Software\\Classes\\' + id + '\\shell\\open\\command' key: '\\Software\\Classes\\' + id + '\\shell\\open\\command'
}) })
@@ -205,7 +205,7 @@ function installWin32 () {
} }
function uninstallWin32 () { function uninstallWin32 () {
var Registry = require('winreg') const Registry = require('winreg')
unregisterProtocolHandlerWin32('magnet', EXEC_COMMAND) unregisterProtocolHandlerWin32('magnet', EXEC_COMMAND)
unregisterProtocolHandlerWin32('stream-magnet', EXEC_COMMAND) unregisterProtocolHandlerWin32('stream-magnet', EXEC_COMMAND)
@@ -215,7 +215,7 @@ function uninstallWin32 () {
getCommand() getCommand()
function getCommand () { function getCommand () {
var commandKey = new Registry({ const commandKey = new Registry({
hive: Registry.HKCU, // HKEY_CURRENT_USER hive: Registry.HKCU, // HKEY_CURRENT_USER
key: '\\Software\\Classes\\' + protocol + '\\shell\\open\\command' key: '\\Software\\Classes\\' + protocol + '\\shell\\open\\command'
}) })
@@ -227,7 +227,7 @@ function uninstallWin32 () {
} }
function destroyProtocol () { function destroyProtocol () {
var protocolKey = new Registry({ const protocolKey = new Registry({
hive: Registry.HKCU, hive: Registry.HKCU,
key: '\\Software\\Classes\\' + protocol key: '\\Software\\Classes\\' + protocol
}) })
@@ -239,7 +239,7 @@ function uninstallWin32 () {
eraseId() eraseId()
function eraseId () { function eraseId () {
var idKey = new Registry({ const idKey = new Registry({
hive: Registry.HKCU, // HKEY_CURRENT_USER hive: Registry.HKCU, // HKEY_CURRENT_USER
key: '\\Software\\Classes\\' + id key: '\\Software\\Classes\\' + id
}) })
@@ -247,7 +247,7 @@ function uninstallWin32 () {
} }
function getExt () { function getExt () {
var extKey = new Registry({ const extKey = new Registry({
hive: Registry.HKCU, hive: Registry.HKCU,
key: '\\Software\\Classes\\' + ext key: '\\Software\\Classes\\' + ext
}) })
@@ -259,7 +259,7 @@ function uninstallWin32 () {
} }
function destroyExt () { function destroyExt () {
var extKey = new Registry({ const extKey = new Registry({
hive: Registry.HKCU, // HKEY_CURRENT_USER hive: Registry.HKCU, // HKEY_CURRENT_USER
key: '\\Software\\Classes\\' + ext key: '\\Software\\Classes\\' + ext
}) })
@@ -273,12 +273,12 @@ function commandToArgs (command) {
} }
function installLinux () { function installLinux () {
var fs = require('fs-extra') const fs = require('fs-extra')
var os = require('os') const os = require('os')
var path = require('path') const path = require('path')
var config = require('../config') const config = require('../config')
var log = require('./log') const log = require('./log')
// Do not install in user dir if running on system // Do not install in user dir if running on system
if (/^\/opt/.test(process.execPath)) return if (/^\/opt/.test(process.execPath)) return
@@ -287,7 +287,7 @@ function installLinux () {
installIconFile() installIconFile()
function installDesktopFile () { function installDesktopFile () {
var templatePath = path.join( const templatePath = path.join(
config.STATIC_PATH, 'linux', 'webtorrent-desktop.desktop' config.STATIC_PATH, 'linux', 'webtorrent-desktop.desktop'
) )
fs.readFile(templatePath, 'utf8', writeDesktopFile) fs.readFile(templatePath, 'utf8', writeDesktopFile)
@@ -296,7 +296,7 @@ function installLinux () {
function writeDesktopFile (err, desktopFile) { function writeDesktopFile (err, desktopFile) {
if (err) return log.error(err.message) if (err) return log.error(err.message)
var appPath = config.IS_PRODUCTION const appPath = config.IS_PRODUCTION
? path.dirname(process.execPath) ? path.dirname(process.execPath)
: config.ROOT_PATH : config.ROOT_PATH
@@ -305,7 +305,7 @@ function installLinux () {
desktopFile = desktopFile.replace(/\$EXEC_PATH/g, EXEC_COMMAND.join(' ')) desktopFile = desktopFile.replace(/\$EXEC_PATH/g, EXEC_COMMAND.join(' '))
desktopFile = desktopFile.replace(/\$TRY_EXEC_PATH/g, process.execPath) desktopFile = desktopFile.replace(/\$TRY_EXEC_PATH/g, process.execPath)
var desktopFilePath = path.join( const desktopFilePath = path.join(
os.homedir(), os.homedir(),
'.local', '.local',
'share', 'share',
@@ -319,14 +319,14 @@ function installLinux () {
} }
function installIconFile () { function installIconFile () {
var iconStaticPath = path.join(config.STATIC_PATH, 'WebTorrent.png') const iconStaticPath = path.join(config.STATIC_PATH, 'WebTorrent.png')
fs.readFile(iconStaticPath, writeIconFile) fs.readFile(iconStaticPath, writeIconFile)
} }
function writeIconFile (err, iconFile) { function writeIconFile (err, iconFile) {
if (err) return log.error(err.message) if (err) return log.error(err.message)
var iconFilePath = path.join( const iconFilePath = path.join(
os.homedir(), os.homedir(),
'.local', '.local',
'share', 'share',
@@ -341,11 +341,11 @@ function installLinux () {
} }
function uninstallLinux () { function uninstallLinux () {
var os = require('os') const os = require('os')
var path = require('path') const path = require('path')
var fs = require('fs-extra') const fs = require('fs-extra')
var desktopFilePath = path.join( const desktopFilePath = path.join(
os.homedir(), os.homedir(),
'.local', '.local',
'share', 'share',
@@ -354,7 +354,7 @@ function uninstallLinux () {
) )
fs.removeSync(desktopFilePath) fs.removeSync(desktopFilePath)
var iconFilePath = path.join( const iconFilePath = path.join(
os.homedir(), os.homedir(),
'.local', '.local',
'share', 'share',

View File

@@ -1,26 +1,30 @@
console.time('init') console.time('init')
var electron = require('electron') const electron = require('electron')
const app = electron.app
const ipcMain = electron.ipcMain
var app = electron.app const announcement = require('./announcement')
var ipcMain = electron.ipcMain const config = require('../config')
const crashReporter = require('../crash-reporter')
const dialog = require('./dialog')
const dock = require('./dock')
const ipc = require('./ipc')
const log = require('./log')
const menu = require('./menu')
const squirrelWin32 = require('./squirrel-win32')
const tray = require('./tray')
const updater = require('./updater')
const userTasks = require('./user-tasks')
const windows = require('./windows')
var announcement = require('./announcement') let shouldQuit = false
var config = require('../config') let argv = sliceArgv(process.argv)
var crashReporter = require('../crash-reporter')
var dialog = require('./dialog')
var dock = require('./dock')
var ipc = require('./ipc')
var log = require('./log')
var menu = require('./menu')
var squirrelWin32 = require('./squirrel-win32')
var tray = require('./tray')
var updater = require('./updater')
var userTasks = require('./user-tasks')
var windows = require('./windows')
var shouldQuit = false // Start the app without showing the main window when auto launching on login
var argv = sliceArgv(process.argv) // (On Windows and Linux, we get a flag. On MacOS, we get special API.)
const hidden = argv.includes('--hidden') ||
(process.platform === 'darwin' && app.getLoginItemSettings().wasOpenedAsHidden)
if (config.IS_PRODUCTION) { if (config.IS_PRODUCTION) {
// When Electron is running in production mode (packaged app), then run React // When Electron is running in production mode (packaged app), then run React
@@ -51,7 +55,7 @@ function init () {
app.setPath('userData', config.CONFIG_PATH) app.setPath('userData', config.CONFIG_PATH)
} }
var isReady = false // app ready, windows can be created let isReady = false // app ready, windows can be created
app.ipcReady = false // main window has finished loading and IPC is ready app.ipcReady = false // main window has finished loading and IPC is ready
app.isQuitting = false app.isQuitting = false
@@ -68,7 +72,7 @@ function init () {
app.on('ready', function () { app.on('ready', function () {
isReady = true isReady = true
windows.main.init() windows.main.init({hidden: hidden})
windows.webtorrent.init() windows.webtorrent.init()
menu.init() menu.init()
@@ -78,7 +82,7 @@ function init () {
// Report uncaught exceptions // Report uncaught exceptions
process.on('uncaughtException', (err) => { process.on('uncaughtException', (err) => {
console.error(err) console.error(err)
var error = {message: err.message, stack: err.stack} const error = {message: err.message, stack: err.stack}
windows.main.dispatch('uncaughtError', 'main', error) windows.main.dispatch('uncaughtError', 'main', error)
}) })
}) })
@@ -148,7 +152,7 @@ function sliceArgv (argv) {
} }
function processArgv (argv) { function processArgv (argv) {
var torrentIds = [] let torrentIds = []
argv.forEach(function (arg) { argv.forEach(function (arg) {
if (arg === '-n') { if (arg === '-n') {
dialog.openSeedDirectory() dialog.openSeedDirectory()
@@ -156,9 +160,15 @@ function processArgv (argv) {
dialog.openTorrentFile() dialog.openTorrentFile()
} else if (arg === '-u') { } else if (arg === '-u') {
dialog.openTorrentAddress() dialog.openTorrentAddress()
} else if (arg === '--hidden') {
// Ignore hidden argument, already being handled
} else if (arg.startsWith('-psn')) { } else if (arg.startsWith('-psn')) {
// Ignore Mac launchd "process serial number" argument // Ignore Mac launchd "process serial number" argument
// Issue: https://github.com/feross/webtorrent-desktop/issues/214 // Issue: https://github.com/feross/webtorrent-desktop/issues/214
} else if (arg.startsWith('--')) {
// Ignore Spectron flags
} else if (arg === 'data:,') {
// Ignore weird Spectron argument
} else if (arg !== '.') { } else if (arg !== '.') {
// Ignore '.' argument, which gets misinterpreted as a torrent id, when a // Ignore '.' argument, which gets misinterpreted as a torrent id, when a
// development copy of WebTorrent is started while a production version is // development copy of WebTorrent is started while a production version is

View File

@@ -2,27 +2,28 @@ module.exports = {
init init
} }
var electron = require('electron') const electron = require('electron')
var app = electron.app const app = electron.app
var dialog = require('./dialog') const dialog = require('./dialog')
var dock = require('./dock') const dock = require('./dock')
var handlers = require('./handlers') const handlers = require('./handlers')
var log = require('./log') const log = require('./log')
var menu = require('./menu') const menu = require('./menu')
var powerSaveBlocker = require('./power-save-blocker') const powerSaveBlocker = require('./power-save-blocker')
var shell = require('./shell') const shell = require('./shell')
var shortcuts = require('./shortcuts') const shortcuts = require('./shortcuts')
var externalPlayer = require('./external-player') const externalPlayer = require('./external-player')
var windows = require('./windows') const windows = require('./windows')
var thumbar = require('./thumbar') const thumbar = require('./thumbar')
const startup = require('./startup')
// Messages from the main process, to be sent once the WebTorrent process starts // Messages from the main process, to be sent once the WebTorrent process starts
var messageQueueMainToWebTorrent = [] const messageQueueMainToWebTorrent = []
function init () { function init () {
var ipc = electron.ipcMain const ipc = electron.ipcMain
ipc.once('ipcReady', function (e) { ipc.once('ipcReady', function (e) {
app.ipcReady = true app.ipcReady = true
@@ -58,14 +59,19 @@ function init () {
*/ */
ipc.on('onPlayerOpen', function () { ipc.on('onPlayerOpen', function () {
menu.setPlayerOpen(true) menu.togglePlaybackControls(true)
powerSaveBlocker.enable() powerSaveBlocker.enable()
shortcuts.enable() shortcuts.enable()
thumbar.enable() thumbar.enable()
}) })
ipc.on('onPlayerUpdate', function (e, ...args) {
menu.onPlayerUpdate(...args)
thumbar.onPlayerUpdate(...args)
})
ipc.on('onPlayerClose', function () { ipc.on('onPlayerClose', function () {
menu.setPlayerOpen(false) menu.togglePlaybackControls(false)
powerSaveBlocker.disable() powerSaveBlocker.disable()
shortcuts.disable() shortcuts.disable()
thumbar.disable() thumbar.disable()
@@ -97,11 +103,19 @@ function init () {
else handlers.uninstall() else handlers.uninstall()
}) })
/**
* Startup
*/
ipc.on('setStartup', (e, flag) => {
if (flag) startup.install()
else startup.uninstall()
})
/** /**
* Windows: Main * Windows: Main
*/ */
var main = windows.main const main = windows.main
ipc.on('setAspectRatio', (e, ...args) => main.setAspectRatio(...args)) ipc.on('setAspectRatio', (e, ...args) => main.setAspectRatio(...args))
ipc.on('setBounds', (e, ...args) => main.setBounds(...args)) ipc.on('setBounds', (e, ...args) => main.setBounds(...args))
@@ -121,11 +135,16 @@ function init () {
}) })
}) })
ipc.on('openExternalPlayer', (e, ...args) => externalPlayer.spawn(...args)) ipc.on('openExternalPlayer', (e, ...args) => {
menu.togglePlaybackControls(false)
thumbar.disable()
externalPlayer.spawn(...args)
})
ipc.on('quitExternalPlayer', () => externalPlayer.kill()) ipc.on('quitExternalPlayer', () => externalPlayer.kill())
// Capture all events // Capture all events
var oldEmit = ipc.emit const oldEmit = ipc.emit
ipc.emit = function (name, e, ...args) { ipc.emit = function (name, e, ...args) {
// Relay messages between the main window and the WebTorrent hidden window // Relay messages between the main window and the WebTorrent hidden window
if (name.startsWith('wt-') && !app.isQuitting) { if (name.startsWith('wt-') && !app.isQuitting) {

View File

@@ -8,10 +8,10 @@ module.exports.error = error
* where they can be viewed in Developer Tools. * where they can be viewed in Developer Tools.
*/ */
var electron = require('electron') const electron = require('electron')
var windows = require('./windows') const windows = require('./windows')
var app = electron.app const app = electron.app
function log (...args) { function log (...args) {
if (app.ipcReady) { if (app.ipcReady) {

View File

@@ -1,30 +1,33 @@
module.exports = { module.exports = {
init, init,
setPlayerOpen, togglePlaybackControls,
setWindowFocus, setWindowFocus,
setAllowNav, setAllowNav,
onPlayerUpdate,
onToggleAlwaysOnTop, onToggleAlwaysOnTop,
onToggleFullScreen onToggleFullScreen
} }
var electron = require('electron') const electron = require('electron')
var app = electron.app const app = electron.app
var config = require('../config') const config = require('../config')
var dialog = require('./dialog') const dialog = require('./dialog')
var shell = require('./shell') const shell = require('./shell')
var windows = require('./windows') const windows = require('./windows')
var menu let menu = null
function init () { function init () {
menu = electron.Menu.buildFromTemplate(getMenuTemplate()) menu = electron.Menu.buildFromTemplate(getMenuTemplate())
electron.Menu.setApplicationMenu(menu) electron.Menu.setApplicationMenu(menu)
} }
function setPlayerOpen (flag) { function togglePlaybackControls (flag) {
getMenuItem('Play/Pause').enabled = flag getMenuItem('Play/Pause').enabled = flag
getMenuItem('Skip Next').enabled = flag
getMenuItem('Skip Previous').enabled = flag
getMenuItem('Increase Volume').enabled = flag getMenuItem('Increase Volume').enabled = flag
getMenuItem('Decrease Volume').enabled = flag getMenuItem('Decrease Volume').enabled = flag
getMenuItem('Step Forward').enabled = flag getMenuItem('Step Forward').enabled = flag
@@ -32,6 +35,16 @@ function setPlayerOpen (flag) {
getMenuItem('Increase Speed').enabled = flag getMenuItem('Increase Speed').enabled = flag
getMenuItem('Decrease Speed').enabled = flag getMenuItem('Decrease Speed').enabled = flag
getMenuItem('Add Subtitles File...').enabled = flag getMenuItem('Add Subtitles File...').enabled = flag
if (flag === false) {
getMenuItem('Skip Next').enabled = false
getMenuItem('Skip Previous').enabled = false
}
}
function onPlayerUpdate (hasNext, hasPrevious) {
getMenuItem('Skip Next').enabled = hasNext
getMenuItem('Skip Previous').enabled = hasPrevious
} }
function setWindowFocus (flag) { function setWindowFocus (flag) {
@@ -59,8 +72,8 @@ function onToggleFullScreen (flag) {
} }
function getMenuItem (label) { function getMenuItem (label) {
for (var i = 0; i < menu.items.length; i++) { for (let i = 0; i < menu.items.length; i++) {
var menuItem = menu.items[i].submenu.items.find(function (item) { const menuItem = menu.items[i].submenu.items.find(function (item) {
return item.label === label return item.label === label
}) })
if (menuItem) return menuItem if (menuItem) return menuItem
@@ -68,7 +81,7 @@ function getMenuItem (label) {
} }
function getMenuTemplate () { function getMenuTemplate () {
var template = [ const template = [
{ {
label: 'File', label: 'File',
submenu: [ submenu: [
@@ -187,6 +200,21 @@ function getMenuTemplate () {
{ {
type: 'separator' type: 'separator'
}, },
{
label: 'Skip Next',
accelerator: 'N',
click: () => windows.main.dispatch('nextTrack'),
enabled: false
},
{
label: 'Skip Previous',
accelerator: 'P',
click: () => windows.main.dispatch('previousTrack'),
enabled: false
},
{
type: 'separator'
},
{ {
label: 'Increase Volume', label: 'Increase Volume',
accelerator: 'CmdOrCtrl+Up', accelerator: 'CmdOrCtrl+Up',

View File

@@ -3,10 +3,10 @@ module.exports = {
disable disable
} }
var electron = require('electron') const electron = require('electron')
var log = require('./log') const log = require('./log')
var blockId = 0 let blockId = 0
/** /**
* Block the system from entering low-power (sleep) mode or turning off the * Block the system from entering low-power (sleep) mode or turning off the

View File

@@ -5,8 +5,8 @@ module.exports = {
moveItemToTrash moveItemToTrash
} }
var electron = require('electron') const electron = require('electron')
var log = require('./log') const log = require('./log')
/** /**
* Open the given external protocol URL in the desktops default manner. * Open the given external protocol URL in the desktops default manner.

View File

@@ -3,8 +3,8 @@ module.exports = {
enable enable
} }
var electron = require('electron') const electron = require('electron')
var windows = require('./windows') const windows = require('./windows')
function enable () { function enable () {
// Register play/pause media key, available on some keyboards. // Register play/pause media key, available on some keyboards.
@@ -12,9 +12,19 @@ function enable () {
'MediaPlayPause', 'MediaPlayPause',
() => windows.main.dispatch('playPause') () => windows.main.dispatch('playPause')
) )
electron.globalShortcut.register(
'MediaNextTrack',
() => windows.main.dispatch('nextTrack')
)
electron.globalShortcut.register(
'MediaPreviousTrack',
() => windows.main.dispatch('previousTrack')
)
} }
function disable () { function disable () {
// Return the media key to the OS, so other apps can use it. // Return the media key to the OS, so other apps can use it.
electron.globalShortcut.unregister('MediaPlayPause') electron.globalShortcut.unregister('MediaPlayPause')
electron.globalShortcut.unregister('MediaNextTrack')
electron.globalShortcut.unregister('MediaPreviousTrack')
} }

View File

@@ -2,18 +2,18 @@ module.exports = {
handleEvent handleEvent
} }
var cp = require('child_process') const cp = require('child_process')
var electron = require('electron') const electron = require('electron')
var fs = require('fs') const fs = require('fs')
var os = require('os') const os = require('os')
var path = require('path') const path = require('path')
var app = electron.app const app = electron.app
var handlers = require('./handlers') const handlers = require('./handlers')
var EXE_NAME = path.basename(process.execPath) const EXE_NAME = path.basename(process.execPath)
var UPDATE_EXE = path.join(process.execPath, '..', '..', 'Update.exe') const UPDATE_EXE = path.join(process.execPath, '..', '..', 'Update.exe')
function handleEvent (cmd) { function handleEvent (cmd) {
if (cmd === '--squirrel-install') { if (cmd === '--squirrel-install') {
@@ -73,9 +73,9 @@ function handleEvent (cmd) {
* the output from standard out. * the output from standard out.
*/ */
function spawn (command, args, cb) { function spawn (command, args, cb) {
var stdout = '' let stdout = ''
let error = null
var child let child = null
try { try {
child = cp.spawn(command, args) child = cp.spawn(command, args)
} catch (err) { } catch (err) {
@@ -90,10 +90,10 @@ function spawn (command, args, cb) {
stdout += data stdout += data
}) })
var error = null
child.on('error', function (processError) { child.on('error', function (processError) {
error = processError error = processError
}) })
child.on('close', function (code, signal) { child.on('close', function (code, signal) {
if (code !== 0 && !error) error = new Error('Command failed: #{signal || code}') if (code !== 0 && !error) error = new Error('Command failed: #{signal || code}')
if (error) error.stdout = stdout if (error) error.stdout = stdout
@@ -122,12 +122,12 @@ function createShortcuts (cb) {
* command. * command.
*/ */
function updateShortcuts (cb) { function updateShortcuts (cb) {
var homeDir = os.homedir() const homeDir = os.homedir()
if (homeDir) { if (homeDir) {
var desktopShortcutPath = path.join(homeDir, 'Desktop', 'WebTorrent.lnk') const desktopShortcutPath = path.join(homeDir, 'Desktop', 'WebTorrent.lnk')
// If the desktop shortcut was deleted by the user, then keep it deleted. // If the desktop shortcut was deleted by the user, then keep it deleted.
fs.access(desktopShortcutPath, function (err) { fs.access(desktopShortcutPath, function (err) {
var desktopShortcutExists = !err const desktopShortcutExists = !err
createShortcuts(function () { createShortcuts(function () {
if (desktopShortcutExists) { if (desktopShortcutExists) {
cb() cb()

36
src/main/startup.js Normal file
View File

@@ -0,0 +1,36 @@
module.exports = {
install,
uninstall
}
const config = require('../config')
const AutoLaunch = require('auto-launch')
const { app } = require('electron')
// On Mac, work around a bug in auto-launch where it opens a Terminal window
// See https://github.com/Teamwork/node-auto-launch/issues/28#issuecomment-222194437
const appPath = process.platform === 'darwin'
? app.getPath('exe').replace(/\.app\/Content.*/, '.app')
: undefined // Use the default
const appLauncher = new AutoLaunch({
name: config.APP_NAME,
path: appPath,
isHidden: true
})
function install () {
return appLauncher
.isEnabled()
.then(enabled => {
if (!enabled) return appLauncher.enable()
})
}
function uninstall () {
return appLauncher
.isEnabled()
.then(enabled => {
if (enabled) return appLauncher.disable()
})
}

View File

@@ -2,7 +2,8 @@ module.exports = {
disable, disable,
enable, enable,
onPlayerPause, onPlayerPause,
onPlayerPlay onPlayerPlay,
onPlayerUpdate
} }
/** /**
@@ -11,44 +12,80 @@ module.exports = {
* or activating the window. * or activating the window.
*/ */
var path = require('path') const path = require('path')
var config = require('../config') const config = require('../config')
var windows = require('./windows') const windows = require('./windows')
const PREV_ICON = path.join(config.STATIC_PATH, 'PreviousTrackThumbnailBarButton.png')
const PLAY_ICON = path.join(config.STATIC_PATH, 'PlayThumbnailBarButton.png')
const PAUSE_ICON = path.join(config.STATIC_PATH, 'PauseThumbnailBarButton.png')
const NEXT_ICON = path.join(config.STATIC_PATH, 'NextTrackThumbnailBarButton.png')
// Array indices for each button
const PREV = 0
const PLAY_PAUSE = 1
const NEXT = 2
let buttons = []
/** /**
* Show the Windows thumbnail toolbar buttons. * Show the Windows thumbnail toolbar buttons.
*/ */
function enable () { function enable () {
update(false) buttons = [
{
tooltip: 'Previous Track',
icon: PREV_ICON,
click: () => windows.main.dispatch('previousTrack')
},
{
tooltip: 'Pause',
icon: PAUSE_ICON,
click: () => windows.main.dispatch('playPause')
},
{
tooltip: 'Next Track',
icon: NEXT_ICON,
click: () => windows.main.dispatch('nextTrack')
}
]
update()
} }
/** /**
* Hide the Windows thumbnail toolbar buttons. * Hide the Windows thumbnail toolbar buttons.
*/ */
function disable () { function disable () {
windows.main.win.setThumbarButtons([]) buttons = []
update()
} }
function onPlayerPause () { function onPlayerPause () {
update(true) if (!isEnabled()) return
buttons[PLAY_PAUSE].tooltip = 'Play'
buttons[PLAY_PAUSE].icon = PLAY_ICON
update()
} }
function onPlayerPlay () { function onPlayerPlay () {
update(false) if (!isEnabled()) return
buttons[PLAY_PAUSE].tooltip = 'Pause'
buttons[PLAY_PAUSE].icon = PAUSE_ICON
update()
} }
function update (isPaused) { function onPlayerUpdate (state) {
var icon = isPaused if (!isEnabled()) return
? 'PlayThumbnailBarButton.png' buttons[PREV].flags = [ state.hasPrevious ? 'enabled' : 'disabled' ]
: 'PauseThumbnailBarButton.png' buttons[NEXT].flags = [ state.hasNext ? 'enabled' : 'disabled' ]
update()
}
var buttons = [ function isEnabled () {
{ return buttons.length > 0
tooltip: isPaused ? 'Play' : 'Pause', }
icon: path.join(config.STATIC_PATH, icon),
click: () => windows.main.dispatch('playPause') function update () {
}
]
windows.main.win.setThumbarButtons(buttons) windows.main.win.setThumbarButtons(buttons)
} }

View File

@@ -4,14 +4,14 @@ module.exports = {
setWindowFocus setWindowFocus
} }
var electron = require('electron') const electron = require('electron')
var app = electron.app const app = electron.app
var config = require('../config') const config = require('../config')
var windows = require('./windows') const windows = require('./windows')
var tray let tray
function init () { function init () {
if (process.platform === 'linux') { if (process.platform === 'linux') {
@@ -49,7 +49,7 @@ function initWin32 () {
* Check for libappindicator1 support before creating tray icon * Check for libappindicator1 support before creating tray icon
*/ */
function checkLinuxTraySupport (cb) { function checkLinuxTraySupport (cb) {
var cp = require('child_process') const cp = require('child_process')
// Check that we're on Ubuntu (or another debian system) and that we have // Check that we're on Ubuntu (or another debian system) and that we have
// libappindicator1. If WebTorrent was installed from the deb file, we should // libappindicator1. If WebTorrent was installed from the deb file, we should
@@ -74,7 +74,7 @@ function createTray () {
} }
function updateTrayMenu () { function updateTrayMenu () {
var contextMenu = electron.Menu.buildFromTemplate(getMenuTemplate()) const contextMenu = electron.Menu.buildFromTemplate(getMenuTemplate())
tray.setContextMenu(contextMenu) tray.setContextMenu(contextMenu)
} }

View File

@@ -2,14 +2,14 @@ module.exports = {
init init
} }
var electron = require('electron') const electron = require('electron')
var get = require('simple-get') const get = require('simple-get')
var config = require('../config') const config = require('../config')
var log = require('./log') const log = require('./log')
var windows = require('./windows') const windows = require('./windows')
var AUTO_UPDATE_URL = config.AUTO_UPDATE_URL + const AUTO_UPDATE_URL = config.AUTO_UPDATE_URL +
'?version=' + config.APP_VERSION + '?version=' + config.APP_VERSION +
'&platform=' + process.platform '&platform=' + process.platform

View File

@@ -2,9 +2,9 @@ module.exports = {
init init
} }
var electron = require('electron') const electron = require('electron')
var app = electron.app const app = electron.app
/** /**
* Add a user task menu to the app icon on right-click. (Windows) * Add a user task menu to the app icon on right-click. (Windows)

View File

@@ -1,17 +1,17 @@
var about = module.exports = { const about = module.exports = {
init, init,
win: null win: null
} }
var config = require('../../config') const config = require('../../config')
var electron = require('electron') const electron = require('electron')
function init () { function init () {
if (about.win) { if (about.win) {
return about.win.show() return about.win.show()
} }
var win = about.win = new electron.BrowserWindow({ const win = about.win = new electron.BrowserWindow({
backgroundColor: '#ECECEC', backgroundColor: '#ECECEC',
center: true, center: true,
fullscreen: false, fullscreen: false,

View File

@@ -1,4 +1,4 @@
var main = module.exports = { const main = module.exports = {
dispatch, dispatch,
hide, hide,
init, init,
@@ -14,23 +14,23 @@ var main = module.exports = {
win: null win: null
} }
var electron = require('electron') const electron = require('electron')
var app = electron.app const app = electron.app
var config = require('../../config') const config = require('../../config')
var log = require('../log') const log = require('../log')
var menu = require('../menu') const menu = require('../menu')
var tray = require('../tray') const tray = require('../tray')
var HEADER_HEIGHT = 38 const HEADER_HEIGHT = 38
var TORRENT_HEIGHT = 100 const TORRENT_HEIGHT = 100
function init () { function init (options) {
if (main.win) { if (main.win) {
return main.win.show() return main.win.show()
} }
var win = main.win = new electron.BrowserWindow({ const win = main.win = new electron.BrowserWindow({
backgroundColor: '#282828', backgroundColor: '#282828',
darkTheme: true, // Forces dark theme (GTK+3) darkTheme: true, // Forces dark theme (GTK+3)
icon: getIconPath(), // Window icon (Windows, Linux) icon: getIconPath(), // Window icon (Windows, Linux)
@@ -40,7 +40,8 @@ function init () {
titleBarStyle: 'hidden-inset', // Hide title bar (Mac) titleBarStyle: 'hidden-inset', // Hide title bar (Mac)
useContentSize: true, // Specify web page size without OS chrome useContentSize: true, // Specify web page size without OS chrome
width: 500, width: 500,
height: HEADER_HEIGHT + (TORRENT_HEIGHT * 6) // header height + 5 torrents height: HEADER_HEIGHT + (TORRENT_HEIGHT * 6), // header height + 5 torrents
show: !options.hidden
}) })
win.loadURL(config.WINDOW_MAIN) win.loadURL(config.WINDOW_MAIN)
@@ -114,7 +115,7 @@ function setBounds (bounds, maximize) {
} }
// Maximize or minimize, if the second argument is present // Maximize or minimize, if the second argument is present
var willBeMaximized let willBeMaximized
if (maximize === true) { if (maximize === true) {
if (!main.win.isMaximized()) { if (!main.win.isMaximized()) {
log('setBounds: maximizing') log('setBounds: maximizing')
@@ -136,7 +137,7 @@ function setBounds (bounds, maximize) {
log('setBounds: setting bounds to ' + JSON.stringify(bounds)) log('setBounds: setting bounds to ' + JSON.stringify(bounds))
if (bounds.x === null && bounds.y === null) { if (bounds.x === null && bounds.y === null) {
// X and Y not specified? By default, center on current screen // X and Y not specified? By default, center on current screen
var scr = electron.screen.getDisplayMatching(main.win.getBounds()) const scr = electron.screen.getDisplayMatching(main.win.getBounds())
bounds.x = Math.round(scr.bounds.x + scr.bounds.width / 2 - bounds.width / 2) bounds.x = Math.round(scr.bounds.x + scr.bounds.width / 2 - bounds.width / 2)
bounds.y = Math.round(scr.bounds.y + scr.bounds.height / 2 - bounds.height / 2) bounds.y = Math.round(scr.bounds.y + scr.bounds.height / 2 - bounds.height / 2)
log('setBounds: centered to ' + JSON.stringify(bounds)) log('setBounds: centered to ' + JSON.stringify(bounds))

View File

@@ -1,4 +1,4 @@
var webtorrent = module.exports = { const webtorrent = module.exports = {
init, init,
send, send,
show, show,
@@ -6,13 +6,13 @@ var webtorrent = module.exports = {
win: null win: null
} }
var electron = require('electron') const electron = require('electron')
var config = require('../../config') const config = require('../../config')
var log = require('../log') const log = require('../log')
function init () { function init () {
var win = webtorrent.win = new electron.BrowserWindow({ const win = webtorrent.win = new electron.BrowserWindow({
backgroundColor: '#1E1E1E', backgroundColor: '#1E1E1E',
center: true, center: true,
fullscreen: false, fullscreen: false,

View File

@@ -4,9 +4,9 @@ const {dispatcher} = require('../lib/dispatcher')
class Header extends React.Component { class Header extends React.Component {
render () { render () {
var loc = this.props.state.location const loc = this.props.state.location
return ( return (
<div className='header'> <div className='header' onMouseMove={dispatcher('mediaMouseMoved')}>
{this.getTitle()} {this.getTitle()}
<div className='nav left float-left'> <div className='nav left float-left'>
<i <i
@@ -31,12 +31,12 @@ class Header extends React.Component {
getTitle () { getTitle () {
if (process.platform !== 'darwin') return null if (process.platform !== 'darwin') return null
var state = this.props.state const state = this.props.state
return (<div className='title ellipsis'>{state.window.title}</div>) return (<div className='title ellipsis'>{state.window.title}</div>)
} }
getAddButton () { getAddButton () {
var state = this.props.state const state = this.props.state
if (state.location.url() !== 'home') return null if (state.location.url() !== 'home') return null
return ( return (
<i <i

View File

@@ -17,15 +17,14 @@ class Heading extends React.Component {
render () { render () {
const HeadingTag = 'h' + this.props.level const HeadingTag = 'h' + this.props.level
const style = {
color: colors.grey100,
fontSize: 20,
marginBottom: 15,
marginTop: 30
}
return ( return (
<HeadingTag <HeadingTag style={style}>
style={{
color: colors.grey100,
fontSize: 20,
marginBottom: 15,
marginTop: 30
}}
>
{this.props.children} {this.props.children}
</HeadingTag> </HeadingTag>
) )

View File

@@ -0,0 +1,24 @@
const React = require('react')
const FlatButton = require('material-ui/FlatButton').default
const RaisedButton = require('material-ui/RaisedButton').default
module.exports = class ModalOKCancel extends React.Component {
render () {
const cancelStyle = { marginRight: 10, color: 'black' }
const {cancelText, onCancel, okText, onOK} = this.props
return (
<div className='float-right'>
<FlatButton
className='control cancel'
style={cancelStyle}
label={cancelText}
onClick={onCancel} />
<RaisedButton
className='control ok'
primary
label={okText}
onClick={onOK} />
</div>
)
}
}

View File

@@ -1,32 +1,41 @@
const React = require('react') const React = require('react')
const TextField = require('material-ui/TextField').default
const ModalOKCancel = require('./modal-ok-cancel')
const {dispatch, dispatcher} = require('../lib/dispatcher') const {dispatch, dispatcher} = require('../lib/dispatcher')
module.exports = class OpenTorrentAddressModal extends React.Component { module.exports = class OpenTorrentAddressModal extends React.Component {
render () { render () {
// TODO: dcposch remove janky inline <script>
return ( return (
<div className='open-torrent-address-modal'> <div className='open-torrent-address-modal'>
<p><label>Enter torrent address or magnet link</label></p> <p><label>Enter torrent address or magnet link</label></p>
<p> <div>
<input id='add-torrent-url' type='text' onKeyPress={handleKeyPress} /> <TextField
</p> id='torrent-address-field'
<p className='float-right'> className='control'
<button className='button button-flat' onClick={dispatcher('exitModal')}>Cancel</button> ref={(c) => { this.torrentURL = c }}
<button className='button button-raised' onClick={handleOK}>OK</button> fullWidth
</p> onKeyDown={handleKeyDown.bind(this)} />
<script>document.querySelector('#add-torrent-url').focus()</script> </div>
<ModalOKCancel
cancelText='CANCEL'
onCancel={dispatcher('exitModal')}
okText='OK'
onOK={handleOK.bind(this)} />
</div> </div>
) )
} }
componentDidMount () {
this.torrentURL.input.focus()
}
} }
function handleKeyPress (e) { function handleKeyDown (e) {
if (e.which === 13) handleOK() /* hit Enter to submit */ if (e.which === 13) handleOK.call(this) /* hit Enter to submit */
} }
function handleOK () { function handleOK () {
dispatch('exitModal') dispatch('exitModal')
// TODO: dcposch use React refs instead dispatch('addTorrent', this.torrentURL.input.value)
dispatch('addTorrent', document.querySelector('#add-torrent-url').value)
} }

View File

@@ -7,6 +7,9 @@ const remote = electron.remote
const RaisedButton = require('material-ui/RaisedButton').default const RaisedButton = require('material-ui/RaisedButton').default
const TextField = require('material-ui/TextField').default const TextField = require('material-ui/TextField').default
// Lets you pick a file or directory.
// Uses the system Open File dialog.
// You can't edit the text field directly.
class PathSelector extends React.Component { class PathSelector extends React.Component {
static get propTypes () { static get propTypes () {
return { return {
@@ -26,7 +29,7 @@ class PathSelector extends React.Component {
} }
handleClick () { handleClick () {
var opts = Object.assign({ const opts = Object.assign({
defaultPath: this.props.value, defaultPath: this.props.value,
properties: [ 'openFile', 'openDirectory' ] properties: [ 'openFile', 'openDirectory' ]
}, this.props.dialog) }, this.props.dialog)
@@ -43,48 +46,38 @@ class PathSelector extends React.Component {
render () { render () {
const id = this.props.title.replace(' ', '-').toLowerCase() const id = this.props.title.replace(' ', '-').toLowerCase()
const wrapperStyle = {
alignItems: 'center',
display: 'flex',
width: '100%'
}
const labelStyle = {
flex: '0 auto',
marginRight: 10,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap'
}
const textareaStyle = {
color: colors.grey50
}
const textFieldStyle = {
flex: '1'
}
const text = this.props.displayValue || this.props.value
const buttonStyle = {
marginLeft: 10
}
return ( return (
<div <div className={this.props.className} style={wrapperStyle}>
className={this.props.className} <div className='label' style={labelStyle}>
style={{
alignItems: 'center',
display: 'flex',
width: '100%'
}}
>
<div
className='label'
style={{
flex: '0 auto',
marginRight: 10,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap'
}}
>
{this.props.title}: {this.props.title}:
</div> </div>
<TextField <TextField className='control' disabled id={id} value={text}
className='control' inputStyle={textareaStyle} style={textFieldStyle} />
disabled <RaisedButton className='control' label='Change' onClick={this.handleClick}
id={id} style={buttonStyle} />
inputStyle={{
color: colors.grey50
}}
style={{
flex: '1',
fontSize: 14
}}
value={this.props.displayValue || this.props.value}
/>
<RaisedButton
className='control'
label='Change'
onClick={this.handleClick}
style={{
marginLeft: 10
}}
/>
</div> </div>
) )
} }

View File

@@ -1,22 +1,24 @@
const React = require('react') const React = require('react')
const ModalOKCancel = require('./modal-ok-cancel')
const {dispatch, dispatcher} = require('../lib/dispatcher') const {dispatch, dispatcher} = require('../lib/dispatcher')
module.exports = class RemoveTorrentModal extends React.Component { module.exports = class RemoveTorrentModal extends React.Component {
render () { render () {
var state = this.props.state const state = this.props.state
var message = state.modal.deleteData const message = state.modal.deleteData
? 'Are you sure you want to remove this torrent from the list and delete the data file?' ? 'Are you sure you want to remove this torrent from the list and delete the data file?'
: 'Are you sure you want to remove this torrent from the list?' : 'Are you sure you want to remove this torrent from the list?'
var buttonText = state.modal.deleteData ? 'Remove Data' : 'Remove' const buttonText = state.modal.deleteData ? 'REMOVE DATA' : 'REMOVE'
return ( return (
<div> <div>
<p><strong>{message}</strong></p> <p><strong>{message}</strong></p>
<p className='float-right'> <ModalOKCancel
<button className='button button-flat' onClick={dispatcher('exitModal')}>Cancel</button> cancelText='CANCEL'
<button className='button button-raised' onClick={handleRemove}>{buttonText}</button> onCancel={dispatcher('exitModal')}
</p> okText={buttonText}
onOK={handleRemove} />
</div> </div>
) )

View File

@@ -1,35 +1,35 @@
const React = require('react') const React = require('react')
const electron = require('electron') const electron = require('electron')
const path = require('path')
const ModalOKCancel = require('./modal-ok-cancel')
const {dispatcher} = require('../lib/dispatcher') const {dispatcher} = require('../lib/dispatcher')
module.exports = class UnsupportedMediaModal extends React.Component { module.exports = class UnsupportedMediaModal extends React.Component {
render () { render () {
var state = this.props.state const state = this.props.state
var err = state.modal.error const err = state.modal.error
var message = (err && err.getMessage) const message = (err && err.getMessage)
? err.getMessage() ? err.getMessage()
: err : err
var playerPath = state.saved.prefs.externalPlayerPath const onAction = state.modal.externalPlayerInstalled
var playerName = playerPath ? dispatcher('openExternalPlayer')
? path.basename(playerPath).split('.')[0] : () => this.onInstall()
: 'VLC' const actionText = state.modal.externalPlayerInstalled
var actionButton = state.modal.externalPlayerInstalled ? 'PLAY IN ' + state.getExternalPlayerName().toUpperCase()
? (<button className='button-raised' onClick={dispatcher('openExternalPlayer')}>Play in {playerName}</button>) : 'INSTALL VLC'
: (<button className='button-raised' onClick={() => this.onInstall}>Install VLC</button>) const errorMessage = state.modal.externalPlayerNotFound
var playerMessage = state.modal.externalPlayerNotFound
? 'Couldn\'t run external player. Please make sure it\'s installed.' ? 'Couldn\'t run external player. Please make sure it\'s installed.'
: '' : ''
return ( return (
<div> <div>
<p><strong>Sorry, we can't play that file.</strong></p> <p><strong>Sorry, we can't play that file.</strong></p>
<p>{message}</p> <p>{message}</p>
<p className='float-right'> <ModalOKCancel
<button className='button-flat' onClick={dispatcher('backToList')}>Cancel</button> cancelText='CANCEL'
{actionButton} onCancel={dispatcher('backToList')}
</p> okText={actionText}
<p className='error-text'>{playerMessage}</p> onOK={onAction} />
<p className='error-text'>{errorMessage}</p>
</div> </div>
) )
} }
@@ -38,7 +38,7 @@ module.exports = class UnsupportedMediaModal extends React.Component {
electron.shell.openExternal('http://www.videolan.org/vlc/') electron.shell.openExternal('http://www.videolan.org/vlc/')
// TODO: dcposch send a dispatch rather than modifying state directly // TODO: dcposch send a dispatch rather than modifying state directly
var state = this.props.state const state = this.props.state
state.modal.externalPlayerInstalled = true // Assume they'll install it successfully state.modal.externalPlayerInstalled = true // Assume they'll install it successfully
} }
} }

View File

@@ -1,19 +1,24 @@
const React = require('react') const React = require('react')
const electron = require('electron') const electron = require('electron')
const ModalOKCancel = require('./modal-ok-cancel')
const {dispatch} = require('../lib/dispatcher') const {dispatch} = require('../lib/dispatcher')
module.exports = class UpdateAvailableModal extends React.Component { module.exports = class UpdateAvailableModal extends React.Component {
render () { render () {
var state = this.props.state const state = this.props.state
return ( return (
<div className='update-available-modal'> <div className='update-available-modal'>
<p><strong>A new version of WebTorrent is available: v{state.modal.version}</strong></p> <p><strong>A new version of WebTorrent is available: v{state.modal.version}</strong></p>
<p>We have an auto-updater for Windows and Mac. We don't have one for Linux yet, so you'll have to download the new version manually.</p> <p>
<p className='float-right'> We have an auto-updater for Windows and Mac.
<button className='button button-flat' onClick={handleSkip}>Skip This Release</button> We don't have one for Linux yet, so you'll have to download the new version manually.
<button className='button button-raised' onClick={handleShow}>Show Download Page</button>
</p> </p>
<ModalOKCancel
cancelText='SKIP THIS RELEASE'
onCancel={handleSkip}
okText='SHOW DOWNLOAD PAGE'
onOK={handleShow} />
</div> </div>
) )

View File

@@ -2,6 +2,8 @@ const electron = require('electron')
const ipcRenderer = electron.ipcRenderer const ipcRenderer = electron.ipcRenderer
const Playlist = require('../lib/playlist')
// Controls local play back: the <video>/<audio> tag and VLC // Controls local play back: the <video>/<audio> tag and VLC
// Does not control remote casting (Chromecast etc) // Does not control remote casting (Chromecast etc)
module.exports = class MediaController { module.exports = class MediaController {
@@ -18,7 +20,7 @@ module.exports = class MediaController {
} }
mediaError (error) { mediaError (error) {
var state = this.state const state = this.state
if (state.location.url() === 'player') { if (state.location.url() === 'player') {
state.playing.result = 'error' state.playing.result = 'error'
state.playing.location = 'error' state.playing.location = 'error'
@@ -43,13 +45,17 @@ module.exports = class MediaController {
} }
openExternalPlayer () { openExternalPlayer () {
var state = this.state const state = this.state
ipcRenderer.send('openExternalPlayer', state.saved.prefs.externalPlayerPath, state.server.localURL, state.window.title) const mediaURL = Playlist.getCurrentLocalURL(this.state)
ipcRenderer.send('openExternalPlayer',
state.saved.prefs.externalPlayerPath,
mediaURL,
state.window.title)
state.playing.location = 'external' state.playing.location = 'external'
} }
externalPlayerNotFound () { externalPlayerNotFound () {
var modal = this.state.modal const modal = this.state.modal
if (modal && modal.id === 'unsupported-media-modal') { if (modal && modal.id === 'unsupported-media-modal') {
modal.externalPlayerNotFound = true modal.externalPlayerNotFound = true
} }

View File

@@ -4,10 +4,12 @@ const path = require('path')
const Cast = require('../lib/cast') const Cast = require('../lib/cast')
const {dispatch} = require('../lib/dispatcher') const {dispatch} = require('../lib/dispatcher')
const telemetry = require('../lib/telemetry') const telemetry = require('../lib/telemetry')
const errors = require('../lib/errors') const {UnplayableFileError, UnplayableTorrentError,
PlaybackTimedOutError} = require('../lib/errors')
const sound = require('../lib/sound') const sound = require('../lib/sound')
const TorrentPlayer = require('../lib/torrent-player') const TorrentPlayer = require('../lib/torrent-player')
const TorrentSummary = require('../lib/torrent-summary') const TorrentSummary = require('../lib/torrent-summary')
const Playlist = require('../lib/playlist')
const State = require('../lib/state') const State = require('../lib/state')
const ipcRenderer = electron.ipcRenderer const ipcRenderer = electron.ipcRenderer
@@ -24,24 +26,43 @@ module.exports = class PlaybackController {
// Play a file in a torrent. // Play a file in a torrent.
// * Start torrenting, if necessary // * Start torrenting, if necessary
// * Stream, if not already fully downloaded // * Stream, if not already fully downloaded
// * If no file index is provided, pick the default file to play // * If no file index is provided, restore the most recently viewed file or autoplay the first
playFile (infoHash, index /* optional */) { playFile (infoHash, index /* optional */) {
this.state.location.go({ const state = this.state
url: 'player', if (state.location.url() === 'player') {
setup: (cb) => { this.updatePlayer(infoHash, index, false, (err) => {
this.play() if (err) dispatch('error', err)
this.openPlayer(infoHash, index, cb) else this.play()
}, })
destroy: () => this.closePlayer() } else {
}, (err) => { let initialized = false
if (err) dispatch('error', err) state.location.go({
}) url: 'player',
setup: (cb) => {
const torrentSummary = TorrentSummary.getByKey(state, infoHash)
if (index === undefined || initialized) index = torrentSummary.mostRecentFileIndex
if (index === undefined) index = torrentSummary.files.findIndex(TorrentPlayer.isPlayable)
if (index === undefined) return cb(new UnplayableTorrentError())
initialized = true
this.openPlayer(infoHash, index, (err) => {
if (!err) this.play()
cb(err)
})
},
destroy: () => this.closePlayer()
}, (err) => {
if (err) dispatch('error', err)
})
}
} }
// Open a file in OS default app. // Open a file in OS default app.
openItem (infoHash, index) { openItem (infoHash, index) {
var torrentSummary = TorrentSummary.getByKey(this.state, infoHash) const torrentSummary = TorrentSummary.getByKey(this.state, infoHash)
var filePath = path.join( const filePath = path.join(
torrentSummary.path, torrentSummary.path,
torrentSummary.files[index].path) torrentSummary.files[index].path)
ipcRenderer.send('openItem', filePath) ipcRenderer.send('openItem', filePath)
@@ -49,12 +70,12 @@ module.exports = class PlaybackController {
// Toggle (play or pause) the currently playing media // Toggle (play or pause) the currently playing media
playPause () { playPause () {
var state = this.state const state = this.state
if (state.location.url() !== 'player') return if (state.location.url() !== 'player') return
// force rerendering if window is hidden, // force rerendering if window is hidden,
// in order to bypass `raf` and play/pause media immediately // in order to bypass `raf` and play/pause media immediately
var mediaTag = document.querySelector('video,audio') const mediaTag = document.querySelector('video,audio')
if (!state.window.isVisible && mediaTag) { if (!state.window.isVisible && mediaTag) {
if (state.playing.isPaused) mediaTag.play() if (state.playing.isPaused) mediaTag.play()
else mediaTag.pause() else mediaTag.pause()
@@ -64,9 +85,33 @@ module.exports = class PlaybackController {
else this.pause() else this.pause()
} }
// Play next file in list (if any)
nextTrack () {
const state = this.state
if (Playlist.hasNext(state) && state.playing.location !== 'external') {
this.updatePlayer(
state.playing.infoHash, Playlist.getNextIndex(state), false, (err) => {
if (err) dispatch('error', err)
else this.play()
})
}
}
// Play previous track in list (if any)
previousTrack () {
const state = this.state
if (Playlist.hasPrevious(state) && state.playing.location !== 'external') {
this.updatePlayer(
state.playing.infoHash, Playlist.getPreviousIndex(state), false, (err) => {
if (err) dispatch('error', err)
else this.play()
})
}
}
// Play (unpause) the current media // Play (unpause) the current media
play () { play () {
var state = this.state const state = this.state
if (!state.playing.isPaused) return if (!state.playing.isPaused) return
state.playing.isPaused = false state.playing.isPaused = false
if (isCasting(state)) { if (isCasting(state)) {
@@ -77,7 +122,7 @@ module.exports = class PlaybackController {
// Pause the currently playing media // Pause the currently playing media
pause () { pause () {
var state = this.state const state = this.state
if (state.playing.isPaused) return if (state.playing.isPaused) return
state.playing.isPaused = true state.playing.isPaused = true
if (isCasting(state)) { if (isCasting(state)) {
@@ -106,19 +151,22 @@ module.exports = class PlaybackController {
// to 0.25 (quarter-speed playback), then goes to -0.25, -0.5, -1, -2, etc // to 0.25 (quarter-speed playback), then goes to -0.25, -0.5, -1, -2, etc
// until -16 (fast rewind) // until -16 (fast rewind)
changePlaybackRate (direction) { changePlaybackRate (direction) {
var state = this.state const state = this.state
var rate = state.playing.playbackRate let rate = state.playing.playbackRate
if (direction > 0 && rate >= 0.25 && rate < 2) { if (direction > 0 && rate >= 0.25 && rate < 2) {
rate += 0.25 rate += 0.25
} else if (direction < 0 && rate > 0.25 && rate <= 2) { } else if (direction < 0 && rate > 0.25 && rate <= 2) {
rate -= 0.25 rate -= 0.25
} else if (direction < 0 && rate === 0.25) { /* when we set playback rate at 0 in html 5, playback hangs ;( */ } else if (direction < 0 && rate === 0.25) {
// When we set playback rate at 0 in html 5, playback hangs ;(
rate = -1 rate = -1
} else if (direction > 0 && rate === -1) { } else if (direction > 0 && rate === -1) {
rate = 0.25 rate = 0.25
} else if ((direction > 0 && rate >= 1 && rate < 16) || (direction < 0 && rate > -16 && rate <= -1)) { } else if ((direction > 0 && rate >= 1 && rate < 16) ||
(direction < 0 && rate > -16 && rate <= -1)) {
rate *= 2 rate *= 2
} else if ((direction < 0 && rate > 1 && rate <= 16) || (direction > 0 && rate >= -16 && rate < -1)) { } else if ((direction < 0 && rate > 1 && rate <= 16) ||
(direction > 0 && rate >= -16 && rate < -1)) {
rate /= 2 rate /= 2
} }
state.playing.playbackRate = rate state.playing.playbackRate = rate
@@ -139,7 +187,7 @@ module.exports = class PlaybackController {
// check if its in [0.0 - 1.0] range // check if its in [0.0 - 1.0] range
volume = Math.max(0, Math.min(1, volume)) volume = Math.max(0, Math.min(1, volume))
var state = this.state const state = this.state
if (isCasting(state)) { if (isCasting(state)) {
Cast.setVolume(volume) Cast.setVolume(volume)
} else { } else {
@@ -153,8 +201,8 @@ module.exports = class PlaybackController {
// * The video is paused // * The video is paused
// * The video is playing remotely on Chromecast or Airplay // * The video is playing remotely on Chromecast or Airplay
showOrHidePlayerControls () { showOrHidePlayerControls () {
var state = this.state const state = this.state
var hideControls = state.location.url() === 'player' && const hideControls = state.location.url() === 'player' &&
state.playing.mouseStationarySince !== 0 && state.playing.mouseStationarySince !== 0 &&
new Date().getTime() - state.playing.mouseStationarySince > 2000 && new Date().getTime() - state.playing.mouseStationarySince > 2000 &&
!state.playing.isPaused && !state.playing.isPaused &&
@@ -169,12 +217,10 @@ module.exports = class PlaybackController {
// Opens the video player to a specific torrent // Opens the video player to a specific torrent
openPlayer (infoHash, index, cb) { openPlayer (infoHash, index, cb) {
var torrentSummary = TorrentSummary.getByKey(this.state, infoHash) const state = this.state
const torrentSummary = TorrentSummary.getByKey(state, infoHash)
// automatically choose which file in the torrent to play, if necessary state.playing.infoHash = torrentSummary.infoHash
if (index === undefined) index = torrentSummary.defaultPlayFileIndex
if (index === undefined) index = TorrentPlayer.pickFileToPlay(torrentSummary.files)
if (index === undefined) return cb(new errors.UnplayableError())
// update UI to show pending playback // update UI to show pending playback
if (torrentSummary.progress !== 1) sound.play('PLAY') if (torrentSummary.progress !== 1) sound.play('PLAY')
@@ -182,43 +228,78 @@ module.exports = class PlaybackController {
torrentSummary.playStatus = 'requested' torrentSummary.playStatus = 'requested'
this.update() this.update()
var timeout = setTimeout(() => { const timeout = setTimeout(() => {
telemetry.logPlayAttempt('timeout') telemetry.logPlayAttempt('timeout')
// TODO: remove torrentSummary.playStatus // TODO: remove torrentSummary.playStatus
torrentSummary.playStatus = 'timeout' /* no seeders available? */ torrentSummary.playStatus = 'timeout' /* no seeders available? */
sound.play('ERROR') sound.play('ERROR')
cb(new Error('Playback timed out. Try again.')) cb(new PlaybackTimedOutError())
this.update() this.update()
}, 10000) /* give it a few seconds */ }, 10000) /* give it a few seconds */
this.startServer(torrentSummary, () => {
clearTimeout(timeout)
// if we timed out (user clicked play a long time ago), don't autoplay
const timedOut = torrentSummary.playStatus === 'timeout'
delete torrentSummary.playStatus
if (timedOut) {
ipcRenderer.send('wt-stop-server')
return this.update()
}
ipcRenderer.send('onPlayerOpen')
this.updatePlayer(infoHash, index, true, cb)
})
}
// Starts WebTorrent server for media streaming
startServer (torrentSummary, cb) {
if (torrentSummary.status === 'paused') { if (torrentSummary.status === 'paused') {
dispatch('startTorrentingSummary', torrentSummary.torrentKey) dispatch('startTorrentingSummary', torrentSummary.torrentKey)
ipcRenderer.once('wt-ready-' + torrentSummary.infoHash, ipcRenderer.once('wt-ready-' + torrentSummary.infoHash,
() => this.openPlayerFromActiveTorrent(torrentSummary, index, timeout, cb)) () => onTorrentReady())
} else { } else {
this.openPlayerFromActiveTorrent(torrentSummary, index, timeout, cb) onTorrentReady()
}
function onTorrentReady () {
ipcRenderer.send('wt-start-server', torrentSummary.infoHash)
ipcRenderer.once('wt-server-' + torrentSummary.infoHash, () => cb())
} }
} }
openPlayerFromActiveTorrent (torrentSummary, index, timeout, cb) { // Called each time the current file changes
var fileSummary = torrentSummary.files[index] updatePlayer (infoHash, index, resume, cb) {
const state = this.state
const torrentSummary = TorrentSummary.getByKey(state, infoHash)
const fileSummary = torrentSummary.files[index]
if (!TorrentPlayer.isPlayable(fileSummary)) {
torrentSummary.mostRecentFileIndex = undefined
return cb(new UnplayableFileError())
}
torrentSummary.mostRecentFileIndex = index
// update state // update state
var state = this.state state.playing.infoHash = infoHash
state.playing.infoHash = torrentSummary.infoHash
state.playing.fileIndex = index state.playing.fileIndex = index
state.playing.type = TorrentPlayer.isVideo(fileSummary) ? 'video' state.playing.type = TorrentPlayer.isVideo(fileSummary) ? 'video'
: TorrentPlayer.isAudio(fileSummary) ? 'audio' : TorrentPlayer.isAudio(fileSummary) ? 'audio'
: 'other' : 'other'
// pick up where we left off // pick up where we left off
if (fileSummary.currentTime) { let jumpToTime = 0
var fraction = fileSummary.currentTime / fileSummary.duration if (resume && fileSummary.currentTime) {
var secondsLeft = fileSummary.duration - fileSummary.currentTime const fraction = fileSummary.currentTime / fileSummary.duration
const secondsLeft = fileSummary.duration - fileSummary.currentTime
if (fraction < 0.9 && secondsLeft > 10) { if (fraction < 0.9 && secondsLeft > 10) {
state.playing.jumpToTime = fileSummary.currentTime jumpToTime = fileSummary.currentTime
} }
} }
state.playing.jumpToTime = jumpToTime
// if it's audio, parse out the metadata (artist, title, etc) // if it's audio, parse out the metadata (artist, title, etc)
if (state.playing.type === 'audio' && !fileSummary.audioInfo) { if (state.playing.type === 'audio' && !fileSummary.audioInfo) {
@@ -233,41 +314,28 @@ module.exports = class PlaybackController {
dispatch('addSubtitles', [fileSummary.selectedSubtitle], true) dispatch('addSubtitles', [fileSummary.selectedSubtitle], true)
} }
ipcRenderer.send('wt-start-server', torrentSummary.infoHash, index) state.window.title = fileSummary.name
ipcRenderer.once('wt-server-' + torrentSummary.infoHash, (e, info) => {
clearTimeout(timeout)
// if we timed out (user clicked play a long time ago), don't autoplay // play in VLC if set as default player (Preferences / Playback / Play in VLC)
var timedOut = torrentSummary.playStatus === 'timeout' if (this.state.saved.prefs.openExternalPlayer) {
delete torrentSummary.playStatus dispatch('openExternalPlayer')
if (timedOut) {
ipcRenderer.send('wt-stop-server')
return this.update()
}
state.window.title = torrentSummary.files[state.playing.fileIndex].name
// play in VLC if set as default player (Preferences / Playback / Play in VLC)
if (this.state.saved.prefs.openExternalPlayer) {
dispatch('openExternalPlayer')
this.update()
cb()
return
}
// otherwise, play the video
this.update() this.update()
ipcRenderer.send('onPlayerOpen')
cb() cb()
}) return
}
// otherwise, play the video
this.update()
ipcRenderer.send('onPlayerUpdate', Playlist.hasNext(state), Playlist.hasPrevious(state))
cb()
} }
closePlayer () { closePlayer () {
console.log('closePlayer') console.log('closePlayer')
// Quit any external players, like Chromecast/Airplay/etc or VLC // Quit any external players, like Chromecast/Airplay/etc or VLC
var state = this.state const state = this.state
if (isCasting(state)) { if (isCasting(state)) {
Cast.stop() Cast.stop()
} }
@@ -279,10 +347,10 @@ module.exports = class PlaybackController {
state.previousVolume = state.playing.volume state.previousVolume = state.playing.volume
// Telemetry: track what happens after the user clicks play // Telemetry: track what happens after the user clicks play
var result = state.playing.result // 'success' or 'error' const result = state.playing.result // 'success' or 'error'
if (result === 'success') telemetry.logPlayAttempt('success') // first frame displayed if (result === 'success') telemetry.logPlayAttempt('success') // first frame displayed
else if (result === 'error') telemetry.logPlayAttempt('error') // codec missing, etc else if (result === 'error') telemetry.logPlayAttempt('error') // codec missing, etc
else if (result === undefined) telemetry.logPlayAttempt('abandoned') // user exited before first frame else if (result === undefined) telemetry.logPlayAttempt('abandoned') // user gave up waiting
else console.error('Unknown state.playing.result', state.playing.result) else console.error('Unknown state.playing.result', state.playing.result)
// Reset the window contents back to the home screen // Reset the window contents back to the home screen

View File

@@ -11,13 +11,15 @@ module.exports = class PrefsController {
// Goes to the Preferences screen // Goes to the Preferences screen
show () { show () {
var state = this.state const state = this.state
state.location.go({ state.location.go({
url: 'preferences', url: 'preferences',
setup: function (cb) { setup: function (cb) {
// initialize preferences // initialize preferences
state.window.title = 'Preferences' state.window.title = 'Preferences'
state.unsaved = Object.assign(state.unsaved || {}, {prefs: state.saved.prefs || {}}) state.unsaved = Object.assign(state.unsaved || {}, {
prefs: Object.assign({}, state.saved.prefs)
})
ipcRenderer.send('setAllowNav', false) ipcRenderer.send('setAllowNav', false)
cb() cb()
}, },
@@ -32,23 +34,27 @@ module.exports = class PrefsController {
// For example: updatePreferences('foo.bar', 'baz') // For example: updatePreferences('foo.bar', 'baz')
// Call save() to save to config.json // Call save() to save to config.json
update (property, value) { update (property, value) {
var path = property.split('.') const path = property.split('.')
var key = this.state.unsaved.prefs let obj = this.state.unsaved.prefs
for (var i = 0; i < path.length - 1; i++) { let i
if (typeof key[path[i]] === 'undefined') { for (i = 0; i < path.length - 1; i++) {
key[path[i]] = {} if (typeof obj[path[i]] === 'undefined') {
obj[path[i]] = {}
} }
key = key[path[i]] obj = obj[path[i]]
} }
key[path[i]] = value obj[path[i]] = value
} }
// All unsaved prefs take effect atomically, and are saved to config.json // All unsaved prefs take effect atomically, and are saved to config.json
save () { save () {
var state = this.state const state = this.state
if (state.unsaved.prefs.isFileHandler !== state.saved.prefs.isFileHandler) { if (state.unsaved.prefs.isFileHandler !== state.saved.prefs.isFileHandler) {
ipcRenderer.send('setDefaultFileHandler', state.unsaved.prefs.isFileHandler) ipcRenderer.send('setDefaultFileHandler', state.unsaved.prefs.isFileHandler)
} }
if (state.unsaved.prefs.startup !== state.saved.prefs.startup) {
ipcRenderer.send('setStartup', state.unsaved.prefs.startup)
}
state.saved.prefs = Object.assign(state.saved.prefs || {}, state.unsaved.prefs) state.saved.prefs = Object.assign(state.saved.prefs || {}, state.unsaved.prefs)
State.save(state) State.save(state)
dispatch('checkDownloadPath') dispatch('checkDownloadPath')

View File

@@ -28,36 +28,35 @@ module.exports = class SubtitlesController {
} }
toggleSubtitlesMenu () { toggleSubtitlesMenu () {
var subtitles = this.state.playing.subtitles const subtitles = this.state.playing.subtitles
subtitles.showMenu = !subtitles.showMenu subtitles.showMenu = !subtitles.showMenu
} }
addSubtitles (files, autoSelect) { addSubtitles (files, autoSelect) {
var state = this.state
// Subtitles are only supported when playing video files // Subtitles are only supported when playing video files
if (state.playing.type !== 'video') return if (this.state.playing.type !== 'video') return
if (files.length === 0) return if (files.length === 0) return
var subtitles = state.playing.subtitles const subtitles = this.state.playing.subtitles
// Read the files concurrently, then add all resulting subtitle tracks // Read the files concurrently, then add all resulting subtitle tracks
var tasks = files.map((file) => (cb) => loadSubtitle(file, cb)) const tasks = files.map((file) => (cb) => loadSubtitle(file, cb))
parallel(tasks, function (err, tracks) { parallel(tasks, function (err, tracks) {
if (err) return dispatch('error', err) if (err) return dispatch('error', err)
for (var i = 0; i < tracks.length; i++) { for (let i = 0; i < tracks.length; i++) {
// No dupes allowed // No dupes allowed
var track = tracks[i] const track = tracks[i]
var trackIndex = state.playing.subtitles.tracks let trackIndex = subtitles.tracks.findIndex((t) =>
.findIndex((t) => track.filePath === t.filePath) track.filePath === t.filePath)
// Add the track // Add the track
if (trackIndex === -1) { if (trackIndex === -1) {
trackIndex = state.playing.subtitles.tracks.push(track) - 1 trackIndex = subtitles.tracks.push(track) - 1
} }
// If we're auto-selecting a track, try to find one in the user's language // If we're auto-selecting a track, try to find one in the user's language
if (autoSelect && (i === 0 || isSystemLanguage(track.language))) { if (autoSelect && (i === 0 || isSystemLanguage(track.language))) {
state.playing.subtitles.selectedIndex = trackIndex subtitles.selectedIndex = trackIndex
} }
} }
@@ -68,46 +67,46 @@ module.exports = class SubtitlesController {
checkForSubtitles () { checkForSubtitles () {
if (this.state.playing.type !== 'video') return if (this.state.playing.type !== 'video') return
var torrentSummary = this.state.getPlayingTorrentSummary() const torrentSummary = this.state.getPlayingTorrentSummary()
if (!torrentSummary || !torrentSummary.progress) return if (!torrentSummary || !torrentSummary.progress) return
torrentSummary.progress.files.forEach((fp, ix) => { torrentSummary.progress.files.forEach((fp, ix) => {
if (fp.numPieces !== fp.numPiecesPresent) return // ignore incomplete files if (fp.numPieces !== fp.numPiecesPresent) return // ignore incomplete files
var file = torrentSummary.files[ix] const file = torrentSummary.files[ix]
if (!this.isSubtitle(file.name)) return if (!this.isSubtitle(file.name)) return
var filePath = path.join(torrentSummary.path, file.path) const filePath = path.join(torrentSummary.path, file.path)
this.addSubtitles([filePath], false) this.addSubtitles([filePath], false)
}) })
} }
isSubtitle (file) { isSubtitle (file) {
var name = typeof file === 'string' ? file : file.name const name = typeof file === 'string' ? file : file.name
var ext = path.extname(name).toLowerCase() const ext = path.extname(name).toLowerCase()
return ext === '.srt' || ext === '.vtt' return ext === '.srt' || ext === '.vtt'
} }
} }
function loadSubtitle (file, cb) { function loadSubtitle (file, cb) {
// Lazy load to keep startup fast // Lazy load to keep startup fast
var concat = require('simple-concat') const concat = require('simple-concat')
var LanguageDetect = require('languagedetect') const LanguageDetect = require('languagedetect')
var srtToVtt = require('srt-to-vtt') const srtToVtt = require('srt-to-vtt')
// Read the .SRT or .VTT file, parse it, add subtitle track // Read the .SRT or .VTT file, parse it, add subtitle track
var filePath = file.path || file const filePath = file.path || file
var vttStream = fs.createReadStream(filePath).pipe(srtToVtt()) const vttStream = fs.createReadStream(filePath).pipe(srtToVtt())
concat(vttStream, function (err, buf) { concat(vttStream, function (err, buf) {
if (err) return dispatch('error', 'Can\'t parse subtitles file.') if (err) return dispatch('error', 'Can\'t parse subtitles file.')
// Detect what language the subtitles are in // Detect what language the subtitles are in
var vttContents = buf.toString().replace(/(.*-->.*)/g, '') const vttContents = buf.toString().replace(/(.*-->.*)/g, '')
var langDetected = (new LanguageDetect()).detect(vttContents, 2) let langDetected = (new LanguageDetect()).detect(vttContents, 2)
langDetected = langDetected.length ? langDetected[0][0] : 'subtitle' langDetected = langDetected.length ? langDetected[0][0] : 'subtitle'
langDetected = langDetected.slice(0, 1).toUpperCase() + langDetected.slice(1) langDetected = langDetected.slice(0, 1).toUpperCase() + langDetected.slice(1)
var track = { const track = {
buffer: 'data:text/vtt;base64,' + buf.toString('base64'), buffer: 'data:text/vtt;base64,' + buf.toString('base64'),
language: langDetected, language: langDetected,
label: langDetected, label: langDetected,
@@ -121,18 +120,18 @@ function loadSubtitle (file, cb) {
// Checks whether a language name like 'English' or 'German' matches the system // Checks whether a language name like 'English' or 'German' matches the system
// language, aka the current locale // language, aka the current locale
function isSystemLanguage (language) { function isSystemLanguage (language) {
var iso639 = require('iso-639-1') const iso639 = require('iso-639-1')
var osLangISO = window.navigator.language.split('-')[0] // eg 'en' const osLangISO = window.navigator.language.split('-')[0] // eg 'en'
var langIso = iso639.getCode(language) // eg 'de' if language is 'German' const langIso = iso639.getCode(language) // eg 'de' if language is 'German'
return langIso === osLangISO return langIso === osLangISO
} }
// Make sure we don't have two subtitle tracks with the same label // Make sure we don't have two subtitle tracks with the same label
// Labels each track by language, eg 'German', 'English', 'English 2', ... // Labels each track by language, eg 'German', 'English', 'English 2', ...
function relabelSubtitles (subtitles) { function relabelSubtitles (subtitles) {
var counts = {} const counts = {}
subtitles.tracks.forEach(function (track) { subtitles.tracks.forEach(function (track) {
var lang = track.language const lang = track.language
counts[lang] = (counts[lang] || 0) + 1 counts[lang] = (counts[lang] || 0) + 1
track.label = counts[lang] > 1 ? (lang + ' ' + counts[lang]) : lang track.label = counts[lang] > 1 ? (lang + ' ' + counts[lang]) : lang
}) })

View File

@@ -2,7 +2,6 @@ const path = require('path')
const ipcRenderer = require('electron').ipcRenderer const ipcRenderer = require('electron').ipcRenderer
const TorrentSummary = require('../lib/torrent-summary') const TorrentSummary = require('../lib/torrent-summary')
const TorrentPlayer = require('../lib/torrent-player')
const sound = require('../lib/sound') const sound = require('../lib/sound')
const {dispatch} = require('../lib/dispatcher') const {dispatch} = require('../lib/dispatcher')
@@ -12,12 +11,12 @@ module.exports = class TorrentController {
} }
torrentInfoHash (torrentKey, infoHash) { torrentInfoHash (torrentKey, infoHash) {
var torrentSummary = this.getTorrentSummary(torrentKey) let torrentSummary = this.getTorrentSummary(torrentKey)
console.log('got infohash for %s torrent %s', console.log('got infohash for %s torrent %s',
torrentSummary ? 'existing' : 'new', torrentKey) torrentSummary ? 'existing' : 'new', torrentKey)
if (!torrentSummary) { if (!torrentSummary) {
var torrents = this.state.saved.torrents const torrents = this.state.saved.torrents
// Check if an existing (non-active) torrent has the same info hash // Check if an existing (non-active) torrent has the same info hash
if (torrents.find((t) => t.infoHash === infoHash)) { if (torrents.find((t) => t.infoHash === infoHash)) {
@@ -49,7 +48,7 @@ module.exports = class TorrentController {
} }
dispatch('error', message) dispatch('error', message)
var torrentSummary = this.getTorrentSummary(torrentKey) const torrentSummary = this.getTorrentSummary(torrentKey)
if (torrentSummary) { if (torrentSummary) {
console.log('Pausing torrent %s due to error: %s', torrentSummary.infoHash, message) console.log('Pausing torrent %s due to error: %s', torrentSummary.infoHash, message)
torrentSummary.status = 'paused' torrentSummary.status = 'paused'
@@ -59,21 +58,20 @@ module.exports = class TorrentController {
torrentMetadata (torrentKey, torrentInfo) { torrentMetadata (torrentKey, torrentInfo) {
// Summarize torrent // Summarize torrent
var torrentSummary = this.getTorrentSummary(torrentKey) const torrentSummary = this.getTorrentSummary(torrentKey)
torrentSummary.status = 'downloading' torrentSummary.status = 'downloading'
torrentSummary.name = torrentSummary.displayName || torrentInfo.name torrentSummary.name = torrentSummary.displayName || torrentInfo.name
torrentSummary.path = torrentInfo.path torrentSummary.path = torrentInfo.path
torrentSummary.magnetURI = torrentInfo.magnetURI torrentSummary.magnetURI = torrentInfo.magnetURI
// TODO: make torrentInfo immutable, save separately as torrentSummary.info // TODO: make torrentInfo immutable, save separately as torrentSummary.info
// For now, check whether torrentSummary.files has already been set: // For now, check whether torrentSummary.files has already been set:
var hasDetailedFileInfo = torrentSummary.files && torrentSummary.files[0].path const hasDetailedFileInfo = torrentSummary.files && torrentSummary.files[0].path
if (!hasDetailedFileInfo) { if (!hasDetailedFileInfo) {
torrentSummary.files = torrentInfo.files torrentSummary.files = torrentInfo.files
} }
if (!torrentSummary.selections) { if (!torrentSummary.selections) {
torrentSummary.selections = torrentSummary.files.map((x) => true) torrentSummary.selections = torrentSummary.files.map((x) => true)
} }
torrentSummary.defaultPlayFileIndex = TorrentPlayer.pickFileToPlay(torrentInfo.files)
dispatch('update') dispatch('update')
// Save the .torrent file, if it hasn't been saved already // Save the .torrent file, if it hasn't been saved already
@@ -85,7 +83,7 @@ module.exports = class TorrentController {
torrentDone (torrentKey, torrentInfo) { torrentDone (torrentKey, torrentInfo) {
// Update the torrent summary // Update the torrent summary
var torrentSummary = this.getTorrentSummary(torrentKey) const torrentSummary = this.getTorrentSummary(torrentKey)
torrentSummary.status = 'seeding' torrentSummary.status = 'seeding'
// Notify the user that a torrent finished, but only if we actually DL'd at least part of it. // Notify the user that a torrent finished, but only if we actually DL'd at least part of it.
@@ -102,22 +100,18 @@ module.exports = class TorrentController {
} }
torrentProgress (progressInfo) { torrentProgress (progressInfo) {
// Overall progress across all active torrents, 0 to 1 // Overall progress across all active torrents, 0 to 1, or -1 to hide the progress bar
var progress = progressInfo.progress
var hasActiveTorrents = progressInfo.hasActiveTorrents
// Hide progress bar when client has no torrents, or progress is 100% // Hide progress bar when client has no torrents, or progress is 100%
// TODO: isn't this equivalent to: if (progress === 1) ? const progress = (!progressInfo.hasActiveTorrents || progressInfo.progress === 1)
if (!hasActiveTorrents || progress === 1) { ? -1
progress = -1 : progressInfo.progress
}
// Show progress bar under the WebTorrent taskbar icon, on OSX // Show progress bar under the WebTorrent taskbar icon, on OSX
this.state.dock.progress = progress this.state.dock.progress = progress
// Update progress for each individual torrent // Update progress for each individual torrent
progressInfo.torrents.forEach((p) => { progressInfo.torrents.forEach((p) => {
var torrentSummary = this.getTorrentSummary(p.torrentKey) const torrentSummary = this.getTorrentSummary(p.torrentKey)
if (!torrentSummary) { if (!torrentSummary) {
console.log('warning: got progress for missing torrent %s', p.torrentKey) console.log('warning: got progress for missing torrent %s', p.torrentKey)
return return
@@ -132,27 +126,28 @@ module.exports = class TorrentController {
} }
torrentFileModtimes (torrentKey, fileModtimes) { torrentFileModtimes (torrentKey, fileModtimes) {
var torrentSummary = this.getTorrentSummary(torrentKey) const torrentSummary = this.getTorrentSummary(torrentKey)
if (!torrentSummary) throw new Error('Not saving modtimes for deleted torrent ' + torrentKey)
torrentSummary.fileModtimes = fileModtimes torrentSummary.fileModtimes = fileModtimes
dispatch('saveStateThrottled') dispatch('saveStateThrottled')
} }
torrentFileSaved (torrentKey, torrentFileName) { torrentFileSaved (torrentKey, torrentFileName) {
console.log('torrent file saved %s: %s', torrentKey, torrentFileName) console.log('torrent file saved %s: %s', torrentKey, torrentFileName)
var torrentSummary = this.getTorrentSummary(torrentKey) const torrentSummary = this.getTorrentSummary(torrentKey)
torrentSummary.torrentFileName = torrentFileName torrentSummary.torrentFileName = torrentFileName
dispatch('saveStateThrottled') dispatch('saveStateThrottled')
} }
torrentPosterSaved (torrentKey, posterFileName) { torrentPosterSaved (torrentKey, posterFileName) {
var torrentSummary = this.getTorrentSummary(torrentKey) const torrentSummary = this.getTorrentSummary(torrentKey)
torrentSummary.posterFileName = posterFileName torrentSummary.posterFileName = posterFileName
dispatch('saveStateThrottled') dispatch('saveStateThrottled')
} }
torrentAudioMetadata (infoHash, index, info) { torrentAudioMetadata (infoHash, index, info) {
var torrentSummary = this.getTorrentSummary(infoHash) const torrentSummary = this.getTorrentSummary(infoHash)
var fileSummary = torrentSummary.files[index] const fileSummary = torrentSummary.files[index]
fileSummary.audioInfo = info fileSummary.audioInfo = info
dispatch('update') dispatch('update')
} }
@@ -169,7 +164,7 @@ module.exports = class TorrentController {
} }
function getTorrentPath (torrentSummary) { function getTorrentPath (torrentSummary) {
var itemPath = TorrentSummary.getFileOrFolder(torrentSummary) let itemPath = TorrentSummary.getFileOrFolder(torrentSummary)
if (torrentSummary.files.length > 1) { if (torrentSummary.files.length > 1) {
itemPath = path.dirname(itemPath) itemPath = path.dirname(itemPath)
} }
@@ -177,12 +172,12 @@ function getTorrentPath (torrentSummary) {
} }
function showDoneNotification (torrent) { function showDoneNotification (torrent) {
var notif = new window.Notification('Download Complete', { const notif = new window.Notification('Download Complete', {
body: torrent.name, body: torrent.name,
silent: true silent: true
}) })
notif.onClick = function () { notif.onclick = function () {
ipcRenderer.send('show') ipcRenderer.send('show')
} }

View File

@@ -3,6 +3,7 @@ const path = require('path')
const electron = require('electron') const electron = require('electron')
const {dispatch} = require('../lib/dispatcher') const {dispatch} = require('../lib/dispatcher')
const {TorrentKeyNotFoundError} = require('../lib/errors')
const State = require('../lib/state') const State = require('../lib/state')
const sound = require('../lib/sound') const sound = require('../lib/sound')
const TorrentSummary = require('../lib/torrent-summary') const TorrentSummary = require('../lib/torrent-summary')
@@ -17,8 +18,8 @@ module.exports = class TorrentListController {
this.state = state this.state = state
} }
// Adds a torrent to the list, starts downloading/seeding. TorrentID can be a // Adds a torrent to the list, starts downloading/seeding.
// magnet URI, infohash, or torrent file: https://github.com/feross/webtorrent#clientaddtorrentid-opts-function-ontorrent-torrent- // TorrentID can be a magnet URI, infohash, or torrent file: https://git.io/vik9M
addTorrent (torrentId) { addTorrent (torrentId) {
if (torrentId.path) { if (torrentId.path) {
// Use path string instead of W3C File object // Use path string instead of W3C File object
@@ -30,8 +31,8 @@ module.exports = class TorrentListController {
torrentId = torrentId.slice(torrentId.indexOf('#') + 1) torrentId = torrentId.slice(torrentId.indexOf('#') + 1)
} }
var torrentKey = this.state.nextTorrentKey++ const torrentKey = this.state.nextTorrentKey++
var path = this.state.saved.prefs.downloadPath const path = this.state.saved.prefs.downloadPath
ipcRenderer.send('wt-start-torrenting', torrentKey, torrentId, path) ipcRenderer.send('wt-start-torrenting', torrentKey, torrentId, path)
@@ -66,16 +67,16 @@ module.exports = class TorrentListController {
// Creates a new torrent and start seeeding // Creates a new torrent and start seeeding
createTorrent (options) { createTorrent (options) {
var state = this.state const state = this.state
var torrentKey = state.nextTorrentKey++ const torrentKey = state.nextTorrentKey++
ipcRenderer.send('wt-create-torrent', torrentKey, options) ipcRenderer.send('wt-create-torrent', torrentKey, options)
state.location.cancel() state.location.cancel()
} }
// Starts downloading and/or seeding a given torrentSummary. // Starts downloading and/or seeding a given torrentSummary.
startTorrentingSummary (torrentKey) { startTorrentingSummary (torrentKey) {
var s = TorrentSummary.getByKey(this.state, torrentKey) const s = TorrentSummary.getByKey(this.state, torrentKey)
if (!s) throw new Error('Missing key: ' + torrentKey) if (!s) throw new TorrentKeyNotFoundError(torrentKey)
// New torrent: give it a path // New torrent: give it a path
if (!s.path) { if (!s.path) {
@@ -84,8 +85,13 @@ module.exports = class TorrentListController {
return start() return start()
} }
const fileOrFolder = TorrentSummary.getFileOrFolder(s)
// New torrent: metadata not yet received
if (!fileOrFolder) return start()
// Existing torrent: check that the path is still there // Existing torrent: check that the path is still there
fs.stat(TorrentSummary.getFileOrFolder(s), function (err) { fs.stat(fileOrFolder, function (err) {
if (err) { if (err) {
s.error = 'path-missing' s.error = 'path-missing'
return return
@@ -105,7 +111,7 @@ module.exports = class TorrentListController {
// TODO: use torrentKey, not infoHash // TODO: use torrentKey, not infoHash
toggleTorrent (infoHash) { toggleTorrent (infoHash) {
var torrentSummary = TorrentSummary.getByKey(this.state, infoHash) const torrentSummary = TorrentSummary.getByKey(this.state, infoHash)
if (torrentSummary.status === 'paused') { if (torrentSummary.status === 'paused') {
torrentSummary.status = 'new' torrentSummary.status = 'new'
this.startTorrentingSummary(torrentSummary.torrentKey) this.startTorrentingSummary(torrentSummary.torrentKey)
@@ -118,11 +124,13 @@ module.exports = class TorrentListController {
} }
toggleTorrentFile (infoHash, index) { toggleTorrentFile (infoHash, index) {
var torrentSummary = TorrentSummary.getByKey(this.state, infoHash) const torrentSummary = TorrentSummary.getByKey(this.state, infoHash)
torrentSummary.selections[index] = !torrentSummary.selections[index] torrentSummary.selections[index] = !torrentSummary.selections[index]
// Let the WebTorrent process know to start or stop fetching that file // Let the WebTorrent process know to start or stop fetching that file
ipcRenderer.send('wt-select-files', infoHash, torrentSummary.selections) if (torrentSummary.status !== 'paused') {
ipcRenderer.send('wt-select-files', infoHash, torrentSummary.selections)
}
} }
confirmDeleteTorrent (infoHash, deleteData) { confirmDeleteTorrent (infoHash, deleteData) {
@@ -137,14 +145,14 @@ module.exports = class TorrentListController {
deleteTorrent (infoHash, deleteData) { deleteTorrent (infoHash, deleteData) {
ipcRenderer.send('wt-stop-torrenting', infoHash) ipcRenderer.send('wt-stop-torrenting', infoHash)
var index = this.state.saved.torrents.findIndex((x) => x.infoHash === infoHash) const index = this.state.saved.torrents.findIndex((x) => x.infoHash === infoHash)
if (index > -1) { if (index > -1) {
var summary = this.state.saved.torrents[index] const summary = this.state.saved.torrents[index]
// remove torrent and poster file // remove torrent and poster file
deleteFile(TorrentSummary.getTorrentPath(summary)) deleteFile(TorrentSummary.getTorrentPath(summary))
deleteFile(TorrentSummary.getPosterPath(summary)) // TODO: will the css path hack affect windows? deleteFile(TorrentSummary.getPosterPath(summary))
// optionally delete the torrent data // optionally delete the torrent data
if (deleteData) moveItemToTrash(summary) if (deleteData) moveItemToTrash(summary)
@@ -154,7 +162,8 @@ module.exports = class TorrentListController {
State.saveThrottled(this.state) State.saveThrottled(this.state)
} }
this.state.location.clearForward('player') // prevent user from going forward to a deleted torrent // prevent user from going forward to a deleted torrent
this.state.location.clearForward('player')
sound.play('DELETE') sound.play('DELETE')
} }
@@ -167,8 +176,8 @@ module.exports = class TorrentListController {
} }
openTorrentContextMenu (infoHash) { openTorrentContextMenu (infoHash) {
var torrentSummary = TorrentSummary.getByKey(this.state, infoHash) const torrentSummary = TorrentSummary.getByKey(this.state, infoHash)
var menu = new electron.remote.Menu() const menu = new electron.remote.Menu()
menu.append(new electron.remote.MenuItem({ menu.append(new electron.remote.MenuItem({
label: 'Remove From List', label: 'Remove From List',
@@ -217,11 +226,11 @@ module.exports = class TorrentListController {
// Calls `cb` on success, calls `onError` on failure // Calls `cb` on success, calls `onError` on failure
function findFilesRecursive (paths, cb) { function findFilesRecursive (paths, cb) {
if (paths.length > 1) { if (paths.length > 1) {
var numComplete = 0 let numComplete = 0
var ret = [] let ret = []
paths.forEach(function (path) { paths.forEach(function (path) {
findFilesRecursive([path], function (fileObjs) { findFilesRecursive([path], function (fileObjs) {
ret = ret.concat(fileObjs) ret.push(...fileObjs)
if (++numComplete === paths.length) { if (++numComplete === paths.length) {
ret.sort((a, b) => a.path < b.path ? -1 : a.path > b.path) ret.sort((a, b) => a.path < b.path ? -1 : a.path > b.path)
cb(ret) cb(ret)
@@ -231,13 +240,13 @@ function findFilesRecursive (paths, cb) {
return return
} }
var fileOrFolder = paths[0] const fileOrFolder = paths[0]
fs.stat(fileOrFolder, function (err, stat) { fs.stat(fileOrFolder, function (err, stat) {
if (err) return dispatch('error', err) if (err) return dispatch('error', err)
// Files: return name, path, and size // Files: return name, path, and size
if (!stat.isDirectory()) { if (!stat.isDirectory()) {
var filePath = fileOrFolder const filePath = fileOrFolder
return cb([{ return cb([{
name: path.basename(filePath), name: path.basename(filePath),
path: filePath, path: filePath,
@@ -246,10 +255,10 @@ function findFilesRecursive (paths, cb) {
} }
// Folders: recurse, make a list of all the files // Folders: recurse, make a list of all the files
var folderPath = fileOrFolder const folderPath = fileOrFolder
fs.readdir(folderPath, function (err, fileNames) { fs.readdir(folderPath, function (err, fileNames) {
if (err) return dispatch('error', err) if (err) return dispatch('error', err)
var paths = fileNames.map((fileName) => path.join(folderPath, fileName)) const paths = fileNames.map((fileName) => path.join(folderPath, fileName))
findFilesRecursive(paths, cb) findFilesRecursive(paths, cb)
}) })
}) })
@@ -264,7 +273,7 @@ function deleteFile (path) {
// Delete all files in a torrent // Delete all files in a torrent
function moveItemToTrash (torrentSummary) { function moveItemToTrash (torrentSummary) {
var filePath = TorrentSummary.getFileOrFolder(torrentSummary) const filePath = TorrentSummary.getFileOrFolder(torrentSummary)
if (filePath) ipcRenderer.send('moveItemToTrash', filePath) if (filePath) ipcRenderer.send('moveItemToTrash', filePath)
} }
@@ -273,9 +282,9 @@ function showItemInFolder (torrentSummary) {
} }
function saveTorrentFileAs (torrentSummary) { function saveTorrentFileAs (torrentSummary) {
var downloadPath = this.state.saved.prefs.downloadPath const downloadPath = this.state.saved.prefs.downloadPath
var newFileName = path.parse(torrentSummary.name).name + '.torrent' const newFileName = path.parse(torrentSummary.name).name + '.torrent'
var opts = { const opts = {
title: 'Save Torrent File', title: 'Save Torrent File',
defaultPath: path.join(downloadPath, newFileName), defaultPath: path.join(downloadPath, newFileName),
filters: [ filters: [
@@ -283,9 +292,10 @@ function saveTorrentFileAs (torrentSummary) {
{ name: 'All Files', extensions: ['*'] } { name: 'All Files', extensions: ['*'] }
] ]
} }
electron.remote.dialog.showSaveDialog(electron.remote.getCurrentWindow(), opts, function (savePath) { const win = electron.remote.getCurrentWindow()
electron.remote.dialog.showSaveDialog(win, opts, function (savePath) {
if (!savePath) return // They clicked Cancel if (!savePath) return // They clicked Cancel
var torrentPath = TorrentSummary.getTorrentPath(torrentSummary) const torrentPath = TorrentSummary.getTorrentPath(torrentSummary)
fs.readFile(torrentPath, function (err, torrentFile) { fs.readFile(torrentPath, function (err, torrentFile) {
if (err) return dispatch('error', err) if (err) return dispatch('error', err)
fs.writeFile(savePath, torrentFile, function (err) { fs.writeFile(savePath, torrentFile, function (err) {

View File

@@ -8,7 +8,7 @@ module.exports = class UpdateController {
// Shows a modal saying that we have an update // Shows a modal saying that we have an update
updateAvailable (version) { updateAvailable (version) {
var skipped = this.state.saved.skippedVersions const skipped = this.state.saved.skippedVersions
if (skipped && skipped.includes(version)) { if (skipped && skipped.includes(version)) {
console.log('new version skipped by user: v' + version) console.log('new version skipped by user: v' + version)
return return
@@ -18,7 +18,7 @@ module.exports = class UpdateController {
// Don't show the modal again until the next version // Don't show the modal again until the next version
skipVersion (version) { skipVersion (version) {
var skipped = this.state.saved.skippedVersions let skipped = this.state.saved.skippedVersions
if (!skipped) skipped = this.state.saved.skippedVersions = [] if (!skipped) skipped = this.state.saved.skippedVersions = []
skipped.push(version) skipped.push(version)
State.saveThrottled(this.state) State.saveThrottled(this.state)

View File

@@ -1,12 +1,14 @@
module.exports = captureVideoFrame module.exports = captureVideoFrame
const {IllegalArgumentError} = require('./errors')
function captureVideoFrame (video, format) { function captureVideoFrame (video, format) {
if (typeof video === 'string') { if (typeof video === 'string') {
video = document.querySelector(video) video = document.querySelector(video)
} }
if (video == null || video.nodeName !== 'VIDEO') { if (video == null || video.nodeName !== 'VIDEO') {
throw new Error('First argument must be a <video> element or selector') throw new IllegalArgumentError('First argument must be a <video> element or selector')
} }
if (format == null) { if (format == null) {
@@ -14,17 +16,17 @@ function captureVideoFrame (video, format) {
} }
if (format !== 'png' && format !== 'jpg' && format !== 'webp') { if (format !== 'png' && format !== 'jpg' && format !== 'webp') {
throw new Error('Second argument must be one of "png", "jpg", or "webp"') throw new IllegalArgumentError('Second argument must be one of "png", "jpg", or "webp"')
} }
var canvas = document.createElement('canvas') const canvas = document.createElement('canvas')
canvas.width = video.videoWidth canvas.width = video.videoWidth
canvas.height = video.videoHeight canvas.height = video.videoHeight
canvas.getContext('2d').drawImage(video, 0, 0) canvas.getContext('2d').drawImage(video, 0, 0)
var dataUri = canvas.toDataURL('image/' + format) const dataUri = canvas.toDataURL('image/' + format)
var data = dataUri.split(',')[1] const data = dataUri.split(',')[1]
return new Buffer(data, 'base64') return new Buffer(data, 'base64')
} }

View File

@@ -13,19 +13,20 @@ module.exports = {
setRate setRate
} }
// Lazy load these for a ~300ms improvement in startup time const config = require('../../config')
var airplayer, chromecasts, dlnacasts const {CastingError} = require('./errors')
var config = require('../../config') // Lazy load these for a ~300ms improvement in startup time
let airplayer, chromecasts, dlnacasts
// App state. Cast modifies state.playing and state.errors in response to events // App state. Cast modifies state.playing and state.errors in response to events
var state let state
// Callback to notify module users when state has changed // Callback to notify module users when state has changed
var update let update
// setInterval() for updating cast status // setInterval() for updating cast status
var statusInterval = null let statusInterval = null
// Start looking for cast devices on the local network // Start looking for cast devices on the local network
function init (appState, callback) { function init (appState, callback) {
@@ -59,7 +60,7 @@ function init (appState, callback) {
// chromecast player implementation // chromecast player implementation
function chromecastPlayer () { function chromecastPlayer () {
var ret = { const ret = {
device: null, device: null,
addDevice, addDevice,
getDevices, getDevices,
@@ -95,8 +96,8 @@ function chromecastPlayer () {
} }
function open () { function open () {
var torrentSummary = state.saved.torrents.find((x) => x.infoHash === state.playing.infoHash) const torrentSummary = state.saved.torrents.find((x) => x.infoHash === state.playing.infoHash)
ret.device.play(state.server.networkURL, { ret.device.play(state.server.networkURL + '/' + state.playing.fileIndex, {
type: 'video/mp4', type: 'video/mp4',
title: config.APP_NAME + ' - ' + torrentSummary.name title: config.APP_NAME + ' - ' + torrentSummary.name
}, function (err) { }, function (err) {
@@ -126,13 +127,7 @@ function chromecastPlayer () {
} }
function status () { function status () {
ret.device.status(function (err, status) { ret.device.status(handleStatus)
if (err) return console.log('error getting %s status: %o', state.playing.location, err)
state.playing.isPaused = status.playerState === 'PAUSED'
state.playing.currentTime = status.currentTime
state.playing.volume = status.volume.muted ? 0 : status.volume.level
update()
})
} }
function seek (time, callback) { function seek (time, callback) {
@@ -146,7 +141,7 @@ function chromecastPlayer () {
// airplay player implementation // airplay player implementation
function airplayPlayer () { function airplayPlayer () {
var ret = { const ret = {
device: null, device: null,
addDevice, addDevice,
getDevices, getDevices,
@@ -183,7 +178,7 @@ function airplayPlayer () {
} }
function open () { function open () {
ret.device.play(state.server.networkURL, function (err, res) { ret.device.play(state.server.networkURL + '/' + state.playing.fileIndex, function (err, res) {
if (err) { if (err) {
state.playing.location = 'local' state.playing.location = 'local'
state.errors.push({ state.errors.push({
@@ -238,7 +233,7 @@ function airplayPlayer () {
// DLNA player implementation // DLNA player implementation
function dlnaPlayer (player) { function dlnaPlayer (player) {
var ret = { const ret = {
device: null, device: null,
addDevice, addDevice,
getDevices, getDevices,
@@ -274,8 +269,8 @@ function dlnaPlayer (player) {
} }
function open () { function open () {
var torrentSummary = state.saved.torrents.find((x) => x.infoHash === state.playing.infoHash) const torrentSummary = state.saved.torrents.find((x) => x.infoHash === state.playing.infoHash)
ret.device.play(state.server.networkURL, { ret.device.play(state.server.networkURL + '/' + state.playing.fileIndex, {
type: 'video/mp4', type: 'video/mp4',
title: config.APP_NAME + ' - ' + torrentSummary.name, title: config.APP_NAME + ' - ' + torrentSummary.name,
seek: state.playing.currentTime > 10 ? state.playing.currentTime : 0 seek: state.playing.currentTime > 10 ? state.playing.currentTime : 0
@@ -306,13 +301,7 @@ function dlnaPlayer (player) {
} }
function status () { function status () {
ret.device.status(function (err, status) { ret.device.status(handleStatus)
if (err) return console.log('error getting %s status: %o', state.playing.location, err)
state.playing.isPaused = status.playerState === 'PAUSED'
state.playing.currentTime = status.currentTime
state.playing.volume = status.volume.level
update()
})
} }
function seek (time, callback) { function seek (time, callback) {
@@ -328,10 +317,22 @@ function dlnaPlayer (player) {
} }
} }
function handleStatus (err, status) {
if (err || !status) {
return console.log('error getting %s status: %o',
state.playing.location,
err || 'missing response')
}
state.playing.isPaused = status.playerState === 'PAUSED'
state.playing.currentTime = status.currentTime
state.playing.volume = status.volume.muted ? 0 : status.volume.level
update()
}
// Start polling cast device state, whenever we're connected // Start polling cast device state, whenever we're connected
function startStatusInterval () { function startStatusInterval () {
statusInterval = setInterval(function () { statusInterval = setInterval(function () {
var player = getPlayer() const player = getPlayer()
if (player) player.status() if (player) player.status()
}, 1000) }, 1000)
} }
@@ -350,23 +351,26 @@ function toggleMenu (location) {
// Never cast to two devices at the same time // Never cast to two devices at the same time
if (state.playing.location !== 'local') { if (state.playing.location !== 'local') {
throw new Error('You can\'t connect to ' + location + ' when already connected to another device') throw new CastingError(
} `You can't connect to ${location} when already connected to another device`
) }
// Find all cast devices of the given type // Find all cast devices of the given type
var player = getPlayer(location) const player = getPlayer(location)
var devices = player ? player.getDevices() : [] const devices = player ? player.getDevices() : []
if (devices.length === 0) throw new Error('No ' + location + ' devices available') if (devices.length === 0) {
throw new CastingError(`No ${location} devices available`)
}
// Show a menu // Show a menu
state.devices.castMenu = {location, devices} state.devices.castMenu = {location, devices}
} }
function selectDevice (index) { function selectDevice (index) {
var {location, devices} = state.devices.castMenu const {location, devices} = state.devices.castMenu
// Start casting // Start casting
var player = getPlayer(location) const player = getPlayer(location)
player.device = devices[index] player.device = devices[index]
player.open() player.open()
@@ -382,7 +386,7 @@ function selectDevice (index) {
// Stops casting, move video back to local screen // Stops casting, move video back to local screen
function stop () { function stop () {
var player = getPlayer() const player = getPlayer()
if (player) { if (player) {
player.stop(function () { player.stop(function () {
player.device = null player.device = null
@@ -417,18 +421,18 @@ function getPlayer (location) {
} }
function play () { function play () {
var player = getPlayer() const player = getPlayer()
if (player) player.play(castCallback) if (player) player.play(castCallback)
} }
function pause () { function pause () {
var player = getPlayer() const player = getPlayer()
if (player) player.pause(castCallback) if (player) player.pause(castCallback)
} }
function setRate (rate) { function setRate (rate) {
var player let player
var result = true let result = true
if (state.playing.location === 'chromecast') { if (state.playing.location === 'chromecast') {
// TODO find how to control playback rate on chromecast // TODO find how to control playback rate on chromecast
castCallback() castCallback()
@@ -443,12 +447,12 @@ function setRate (rate) {
} }
function seek (time) { function seek (time) {
var player = getPlayer() const player = getPlayer()
if (player) player.seek(time, castCallback) if (player) player.seek(time, castCallback)
} }
function setVolume (volume) { function setVolume (volume) {
var player = getPlayer() const player = getPlayer()
if (player) player.volume(volume, castCallback) if (player) player.volume(volume, castCallback)
} }

View File

@@ -4,8 +4,8 @@ module.exports = {
setDispatch setDispatch
} }
var dispatchers = {} const dispatchers = {}
var _dispatch = function () {} let _dispatch = function () {}
function setDispatch (dispatch) { function setDispatch (dispatch) {
_dispatch = dispatch _dispatch = dispatch
@@ -20,8 +20,8 @@ function dispatch (...args) {
// function. This prevents React from updating the listener functions on // function. This prevents React from updating the listener functions on
// each update(). // each update().
function dispatcher (...args) { function dispatcher (...args) {
var str = JSON.stringify(args) const str = JSON.stringify(args)
var handler = dispatchers[str] let handler = dispatchers[str]
if (!handler) { if (!handler) {
handler = dispatchers[str] = function (e) { handler = dispatchers[str] = function (e) {
// Do not propagate click to elements below the button // Do not propagate click to elements below the button

View File

@@ -1,8 +1,54 @@
module.exports = { const ExtendableError = require('es6-error')
UnplayableError
/* Generic errors */
class CastingError extends ExtendableError {}
class PlaybackError extends ExtendableError {}
class SoundError extends ExtendableError {}
class TorrentError extends ExtendableError {}
/* Playback */
class UnplayableTorrentError extends PlaybackError {
constructor () { super('Can\'t play any files in torrent') }
} }
function UnplayableError () { class UnplayableFileError extends PlaybackError {
this.message = 'Can\'t play any files in torrent' constructor () { super('Can\'t play that file') }
}
class PlaybackTimedOutError extends PlaybackError {
constructor () { super('Playback timed out. Try again.') }
}
/* Sound */
class InvalidSoundNameError extends SoundError {
constructor (name) { super(`Invalid sound name: ${name}`) }
}
/* Torrent */
class TorrentKeyNotFoundError extends TorrentError {
constructor (torrentKey) { super(`Can't resolve torrent key ${torrentKey}`) }
}
class InvalidTorrentError extends TorrentError {}
/* Miscellaneous */
class IllegalArgumentError extends ExtendableError {}
module.exports = {
CastingError,
PlaybackError,
SoundError,
TorrentError,
UnplayableTorrentError,
UnplayableFileError,
PlaybackTimedOutError,
InvalidSoundNameError,
TorrentKeyNotFoundError,
InvalidTorrentError,
IllegalArgumentError
} }
UnplayableError.prototype = Error

View File

@@ -17,7 +17,7 @@ function run (state) {
state.saved.version = '0.0.0' // Pre-0.7.0 version, so run all migrations state.saved.version = '0.0.0' // Pre-0.7.0 version, so run all migrations
} }
var version = state.saved.version const version = state.saved.version
if (semver.lt(version, '0.7.0')) { if (semver.lt(version, '0.7.0')) {
migrate_0_7_0(state.saved) migrate_0_7_0(state.saved)
@@ -35,16 +35,20 @@ function run (state) {
migrate_0_12_0(state.saved) migrate_0_12_0(state.saved)
} }
if (semver.lt(version, '0.14.0')) {
migrate_0_14_0(state.saved)
}
// Config is now on the new version // Config is now on the new version
state.saved.version = config.APP_VERSION state.saved.version = config.APP_VERSION
} }
function migrate_0_7_0 (saved) { function migrate_0_7_0 (saved) {
var fs = require('fs-extra') const fs = require('fs-extra')
var path = require('path') const path = require('path')
saved.torrents.forEach(function (ts) { saved.torrents.forEach(function (ts) {
var infoHash = ts.infoHash const infoHash = ts.infoHash
// Replace torrentPath with torrentFileName // Replace torrentPath with torrentFileName
// There are a number of cases to handle here: // There are a number of cases to handle here:
@@ -52,7 +56,7 @@ function migrate_0_7_0 (saved) {
// * Then, relative paths for the default torrents, eg '../static/sintel.torrent' // * Then, relative paths for the default torrents, eg '../static/sintel.torrent'
// * Then, paths computed at runtime for default torrents, eg 'sintel.torrent' // * Then, paths computed at runtime for default torrents, eg 'sintel.torrent'
// * Finally, now we're getting rid of torrentPath altogether // * Finally, now we're getting rid of torrentPath altogether
var src, dst let src, dst
if (ts.torrentPath) { if (ts.torrentPath) {
if (path.isAbsolute(ts.torrentPath) || ts.torrentPath.startsWith('..')) { if (path.isAbsolute(ts.torrentPath) || ts.torrentPath.startsWith('..')) {
src = ts.torrentPath src = ts.torrentPath
@@ -70,7 +74,7 @@ function migrate_0_7_0 (saved) {
// Replace posterURL with posterFileName // Replace posterURL with posterFileName
if (ts.posterURL) { if (ts.posterURL) {
var extension = path.extname(ts.posterURL) const extension = path.extname(ts.posterURL)
src = path.isAbsolute(ts.posterURL) src = path.isAbsolute(ts.posterURL)
? ts.posterURL ? ts.posterURL
: path.join(config.STATIC_PATH, ts.posterURL) : path.join(config.STATIC_PATH, ts.posterURL)
@@ -116,7 +120,7 @@ function migrate_0_12_0 (saved) {
// Undo a terrible bug where clicking Play on a default torrent on a fresh // Undo a terrible bug where clicking Play on a default torrent on a fresh
// install results in a "path missing" error // install results in a "path missing" error
// See https://github.com/feross/webtorrent-desktop/pull/806 // See https://github.com/feross/webtorrent-desktop/pull/806
var defaultTorrentFiles = [ const defaultTorrentFiles = [
'6a9759bffd5c0af65319979fb7832189f4f3c35d.torrent', '6a9759bffd5c0af65319979fb7832189f4f3c35d.torrent',
'88594aaacbde40ef3e2510c47374ec0aa396c08e.torrent', '88594aaacbde40ef3e2510c47374ec0aa396c08e.torrent',
'6a02592d2bbc069628cd5ed8a54f88ee06ac0ba5.torrent', '6a02592d2bbc069628cd5ed8a54f88ee06ac0ba5.torrent',
@@ -125,7 +129,7 @@ function migrate_0_12_0 (saved) {
] ]
saved.torrents.forEach(function (torrentSummary) { saved.torrents.forEach(function (torrentSummary) {
if (!defaultTorrentFiles.includes(torrentSummary.torrentFileName)) return if (!defaultTorrentFiles.includes(torrentSummary.torrentFileName)) return
var fileOrFolder = TorrentSummary.getFileOrFolder(torrentSummary) const fileOrFolder = TorrentSummary.getFileOrFolder(torrentSummary)
if (!fileOrFolder) return if (!fileOrFolder) return
try { try {
fs.statSync(fileOrFolder) fs.statSync(fileOrFolder)
@@ -135,3 +139,9 @@ function migrate_0_12_0 (saved) {
} }
}) })
} }
function migrate_0_14_0 (saved) {
saved.torrents.forEach(function (ts) {
delete ts.defaultPlayFileIndex
})
}

View File

@@ -0,0 +1,85 @@
module.exports = {
hasNext,
getNextIndex,
hasPrevious,
getPreviousIndex,
getCurrentLocalURL
}
const TorrentSummary = require('./torrent-summary')
const TorrentPlayer = require('./torrent-player')
const cache = {
infoHash: null,
previousIndex: null,
currentIndex: null,
nextIndex: null
}
function hasNext (state) {
updateCache(state)
return cache.nextIndex !== null
}
function getNextIndex (state) {
updateCache(state)
return cache.nextIndex
}
function hasPrevious (state) {
updateCache(state)
return cache.previousIndex !== null
}
function getPreviousIndex (state) {
updateCache(state)
return cache.previousIndex
}
function getCurrentLocalURL (state) {
return state.server.localURL + '/' + state.playing.fileIndex
}
function updateCache (state) {
const infoHash = state.playing.infoHash
const fileIndex = state.playing.fileIndex
if (infoHash === cache.infoHash) {
switch (fileIndex) {
case cache.currentIndex:
return
case cache.nextIndex:
cache.previousIndex = cache.currentIndex
cache.currentIndex = fileIndex
cache.nextIndex = findNextIndex(state)
return
case cache.previousIndex:
cache.previousIndex = findPreviousIndex(state)
cache.nextIndex = cache.currentIndex
cache.currentIndex = fileIndex
return
}
} else {
cache.infoHash = infoHash
}
cache.previousIndex = findPreviousIndex(state)
cache.currentIndex = fileIndex
cache.nextIndex = findNextIndex(state)
}
function findPreviousIndex (state) {
const files = TorrentSummary.getByKey(state, state.playing.infoHash).files
for (var i = state.playing.fileIndex - 1; i >= 0; i--) {
if (TorrentPlayer.isPlayable(files[i])) return i
}
return null
}
function findNextIndex (state) {
const files = TorrentSummary.getByKey(state, state.playing.infoHash).files
for (var i = state.playing.fileIndex + 1; i < files.length; i++) {
if (TorrentPlayer.isPlayable(files[i])) return i
}
return null
}

View File

@@ -3,15 +3,16 @@ module.exports = {
play play
} }
var config = require('../../config') const config = require('../../config')
var path = require('path') const {InvalidSoundNameError} = require('./errors')
const path = require('path')
var VOLUME = 0.15 const VOLUME = 0.15
/* Cache of Audio elements, for instant playback */ /* Cache of Audio elements, for instant playback */
var cache = {} const cache = {}
var sounds = { const sounds = {
ADD: { ADD: {
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'add.wav'), url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'add.wav'),
volume: VOLUME volume: VOLUME
@@ -47,10 +48,10 @@ var sounds = {
} }
function preload () { function preload () {
for (var name in sounds) { for (let name in sounds) {
if (!cache[name]) { if (!cache[name]) {
var sound = sounds[name] const sound = sounds[name]
var audio = cache[name] = new window.Audio() const audio = cache[name] = new window.Audio()
audio.volume = sound.volume audio.volume = sound.volume
audio.src = sound.url audio.src = sound.url
} }
@@ -58,11 +59,11 @@ function preload () {
} }
function play (name) { function play (name) {
var audio = cache[name] let audio = cache[name]
if (!audio) { if (!audio) {
var sound = sounds[name] const sound = sounds[name]
if (!sound) { if (!sound) {
throw new Error('Invalid sound name') throw new InvalidSoundNameError(name)
} }
audio = cache[name] = new window.Audio() audio = cache[name] = new window.Audio()
audio.volume = sound.volume audio.volume = sound.volume

View File

@@ -1,11 +1,11 @@
var appConfig = require('application-config')('WebTorrent') const appConfig = require('application-config')('WebTorrent')
var path = require('path') const path = require('path')
var {EventEmitter} = require('events') const {EventEmitter} = require('events')
var config = require('../../config') const config = require('../../config')
var migrations = require('./migrations') const migrations = require('./migrations')
var State = module.exports = Object.assign(new EventEmitter(), { const State = module.exports = Object.assign(new EventEmitter(), {
getDefaultPlayState, getDefaultPlayState,
load, load,
save, save,
@@ -15,7 +15,7 @@ var State = module.exports = Object.assign(new EventEmitter(), {
appConfig.filePath = path.join(config.CONFIG_PATH, 'config.json') appConfig.filePath = path.join(config.CONFIG_PATH, 'config.json')
function getDefaultState () { function getDefaultState () {
var LocationHistory = require('location-history') const LocationHistory = require('location-history')
return { return {
/* /*
@@ -68,7 +68,8 @@ function getDefaultState () {
* Getters, for convenience * Getters, for convenience
*/ */
getPlayingTorrentSummary, getPlayingTorrentSummary,
getPlayingFileSummary getPlayingFileSummary,
getExternalPlayerName
} }
} }
@@ -98,25 +99,26 @@ function getDefaultPlayState () {
/* If the saved state file doesn't exist yet, here's what we use instead */ /* If the saved state file doesn't exist yet, here's what we use instead */
function setupSavedState (cb) { function setupSavedState (cb) {
var fs = require('fs-extra') const fs = require('fs-extra')
var parseTorrent = require('parse-torrent') const parseTorrent = require('parse-torrent')
var parallel = require('run-parallel') const parallel = require('run-parallel')
var saved = { const saved = {
prefs: { prefs: {
downloadPath: config.DEFAULT_DOWNLOAD_PATH, downloadPath: config.DEFAULT_DOWNLOAD_PATH,
isFileHandler: false, isFileHandler: false,
openExternalPlayer: false, openExternalPlayer: false,
externalPlayerPath: null externalPlayerPath: null,
startup: false
}, },
torrents: config.DEFAULT_TORRENTS.map(createTorrentObject), torrents: config.DEFAULT_TORRENTS.map(createTorrentObject),
version: config.APP_VERSION /* make sure we can upgrade gracefully later */ version: config.APP_VERSION /* make sure we can upgrade gracefully later */
} }
var tasks = [] const tasks = []
config.DEFAULT_TORRENTS.map(function (t, i) { config.DEFAULT_TORRENTS.map(function (t, i) {
var infoHash = saved.torrents[i].infoHash const infoHash = saved.torrents[i].infoHash
tasks.push(function (cb) { tasks.push(function (cb) {
fs.copy( fs.copy(
path.join(config.STATIC_PATH, t.posterFileName), path.join(config.STATIC_PATH, t.posterFileName),
@@ -139,8 +141,8 @@ function setupSavedState (cb) {
}) })
function createTorrentObject (t) { function createTorrentObject (t) {
var torrent = fs.readFileSync(path.join(config.STATIC_PATH, t.torrentFileName)) const torrent = fs.readFileSync(path.join(config.STATIC_PATH, t.torrentFileName))
var parsedTorrent = parseTorrent(torrent) const parsedTorrent = parseTorrent(torrent)
return { return {
status: 'paused', status: 'paused',
@@ -151,24 +153,31 @@ function setupSavedState (cb) {
torrentFileName: parsedTorrent.infoHash + '.torrent', torrentFileName: parsedTorrent.infoHash + '.torrent',
magnetURI: parseTorrent.toMagnetURI(parsedTorrent), magnetURI: parseTorrent.toMagnetURI(parsedTorrent),
files: parsedTorrent.files, files: parsedTorrent.files,
selections: parsedTorrent.files.map((x) => true) selections: parsedTorrent.files.map((x) => true),
testID: t.testID
} }
} }
} }
function getPlayingTorrentSummary () { function getPlayingTorrentSummary () {
var infoHash = this.playing.infoHash const infoHash = this.playing.infoHash
return this.saved.torrents.find((x) => x.infoHash === infoHash) return this.saved.torrents.find((x) => x.infoHash === infoHash)
} }
function getPlayingFileSummary () { function getPlayingFileSummary () {
var torrentSummary = this.getPlayingTorrentSummary() const torrentSummary = this.getPlayingTorrentSummary()
if (!torrentSummary) return null if (!torrentSummary) return null
return torrentSummary.files[this.playing.fileIndex] return torrentSummary.files[this.playing.fileIndex]
} }
function getExternalPlayerName () {
const playerPath = this.saved.prefs.externalPlayerPath
if (!playerPath) return 'VLC'
return path.basename(playerPath).split('.')[0]
}
function load (cb) { function load (cb) {
var state = getDefaultState() const state = getDefaultState()
appConfig.read(function (err, saved) { appConfig.read(function (err, saved) {
if (err || !saved.version) { if (err || !saved.version) {
@@ -193,14 +202,14 @@ function save (state, cb) {
delete state.saveStateTimeout delete state.saveStateTimeout
// Clean up, so that we're not saving any pending state // Clean up, so that we're not saving any pending state
var copy = Object.assign({}, state.saved) const copy = Object.assign({}, state.saved)
// Remove torrents pending addition to the list, where we haven't finished // Remove torrents pending addition to the list, where we haven't finished
// reading the torrent file or file(s) to seed & don't have an infohash // reading the torrent file or file(s) to seed & don't have an infohash
copy.torrents = copy.torrents copy.torrents = copy.torrents
.filter((x) => x.infoHash) .filter((x) => x.infoHash)
.map(function (x) { .map(function (x) {
var torrent = {} const torrent = {}
for (var key in x) { for (let key in x) {
if (key === 'progress' || key === 'torrentKey') { if (key === 'progress' || key === 'torrentKey') {
continue // Don't save progress info or key for the webtorrent process continue // Don't save progress info or key for the webtorrent process
} }

View File

@@ -14,7 +14,7 @@ const url = require('url')
const config = require('../../config') const config = require('../../config')
var telemetry let telemetry
function init (state) { function init (state) {
telemetry = state.saved.telemetry telemetry = state.saved.telemetry
@@ -23,13 +23,14 @@ function init (state) {
reset() reset()
} }
var now = new Date() const now = new Date()
telemetry.version = config.APP_VERSION telemetry.version = config.APP_VERSION
telemetry.timestamp = now.toISOString() telemetry.timestamp = now.toISOString()
telemetry.localTime = now.toTimeString() telemetry.localTime = now.toTimeString()
telemetry.screens = getScreenInfo() telemetry.screens = getScreenInfo()
telemetry.system = getSystemInfo() telemetry.system = getSystemInfo()
telemetry.approxNumTorrents = getApproxNumTorrents(state) telemetry.torrentStats = getTorrentStats(state)
telemetry.approxNumTorrents = telemetry.torrentStats.approxCount
if (config.IS_PRODUCTION) { if (config.IS_PRODUCTION) {
postToServer() postToServer()
@@ -56,17 +57,17 @@ function reset () {
function postToServer () { function postToServer () {
// Serialize the telemetry summary // Serialize the telemetry summary
var payload = new Buffer(JSON.stringify(telemetry), 'utf8') const payload = new Buffer(JSON.stringify(telemetry), 'utf8')
// POST to our server // POST to our server
var options = url.parse(config.TELEMETRY_URL) const options = url.parse(config.TELEMETRY_URL)
options.method = 'POST' options.method = 'POST'
options.headers = { options.headers = {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Content-Length': payload.length 'Content-Length': payload.length
} }
var req = https.request(options, function (res) { const req = https.request(options, function (res) {
if (res.statusCode === 200) { if (res.statusCode === 200) {
console.log('Successfully posted telemetry summary') console.log('Successfully posted telemetry summary')
reset() reset()
@@ -85,7 +86,7 @@ function postToServer () {
// collects screen resolution, etc // collects screen resolution, etc
function createSummary () { function createSummary () {
// Make a 256-bit random unique ID // Make a 256-bit random unique ID
var userID = crypto.randomBytes(32).toString('hex') const userID = crypto.randomBytes(32).toString('hex')
return { userID } return { userID }
} }
@@ -104,18 +105,63 @@ function getSystemInfo () {
osPlatform: process.platform, osPlatform: process.platform,
osRelease: os.type() + ' ' + os.release(), osRelease: os.type() + ' ' + os.release(),
architecture: os.arch(), architecture: os.arch(),
totalMemoryMB: os.totalmem() / (1 << 20), totalMemoryMB: roundPow2(os.totalmem() / (1 << 20)),
numCores: os.cpus().length numCores: os.cpus().length
} }
} }
// Get the number of torrents, rounded to the nearest power of two // Get stats like the # of torrents currently active, # in list, total size
function getApproxNumTorrents (state) { function getTorrentStats (state) {
var exactNum = state.saved.torrents.length const count = state.saved.torrents.length
if (exactNum === 0) return 0 let sizeMB = 0
let byStatus = {
new: { count: 0, sizeMB: 0 },
downloading: { count: 0, sizeMB: 0 },
seeding: { count: 0, sizeMB: 0 },
paused: { count: 0, sizeMB: 0 }
}
// First, count torrents & total file size
for (let i = 0; i < count; i++) {
const t = state.saved.torrents[i]
const stat = byStatus[t.status]
if (!t || !t.files || !stat) continue
stat.count++
for (let j = 0; j < t.files.length; j++) {
const f = t.files[j]
if (!f || !f.length) continue
const fileSizeMB = f.length / (1 << 20)
sizeMB += fileSizeMB
stat.sizeMB += fileSizeMB
}
}
// Then, round all the counts and sums to the nearest power of 2
const ret = roundTorrentStats({count, sizeMB})
ret.byStatus = {
new: roundTorrentStats(byStatus.new),
downloading: roundTorrentStats(byStatus.downloading),
seeding: roundTorrentStats(byStatus.seeding),
paused: roundTorrentStats(byStatus.paused)
}
return ret
}
function roundTorrentStats (stats) {
return {
approxCount: roundPow2(stats.count),
approxSizeMB: roundPow2(stats.sizeMB)
}
}
// Rounds to the nearest power of 2, for privacy and easy bucketing.
// Rounds 35 to 32, 70 to 64, 5 to 4, 1 to 1, 0 to 0.
// Supports nonnegative numbers only.
function roundPow2 (n) {
if (n <= 0) return 0
// Otherwise, return 1, 2, 4, 8, etc by rounding in log space // Otherwise, return 1, 2, 4, 8, etc by rounding in log space
var log2 = Math.log(exactNum) / Math.log(2) const log2 = Math.log(n) / Math.log(2)
return 1 << Math.round(log2) return Math.pow(2, Math.round(log2))
} }
// An uncaught error happened in the main process or in one of the windows // An uncaught error happened in the main process or in one of the windows
@@ -124,14 +170,10 @@ function logUncaughtError (procName, e) {
// Hopefully uncaught errors immediately on startup are fixed in dev // Hopefully uncaught errors immediately on startup are fixed in dev
if (!telemetry) return if (!telemetry) return
var message let message
var stack = '' let stack = ''
if (e == null) { if (e == null) {
message = 'Unexpected undefined error' message = 'Unexpected undefined error'
} else if (e.message) {
// err is either an Error or a plain object {message, stack}
message = e.message
stack = e.stack
} else if (e.error) { } else if (e.error) {
// Uncaught Javascript errors (window.onerror), err is an ErrorEvent // Uncaught Javascript errors (window.onerror), err is an ErrorEvent
if (!e.error.message) { if (!e.error.message) {
@@ -140,6 +182,10 @@ function logUncaughtError (procName, e) {
message = e.error.message message = e.error.message
stack = e.error.stack stack = e.error.stack
} }
} else if (e.message) {
// err is either an Error or a plain object {message, stack}
message = e.message
stack = e.stack
} else { } else {
// Resource errors (captured element.onerror), err is an Event // Resource errors (captured element.onerror), err is an Event
if (!e.target) { if (!e.target) {
@@ -158,6 +204,7 @@ function logUncaughtError (procName, e) {
// - Privacy: remove personal info like C:\Users\<full name> // - Privacy: remove personal info like C:\Users\<full name>
// - Aggregation: this lets us find which stacktraces occur often // - Aggregation: this lets us find which stacktraces occur often
stack = stack.replace(/\(.*app.asar/g, '(...') stack = stack.replace(/\(.*app.asar/g, '(...')
stack = stack.replace(/at .*app.asar/g, 'at ...')
// We need to POST the telemetry object, make sure it stays < 100kb // We need to POST the telemetry object, make sure it stays < 100kb
if (telemetry.uncaughtErrors.length > 20) return if (telemetry.uncaughtErrors.length > 20) return
@@ -165,14 +212,14 @@ function logUncaughtError (procName, e) {
if (stack.length > 1000) stack = stack.substring(0, 1000) if (stack.length > 1000) stack = stack.substring(0, 1000)
// Log the app version *at the time of the error* // Log the app version *at the time of the error*
var version = config.APP_VERSION const version = config.APP_VERSION
telemetry.uncaughtErrors.push({process: procName, message, stack, version}) telemetry.uncaughtErrors.push({process: procName, message, stack, version})
} }
// Turns a DOM element into a string, eg "DIV.my-class.visible" // Turns a DOM element into a string, eg "DIV.my-class.visible"
function getElemString (elem) { function getElemString (elem) {
var ret = elem.tagName let ret = elem.tagName
try { try {
ret += '.' + Array.from(elem.classList).join('.') ret += '.' + Array.from(elem.classList).join('.')
} catch (e) {} } catch (e) {}
@@ -186,7 +233,7 @@ function logPlayAttempt (result) {
return console.error('Unknown play attempt result', result) return console.error('Unknown play attempt result', result)
} }
var attempts = telemetry.playAttempts const attempts = telemetry.playAttempts
attempts.total = (attempts.total || 0) + 1 attempts.total = (attempts.total || 0) + 1
attempts[result] = (attempts[result] || 0) + 1 attempts[result] = (attempts[result] || 0) + 1
} }

View File

@@ -3,11 +3,10 @@ module.exports = {
isVideo, isVideo,
isAudio, isAudio,
isTorrent, isTorrent,
isPlayableTorrentSummary, isPlayableTorrentSummary
pickFileToPlay
} }
var path = require('path') const path = require('path')
// Checks whether a fileSummary or file path is audio/video that we can play, // Checks whether a fileSummary or file path is audio/video that we can play,
// based on the file extension // based on the file extension
@@ -47,38 +46,16 @@ function isAudio (file) {
// - a file object where obj.name is ends in .torrent // - a file object where obj.name is ends in .torrent
// - a string that's a magnet link (magnet://...) // - a string that's a magnet link (magnet://...)
function isTorrent (file) { function isTorrent (file) {
var isTorrentFile = getFileExtension(file) === '.torrent' const isTorrentFile = getFileExtension(file) === '.torrent'
var isMagnet = typeof file === 'string' && /^(stream-)?magnet:/.test(file) const isMagnet = typeof file === 'string' && /^(stream-)?magnet:/.test(file)
return isTorrentFile || isMagnet return isTorrentFile || isMagnet
} }
function getFileExtension (file) { function getFileExtension (file) {
var name = typeof file === 'string' ? file : file.name const name = typeof file === 'string' ? file : file.name
return path.extname(name).toLowerCase() return path.extname(name).toLowerCase()
} }
function isPlayableTorrentSummary (torrentSummary) { function isPlayableTorrentSummary (torrentSummary) {
return torrentSummary.files && torrentSummary.files.some(isPlayable) return torrentSummary.files && torrentSummary.files.some(isPlayable)
} }
// Picks the default file to play from a list of torrent or torrentSummary files
// Returns an index or undefined, if no files are playable
function pickFileToPlay (files) {
// first, try to find the biggest video file
var videoFiles = files.filter(isVideo)
if (videoFiles.length > 0) {
var largestVideoFile = videoFiles.reduce(function (a, b) {
return a.length > b.length ? a : b
})
return files.indexOf(largestVideoFile)
}
// if there are no videos, play the first audio file
var audioFiles = files.filter(isAudio)
if (audioFiles.length > 0) {
return files.indexOf(audioFiles[0])
}
// no video or audio means nothing is playable
return undefined
}

View File

@@ -1,22 +1,22 @@
module.exports = torrentPoster module.exports = torrentPoster
var captureVideoFrame = require('./capture-video-frame') const captureVideoFrame = require('./capture-video-frame')
var path = require('path') const path = require('path')
function torrentPoster (torrent, cb) { function torrentPoster (torrent, cb) {
// First, try to use a poster image if available // First, try to use a poster image if available
var posterFile = torrent.files.filter(function (file) { const posterFile = torrent.files.filter(function (file) {
return /^poster\.(jpg|png|gif)$/.test(file.name) return /^poster\.(jpg|png|gif)$/.test(file.name)
})[0] })[0]
if (posterFile) return torrentPosterFromImage(posterFile, torrent, cb) if (posterFile) return torrentPosterFromImage(posterFile, torrent, cb)
// Second, try to use the largest video file // Second, try to use the largest video file
// Filter out file formats that the <video> tag definitely can't play // Filter out file formats that the <video> tag definitely can't play
var videoFile = getLargestFileByExtension(torrent, ['.mp4', '.m4v', '.webm', '.mov', '.mkv']) const videoFile = getLargestFileByExtension(torrent, ['.mp4', '.m4v', '.webm', '.mov', '.mkv'])
if (videoFile) return torrentPosterFromVideo(videoFile, torrent, cb) if (videoFile) return torrentPosterFromVideo(videoFile, torrent, cb)
// Third, try to use the largest image file // Third, try to use the largest image file
var imgFile = getLargestFileByExtension(torrent, ['.gif', '.jpg', '.jpeg', '.png']) const imgFile = getLargestFileByExtension(torrent, ['.gif', '.jpg', '.jpeg', '.png'])
if (imgFile) return torrentPosterFromImage(imgFile, torrent, cb) if (imgFile) return torrentPosterFromImage(imgFile, torrent, cb)
// TODO: generate a waveform from the largest sound file // TODO: generate a waveform from the largest sound file
@@ -25,8 +25,8 @@ function torrentPoster (torrent, cb) {
} }
function getLargestFileByExtension (torrent, extensions) { function getLargestFileByExtension (torrent, extensions) {
var files = torrent.files.filter(function (file) { const files = torrent.files.filter(function (file) {
var extname = path.extname(file.name).toLowerCase() const extname = path.extname(file.name).toLowerCase()
return extensions.indexOf(extname) !== -1 return extensions.indexOf(extname) !== -1
}) })
if (files.length === 0) return undefined if (files.length === 0) return undefined
@@ -36,15 +36,15 @@ function getLargestFileByExtension (torrent, extensions) {
} }
function torrentPosterFromVideo (file, torrent, cb) { function torrentPosterFromVideo (file, torrent, cb) {
var index = torrent.files.indexOf(file) const index = torrent.files.indexOf(file)
var server = torrent.createServer(0) const server = torrent.createServer(0)
server.listen(0, onListening) server.listen(0, onListening)
function onListening () { function onListening () {
var port = server.address().port const port = server.address().port
var url = 'http://localhost:' + port + '/' + index const url = 'http://localhost:' + port + '/' + index
var video = document.createElement('video') const video = document.createElement('video')
video.addEventListener('canplay', onCanPlay) video.addEventListener('canplay', onCanPlay)
video.volume = 0 video.volume = 0
@@ -61,7 +61,7 @@ function torrentPosterFromVideo (file, torrent, cb) {
function onSeeked () { function onSeeked () {
video.removeEventListener('seeked', onSeeked) video.removeEventListener('seeked', onSeeked)
var buf = captureVideoFrame(video) const buf = captureVideoFrame(video)
// unload video element // unload video element
video.pause() video.pause()
@@ -78,6 +78,6 @@ function torrentPosterFromVideo (file, torrent, cb) {
} }
function torrentPosterFromImage (file, torrent, cb) { function torrentPosterFromImage (file, torrent, cb) {
var extname = path.extname(file.name) const extname = path.extname(file.name)
file.getBuffer((err, buf) => cb(err, buf, extname)) file.getBuffer((err, buf) => cb(err, buf, extname))
} }

View File

@@ -6,8 +6,8 @@ module.exports = {
getFileOrFolder getFileOrFolder
} }
var path = require('path') const path = require('path')
var config = require('../../config') const config = require('../../config')
// Expects a torrentSummary // Expects a torrentSummary
// Returns an absolute path to the torrent file, or null if unavailable // Returns an absolute path to the torrent file, or null if unavailable
@@ -20,7 +20,7 @@ function getTorrentPath (torrentSummary) {
// Returns an absolute path to the poster image, or null if unavailable // Returns an absolute path to the poster image, or null if unavailable
function getPosterPath (torrentSummary) { function getPosterPath (torrentSummary) {
if (!torrentSummary || !torrentSummary.posterFileName) return null if (!torrentSummary || !torrentSummary.posterFileName) return null
var posterPath = path.join(config.POSTER_PATH, torrentSummary.posterFileName) const posterPath = path.join(config.POSTER_PATH, torrentSummary.posterFileName)
// Work around a Chrome bug (reproduced in vanilla Chrome, not just Electron): // Work around a Chrome bug (reproduced in vanilla Chrome, not just Electron):
// Backslashes in URLS in CSS cause bizarre string encoding issues // Backslashes in URLS in CSS cause bizarre string encoding issues
return posterPath.replace(/\\/g, '/') return posterPath.replace(/\\/g, '/')
@@ -29,7 +29,7 @@ function getPosterPath (torrentSummary) {
// Expects a torrentSummary // Expects a torrentSummary
// Returns a torrentID: filename, magnet URI, or infohash // Returns a torrentID: filename, magnet URI, or infohash
function getTorrentID (torrentSummary) { function getTorrentID (torrentSummary) {
var s = torrentSummary const s = torrentSummary
if (s.torrentFileName) { // Load torrent file from disk if (s.torrentFileName) { // Load torrent file from disk
return getTorrentPath(s) return getTorrentPath(s)
} else { // Load torrent from DHT } else { // Load torrent from DHT
@@ -51,7 +51,7 @@ function getByKey (state, torrentKey) {
// TODO: make this assumption explicit, enforce it in the `create-torrent` // TODO: make this assumption explicit, enforce it in the `create-torrent`
// module. Store root folder explicitly to avoid hacky path processing below. // module. Store root folder explicitly to avoid hacky path processing below.
function getFileOrFolder (torrentSummary) { function getFileOrFolder (torrentSummary) {
var ts = torrentSummary const ts = torrentSummary
if (!ts.path || !ts.files || ts.files.length === 0) return null if (!ts.path || !ts.files || ts.files.length === 0) return null
return path.join(ts.path, ts.files[0].path.split('/')[0]) return path.join(ts.path, ts.files[0].path.split('/')[0])
} }

View File

@@ -18,7 +18,7 @@ const TorrentPlayer = require('./lib/torrent-player')
// Required by Material UI -- adds `onTouchTap` event // Required by Material UI -- adds `onTouchTap` event
require('react-tap-event-plugin')() require('react-tap-event-plugin')()
const App = require('./pages/App') const App = require('./pages/app')
const MediaController = require('./controllers/media-controller') const MediaController = require('./controllers/media-controller')
const UpdateController = require('./controllers/update-controller') const UpdateController = require('./controllers/update-controller')
@@ -28,27 +28,27 @@ const SubtitlesController = require('./controllers/subtitles-controller')
const TorrentListController = require('./controllers/torrent-list-controller') const TorrentListController = require('./controllers/torrent-list-controller')
const TorrentController = require('./controllers/torrent-controller') const TorrentController = require('./controllers/torrent-controller')
// Electron apps have two processes: a main process (node) runs first and starts
// a renderer process (essentially a Chrome window). We're in the renderer process,
// and this IPC channel receives from and sends messages to the main process
const ipcRenderer = electron.ipcRenderer
// Yo-yo pattern: state object lives here and percolates down thru all the views. // Yo-yo pattern: state object lives here and percolates down thru all the views.
// Events come back up from the views via dispatch(...) // Events come back up from the views via dispatch(...)
require('./lib/dispatcher').setDispatch(dispatch) require('./lib/dispatcher').setDispatch(dispatch)
// From dispatch(...), events are sent to one of the controllers // From dispatch(...), events are sent to one of the controllers
var controllers = null let controllers = null
// This dependency is the slowest-loading, so we lazy load it // This dependency is the slowest-loading, so we lazy load it
var Cast = null let Cast = null
// Electron apps have two processes: a main process (node) runs first and starts
// a renderer process (essentially a Chrome window). We're in the renderer process,
// and this IPC channel receives from and sends messages to the main process
var ipcRenderer = electron.ipcRenderer
// All state lives in state.js. `state.saved` is read from and written to a file. // All state lives in state.js. `state.saved` is read from and written to a file.
// All other state is ephemeral. First we load state.saved then initialize the app. // All other state is ephemeral. First we load state.saved then initialize the app.
var state let state
// Root React component // Root React component
var app let app
State.load(onState) State.load(onState)
@@ -58,6 +58,14 @@ State.load(onState)
function onState (err, _state) { function onState (err, _state) {
if (err) return onError(err) if (err) return onError(err)
state = window.state = _state // Make available for easier debugging state = window.state = _state // Make available for easier debugging
window.dispatch = dispatch
telemetry.init(state)
// Log uncaught JS errors
window.addEventListener('error',
(e) => telemetry.logUncaughtError('window', e),
true /* capture */)
// Create controllers // Create controllers
controllers = { controllers = {
@@ -114,13 +122,10 @@ function onState (err, _state) {
// ...window visibility state. // ...window visibility state.
document.addEventListener('webkitvisibilitychange', onVisibilityChange) document.addEventListener('webkitvisibilitychange', onVisibilityChange)
// Log uncaught JS errors
window.addEventListener('error',
(e) => telemetry.logUncaughtError('window', e),
true /* capture */)
// Done! Ideally we want to get here < 500ms after the user clicks the app // Done! Ideally we want to get here < 500ms after the user clicks the app
sound.play('STARTUP') if (electron.remote.getCurrentWindow().isVisible()) {
sound.play('STARTUP')
}
console.timeEnd('init') console.timeEnd('init')
} }
@@ -128,7 +133,6 @@ function onState (err, _state) {
function delayedInit () { function delayedInit () {
lazyLoadCast() lazyLoadCast()
sound.preload() sound.preload()
telemetry.init(state)
} }
// Lazily loads Chromecast and Airplay support // Lazily loads Chromecast and Airplay support
@@ -178,16 +182,24 @@ const dispatchHandlers = {
'showCreateTorrent': (paths) => controllers.torrentList.showCreateTorrent(paths), 'showCreateTorrent': (paths) => controllers.torrentList.showCreateTorrent(paths),
'createTorrent': (options) => controllers.torrentList.createTorrent(options), 'createTorrent': (options) => controllers.torrentList.createTorrent(options),
'toggleTorrent': (infoHash) => controllers.torrentList.toggleTorrent(infoHash), 'toggleTorrent': (infoHash) => controllers.torrentList.toggleTorrent(infoHash),
'toggleTorrentFile': (infoHash, index) => controllers.torrentList.toggleTorrentFile(infoHash, index), 'toggleTorrentFile': (infoHash, index) =>
'confirmDeleteTorrent': (infoHash, deleteData) => controllers.torrentList.confirmDeleteTorrent(infoHash, deleteData), controllers.torrentList.toggleTorrentFile(infoHash, index),
'deleteTorrent': (infoHash, deleteData) => controllers.torrentList.deleteTorrent(infoHash, deleteData), 'confirmDeleteTorrent': (infoHash, deleteData) =>
'toggleSelectTorrent': (infoHash) => controllers.torrentList.toggleSelectTorrent(infoHash), controllers.torrentList.confirmDeleteTorrent(infoHash, deleteData),
'openTorrentContextMenu': (infoHash) => controllers.torrentList.openTorrentContextMenu(infoHash), 'deleteTorrent': (infoHash, deleteData) =>
'startTorrentingSummary': (torrentKey) => controllers.torrentList.startTorrentingSummary(torrentKey), controllers.torrentList.deleteTorrent(infoHash, deleteData),
'toggleSelectTorrent': (infoHash) =>
controllers.torrentList.toggleSelectTorrent(infoHash),
'openTorrentContextMenu': (infoHash) =>
controllers.torrentList.openTorrentContextMenu(infoHash),
'startTorrentingSummary': (torrentKey) =>
controllers.torrentList.startTorrentingSummary(torrentKey),
// Playback // Playback
'playFile': (infoHash, index) => controllers.playback.playFile(infoHash, index), 'playFile': (infoHash, index) => controllers.playback.playFile(infoHash, index),
'playPause': () => controllers.playback.playPause(), 'playPause': () => controllers.playback.playPause(),
'nextTrack': () => controllers.playback.nextTrack(),
'previousTrack': () => controllers.playback.previousTrack(),
'skip': (time) => controllers.playback.skip(time), 'skip': (time) => controllers.playback.skip(time),
'skipTo': (time) => controllers.playback.skipTo(time), 'skipTo': (time) => controllers.playback.skipTo(time),
'changePlaybackRate': (dir) => controllers.playback.changePlaybackRate(dir), 'changePlaybackRate': (dir) => controllers.playback.changePlaybackRate(dir),
@@ -255,7 +267,7 @@ function dispatch (action, ...args) {
console.log('dispatch: %s %o', action, args) console.log('dispatch: %s %o', action, args)
} }
var handler = dispatchHandlers[action] const handler = dispatchHandlers[action]
if (handler) handler(...args) if (handler) handler(...args)
else console.error('Missing dispatch handler: ' + action) else console.error('Missing dispatch handler: ' + action)
@@ -275,7 +287,7 @@ function setupIpc () {
ipcRenderer.on('fullscreenChanged', onFullscreenChanged) ipcRenderer.on('fullscreenChanged', onFullscreenChanged)
var tc = controllers.torrent const tc = controllers.torrent
ipcRenderer.on('wt-infohash', (e, ...args) => tc.torrentInfoHash(...args)) ipcRenderer.on('wt-infohash', (e, ...args) => tc.torrentInfoHash(...args))
ipcRenderer.on('wt-metadata', (e, ...args) => tc.torrentMetadata(...args)) ipcRenderer.on('wt-metadata', (e, ...args) => tc.torrentMetadata(...args))
ipcRenderer.on('wt-done', (e, ...args) => tc.torrentDone(...args)) ipcRenderer.on('wt-done', (e, ...args) => tc.torrentDone(...args))
@@ -302,7 +314,7 @@ function backToList () {
state.modal = null state.modal = null
state.location.backToFirst(function () { state.location.backToFirst(function () {
// If we were already on the torrent list, scroll to the top // If we were already on the torrent list, scroll to the top
var contentTag = document.querySelector('.content') const contentTag = document.querySelector('.content')
if (contentTag) contentTag.scrollTop = 0 if (contentTag) contentTag.scrollTop = 0
}) })
} }
@@ -349,18 +361,18 @@ function setDimensions (dimensions) {
state.window.wasMaximized = electron.remote.getCurrentWindow().isMaximized state.window.wasMaximized = electron.remote.getCurrentWindow().isMaximized
// Limit window size to screen size // Limit window size to screen size
var screenWidth = window.screen.width const screenWidth = window.screen.width
var screenHeight = window.screen.height const screenHeight = window.screen.height
var aspectRatio = dimensions.width / dimensions.height const aspectRatio = dimensions.width / dimensions.height
var scaleFactor = Math.min( const scaleFactor = Math.min(
Math.min(screenWidth / dimensions.width, 1), Math.min(screenWidth / dimensions.width, 1),
Math.min(screenHeight / dimensions.height, 1) Math.min(screenHeight / dimensions.height, 1)
) )
var width = Math.max( const width = Math.max(
Math.floor(dimensions.width * scaleFactor), Math.floor(dimensions.width * scaleFactor),
config.WINDOW_MIN_WIDTH config.WINDOW_MIN_WIDTH
) )
var height = Math.max( const height = Math.max(
Math.floor(dimensions.height * scaleFactor), Math.floor(dimensions.height * scaleFactor),
config.WINDOW_MIN_HEIGHT config.WINDOW_MIN_HEIGHT
) )
@@ -375,9 +387,9 @@ function setDimensions (dimensions) {
function onOpen (files) { function onOpen (files) {
if (!Array.isArray(files)) files = [ files ] if (!Array.isArray(files)) files = [ files ]
var url = state.location.url() const url = state.location.url()
var allTorrents = files.every(TorrentPlayer.isTorrent) const allTorrents = files.every(TorrentPlayer.isTorrent)
var allSubtitles = files.every(controllers.subtitles.isSubtitle) const allSubtitles = files.every(controllers.subtitles.isSubtitle)
if (allTorrents) { if (allTorrents) {
// Drop torrents onto the app: go to home screen, add torrents, no matter what // Drop torrents onto the app: go to home screen, add torrents, no matter what
@@ -413,7 +425,7 @@ function onError (err) {
function onPaste (e) { function onPaste (e) {
if (e.target.tagName.toLowerCase() === 'input') return if (e.target.tagName.toLowerCase() === 'input') return
var torrentIds = electron.clipboard.readText().split('\n') const torrentIds = electron.clipboard.readText().split('\n')
torrentIds.forEach(function (torrentId) { torrentIds.forEach(function (torrentId) {
torrentId = torrentId.trim() torrentId = torrentId.trim()
if (torrentId.length === 0) return if (torrentId.length === 0) return

View File

@@ -2,16 +2,17 @@ const colors = require('material-ui/styles/colors')
const React = require('react') const React = require('react')
const darkBaseTheme = require('material-ui/styles/baseThemes/darkBaseTheme').default const darkBaseTheme = require('material-ui/styles/baseThemes/darkBaseTheme').default
const lightBaseTheme = require('material-ui/styles/baseThemes/lightBaseTheme').default
const getMuiTheme = require('material-ui/styles/getMuiTheme').default const getMuiTheme = require('material-ui/styles/getMuiTheme').default
const MuiThemeProvider = require('material-ui/styles/MuiThemeProvider').default const MuiThemeProvider = require('material-ui/styles/MuiThemeProvider').default
const Header = require('../components/header') const Header = require('../components/header')
const Views = { const Views = {
'home': require('./TorrentListPage'), 'home': require('./torrent-list-page'),
'player': require('./PlayerPage'), 'player': require('./player-page'),
'create-torrent': require('./CreateTorrentPage'), 'create-torrent': require('./create-torrent-page'),
'preferences': require('./PreferencesPage') 'preferences': require('./preferences-page')
} }
const Modals = { const Modals = {
@@ -21,9 +22,11 @@ const Modals = {
'unsupported-media-modal': require('../components/unsupported-media-modal') 'unsupported-media-modal': require('../components/unsupported-media-modal')
} }
darkBaseTheme.fontFamily = process.platform === 'win32' const fontFamily = process.platform === 'win32'
? '"Segoe UI", sans-serif' ? '"Segoe UI", sans-serif'
: 'BlinkMacSystemFont, "Helvetica Neue", Helvetica, sans-serif' : 'BlinkMacSystemFont, "Helvetica Neue", Helvetica, sans-serif'
lightBaseTheme.fontFamily = fontFamily
darkBaseTheme.fontFamily = fontFamily
darkBaseTheme.palette.primary1Color = colors.cyan500 darkBaseTheme.palette.primary1Color = colors.cyan500
darkBaseTheme.palette.primary2Color = colors.cyan500 darkBaseTheme.palette.primary2Color = colors.cyan500
darkBaseTheme.palette.primary3Color = colors.grey600 darkBaseTheme.palette.primary3Color = colors.grey600
@@ -33,21 +36,21 @@ darkBaseTheme.palette.accent3Color = colors.redA100
class App extends React.Component { class App extends React.Component {
render () { render () {
var state = this.props.state const state = this.props.state
// Hide player controls while playing video, if the mouse stays still for a while // Hide player controls while playing video, if the mouse stays still for a while
// Never hide the controls when: // Never hide the controls when:
// * The mouse is over the controls or we're scrubbing (see CSS) // * The mouse is over the controls or we're scrubbing (see CSS)
// * The video is paused // * The video is paused
// * The video is playing remotely on Chromecast or Airplay // * The video is playing remotely on Chromecast or Airplay
var hideControls = state.location.url() === 'player' && const hideControls = state.location.url() === 'player' &&
state.playing.mouseStationarySince !== 0 && state.playing.mouseStationarySince !== 0 &&
new Date().getTime() - state.playing.mouseStationarySince > 2000 && new Date().getTime() - state.playing.mouseStationarySince > 2000 &&
!state.playing.isPaused && !state.playing.isPaused &&
state.playing.location === 'local' && state.playing.location === 'local' &&
state.playing.playbackRate === 1 state.playing.playbackRate === 1
var cls = [ const cls = [
'view-' + state.location.url(), /* e.g. view-home, view-player */ 'view-' + state.location.url(), /* e.g. view-home, view-player */
'is-' + process.platform /* e.g. is-darwin, is-win32, is-linux */ 'is-' + process.platform /* e.g. is-darwin, is-win32, is-linux */
] ]
@@ -55,7 +58,7 @@ class App extends React.Component {
if (state.window.isFocused) cls.push('is-focused') if (state.window.isFocused) cls.push('is-focused')
if (hideControls) cls.push('hide-video-controls') if (hideControls) cls.push('hide-video-controls')
var vdom = ( const vdom = (
<MuiThemeProvider muiTheme={getMuiTheme(darkBaseTheme)}> <MuiThemeProvider muiTheme={getMuiTheme(darkBaseTheme)}>
<div className={'app ' + cls.join(' ')}> <div className={'app ' + cls.join(' ')}>
<Header state={state} /> <Header state={state} />
@@ -70,12 +73,12 @@ class App extends React.Component {
} }
getErrorPopover () { getErrorPopover () {
var state = this.props.state const state = this.props.state
var now = new Date().getTime() const now = new Date().getTime()
var recentErrors = state.errors.filter((x) => now - x.time < 5000) const recentErrors = state.errors.filter((x) => now - x.time < 5000)
var hasErrors = recentErrors.length > 0 const hasErrors = recentErrors.length > 0
var errorElems = recentErrors.map(function (error, i) { const errorElems = recentErrors.map(function (error, i) {
return (<div key={i} className='error'>{error.message}</div>) return (<div key={i} className='error'>{error.message}</div>)
}) })
return ( return (
@@ -88,22 +91,24 @@ class App extends React.Component {
} }
getModal () { getModal () {
var state = this.props.state const state = this.props.state
if (!state.modal) return if (!state.modal) return
var ModalContents = Modals[state.modal.id] const ModalContents = Modals[state.modal.id]
return ( return (
<div key='modal' className='modal'> <MuiThemeProvider muiTheme={getMuiTheme(lightBaseTheme)}>
<div key='modal-background' className='modal-background' /> <div key='modal' className='modal'>
<div key='modal-content' className='modal-content'> <div key='modal-background' className='modal-background' />
<ModalContents state={state} /> <div key='modal-content' className='modal-content'>
<ModalContents state={state} />
</div>
</div> </div>
</div> </MuiThemeProvider>
) )
} }
getView () { getView () {
var state = this.props.state const state = this.props.state
var View = Views[state.location.url()] const View = Views[state.location.url()]
return (<View state={state} />) return (<View state={state} />)
} }
} }

View File

@@ -11,18 +11,20 @@ const TextField = require('material-ui/TextField').default
const Checkbox = require('material-ui/Checkbox').default const Checkbox = require('material-ui/Checkbox').default
const CreateTorrentErrorPage = require('../components/create-torrent-error-page') const CreateTorrentErrorPage = require('../components/create-torrent-error-page')
const Heading = require('../components/Heading') const Heading = require('../components/heading')
const ShowMore = require('../components/ShowMore') const ShowMore = require('../components/show-more')
// Shows a basic UI to create a torrent, from an already-selected file or folder.
// Includes a "Show Advanced..." button and more advanced UI.
class CreateTorrentPage extends React.Component { class CreateTorrentPage extends React.Component {
constructor (props) { constructor (props) {
super(props) super(props)
var state = this.props.state const state = this.props.state
var info = state.location.current() const info = state.location.current()
// First, extract the base folder that the files are all in // First, extract the base folder that the files are all in
var pathPrefix = info.folderPath let pathPrefix = info.folderPath
if (!pathPrefix) { if (!pathPrefix) {
pathPrefix = info.files.map((x) => x.path).reduce(findCommonPrefix) pathPrefix = info.files.map((x) => x.path).reduce(findCommonPrefix)
if (!pathPrefix.endsWith('/') && !pathPrefix.endsWith('\\')) { if (!pathPrefix.endsWith('/') && !pathPrefix.endsWith('\\')) {
@@ -31,14 +33,14 @@ class CreateTorrentPage extends React.Component {
} }
// Then, exclude .DS_Store and other dotfiles // Then, exclude .DS_Store and other dotfiles
var files = info.files const files = info.files
.filter((f) => !containsDots(f.path, pathPrefix)) .filter((f) => !containsDots(f.path, pathPrefix))
.map((f) => ({name: f.name, path: f.path, size: f.size})) .map((f) => ({name: f.name, path: f.path, size: f.size}))
if (files.length === 0) return (<CreateTorrentErrorPage state={state} />) if (files.length === 0) return (<CreateTorrentErrorPage state={state} />)
// Then, use the name of the base folder (or sole file, for a single file torrent) // Then, use the name of the base folder (or sole file, for a single file torrent)
// as the default name. Show all files relative to the base folder. // as the default name. Show all files relative to the base folder.
var defaultName, basePath let defaultName, basePath
if (files.length === 1) { if (files.length === 1) {
// Single file torrent: /a/b/foo.jpg -> torrent name 'foo.jpg', path '/a/b' // Single file torrent: /a/b/foo.jpg -> torrent name 'foo.jpg', path '/a/b'
defaultName = files[0].name defaultName = files[0].name
@@ -50,7 +52,7 @@ class CreateTorrentPage extends React.Component {
} }
// Default trackers // Default trackers
var trackers = createTorrent.announceList.join('\n') const trackers = createTorrent.announceList.join('\n')
this.state = { this.state = {
comment: '', comment: '',
@@ -70,14 +72,14 @@ class CreateTorrentPage extends React.Component {
} }
render () { render () {
var files = this.state.files const files = this.state.files
// Sanity check: show the number of files and total size // Sanity check: show the number of files and total size
var numFiles = files.length const numFiles = files.length
var totalBytes = files const totalBytes = files
.map((f) => f.size) .map((f) => f.size)
.reduce((a, b) => a + b, 0) .reduce((a, b) => a + b, 0)
var torrentInfo = `${numFiles} files, ${prettyBytes(totalBytes)}` const torrentInfo = `${numFiles} files, ${prettyBytes(totalBytes)}`
return ( return (
<div className='create-torrent'> <div className='create-torrent'>
@@ -92,33 +94,37 @@ class CreateTorrentPage extends React.Component {
marginBottom: 10 marginBottom: 10
}} }}
hideLabel='Hide advanced settings...' hideLabel='Hide advanced settings...'
showLabel='Show advanced settings...' > showLabel='Show advanced settings...'>
{this.renderAdvanced()} {this.renderAdvanced()}
</ShowMore> </ShowMore>
<div className='float-right'> <div className='float-right'>
<FlatButton <FlatButton
className='control'
label='Cancel' label='Cancel'
style={{ style={{
marginRight: 10 marginRight: 10
}} }}
onClick={dispatcher('cancel')} onClick={dispatcher('cancel')} />
/>
<RaisedButton <RaisedButton
className='control'
label='Create Torrent' label='Create Torrent'
primary primary
onClick={this.handleSubmit} onClick={this.handleSubmit} />
/>
</div> </div>
</div> </div>
) )
} }
// Renders everything after clicking Show Advanced...:
// * Is Private? (private torrents, not announced to DHT)
// * Announce list (trackers)
// * Comment
renderAdvanced () { renderAdvanced () {
// Create file list // Create file list
var maxFileElems = 100 const maxFileElems = 100
var files = this.state.files const files = this.state.files
var fileElems = files.slice(0, maxFileElems).map((file, i) => { const fileElems = files.slice(0, maxFileElems).map((file, i) => {
var relativePath = path.relative(this.state.pathPrefix, file.path) const relativePath = path.relative(this.state.pathPrefix, file.path)
return (<div key={i}>{relativePath}</div>) return (<div key={i}>{relativePath}</div>)
}) })
if (files.length > maxFileElems) { if (files.length > maxFileElems) {
@@ -126,15 +132,15 @@ class CreateTorrentPage extends React.Component {
} }
// Align the text fields // Align the text fields
var textFieldStyle = { width: '' } const textFieldStyle = { width: '' }
var textareaStyle = { margin: 0 } const textareaStyle = { margin: 0 }
return ( return (
<div key='advanced' className='create-torrent-advanced'> <div key='advanced' className='create-torrent-advanced'>
<div key='private' className='torrent-attribute'> <div key='private' className='torrent-attribute'>
<label>Private:</label> <label>Private:</label>
<Checkbox <Checkbox
className='torrent-is-private' className='torrent-is-private control'
style={{display: ''}} style={{display: ''}}
checked={this.state.isPrivate} checked={this.state.isPrivate}
onCheck={this.setIsPrivate} /> onCheck={this.setIsPrivate} />
@@ -142,7 +148,7 @@ class CreateTorrentPage extends React.Component {
<div key='trackers' className='torrent-attribute'> <div key='trackers' className='torrent-attribute'>
<label>Trackers:</label> <label>Trackers:</label>
<TextField <TextField
className='torrent-trackers' className='torrent-trackers control'
style={textFieldStyle} style={textFieldStyle}
textareaStyle={textareaStyle} textareaStyle={textareaStyle}
multiLine multiLine
@@ -154,7 +160,7 @@ class CreateTorrentPage extends React.Component {
<div key='comment' className='torrent-attribute'> <div key='comment' className='torrent-attribute'>
<label>Comment:</label> <label>Comment:</label>
<TextField <TextField
className='torrent-comment' className='torrent-comment control'
style={textFieldStyle} style={textFieldStyle}
textareaStyle={textareaStyle} textareaStyle={textareaStyle}
hintText='Optionally describe your torrent...' hintText='Optionally describe your torrent...'
@@ -174,11 +180,11 @@ class CreateTorrentPage extends React.Component {
} }
function handleSubmit () { function handleSubmit () {
var announceList = this.state.trackers const announceList = this.state.trackers
.split('\n') .split('\n')
.map((s) => s.trim()) .map((s) => s.trim())
.filter((s) => s !== '') .filter((s) => s !== '')
var options = { const options = {
// We can't let the user choose their own name if we want WebTorrent // We can't let the user choose their own name if we want WebTorrent
// to use the files in place rather than creating a new folder. // to use the files in place rather than creating a new folder.
name: this.state.defaultName, name: this.state.defaultName,
@@ -193,7 +199,8 @@ function handleSubmit () {
// Finds the longest common prefix // Finds the longest common prefix
function findCommonPrefix (a, b) { function findCommonPrefix (a, b) {
for (var i = 0; i < a.length && i < b.length; i++) { let i
for (i = 0; i < a.length && i < b.length; i++) {
if (a.charCodeAt(i) !== b.charCodeAt(i)) break if (a.charCodeAt(i) !== b.charCodeAt(i)) break
} }
if (i === a.length) return a if (i === a.length) return a
@@ -202,7 +209,7 @@ function findCommonPrefix (a, b) {
} }
function containsDots (path, pathPrefix) { function containsDots (path, pathPrefix) {
var suffix = path.substring(pathPrefix.length).replace(/\\/g, '/') const suffix = path.substring(pathPrefix.length).replace(/\\/g, '/')
return ('/' + suffix).includes('/.') return ('/' + suffix).includes('/.')
} }

View File

@@ -2,9 +2,9 @@ const React = require('react')
const Bitfield = require('bitfield') const Bitfield = require('bitfield')
const prettyBytes = require('prettier-bytes') const prettyBytes = require('prettier-bytes')
const zeroFill = require('zero-fill') const zeroFill = require('zero-fill')
const path = require('path')
const TorrentSummary = require('../lib/torrent-summary') const TorrentSummary = require('../lib/torrent-summary')
const Playlist = require('../lib/playlist')
const {dispatch, dispatcher} = require('../lib/dispatcher') const {dispatch, dispatcher} = require('../lib/dispatcher')
// Shows a streaming video player. Standard features + Chromecast + Airplay // Shows a streaming video player. Standard features + Chromecast + Airplay
@@ -12,9 +12,9 @@ module.exports = class Player extends React.Component {
render () { render () {
// Show the video as large as will fit in the window, play immediately // Show the video as large as will fit in the window, play immediately
// If the video is on Chromecast or Airplay, show a title screen instead // If the video is on Chromecast or Airplay, show a title screen instead
var state = this.props.state const state = this.props.state
var showVideo = state.playing.location === 'local' const showVideo = state.playing.location === 'local'
var showControls = state.playing.location !== 'external' const showControls = state.playing.location !== 'external'
return ( return (
<div <div
className='player' className='player'
@@ -38,7 +38,7 @@ function renderMedia (state) {
// Unfortunately, play/pause can't be done just by modifying HTML. // Unfortunately, play/pause can't be done just by modifying HTML.
// Instead, grab the DOM node and play/pause it if necessary // Instead, grab the DOM node and play/pause it if necessary
// Get the <video> or <audio> tag // Get the <video> or <audio> tag
var mediaElement = document.querySelector(state.playing.type) const mediaElement = document.querySelector(state.playing.type)
if (mediaElement !== null) { if (mediaElement !== null) {
if (state.playing.isPaused && !mediaElement.paused) { if (state.playing.isPaused && !mediaElement.paused) {
mediaElement.pause() mediaElement.pause()
@@ -66,20 +66,20 @@ function renderMedia (state) {
} }
// Switch to the newly added subtitle track, if available // Switch to the newly added subtitle track, if available
var tracks = mediaElement.textTracks || [] const tracks = mediaElement.textTracks || []
for (var j = 0; j < tracks.length; j++) { for (let j = 0; j < tracks.length; j++) {
var isSelectedTrack = j === state.playing.subtitles.selectedIndex const isSelectedTrack = j === state.playing.subtitles.selectedIndex
tracks[j].mode = isSelectedTrack ? 'showing' : 'hidden' tracks[j].mode = isSelectedTrack ? 'showing' : 'hidden'
} }
// Save video position // Save video position
var file = state.getPlayingFileSummary() const file = state.getPlayingFileSummary()
file.currentTime = state.playing.currentTime = mediaElement.currentTime file.currentTime = state.playing.currentTime = mediaElement.currentTime
file.duration = state.playing.duration = mediaElement.duration file.duration = state.playing.duration = mediaElement.duration
// Save selected subtitle // Save selected subtitle
if (state.playing.subtitles.selectedIndex !== -1) { if (state.playing.subtitles.selectedIndex !== -1) {
var index = state.playing.subtitles.selectedIndex const index = state.playing.subtitles.selectedIndex
file.selectedSubtitle = state.playing.subtitles.tracks[index].filePath file.selectedSubtitle = state.playing.subtitles.tracks[index].filePath
} else if (file.selectedSubtitle != null) { } else if (file.selectedSubtitle != null) {
delete file.selectedSubtitle delete file.selectedSubtitle
@@ -89,11 +89,11 @@ function renderMedia (state) {
} }
// Add subtitles to the <video> tag // Add subtitles to the <video> tag
var trackTags = [] const trackTags = []
if (state.playing.subtitles.selectedIndex >= 0) { if (state.playing.subtitles.selectedIndex >= 0) {
for (var i = 0; i < state.playing.subtitles.tracks.length; i++) { for (let i = 0; i < state.playing.subtitles.tracks.length; i++) {
var track = state.playing.subtitles.tracks[i] const track = state.playing.subtitles.tracks[i]
var isSelected = state.playing.subtitles.selectedIndex === i const isSelected = state.playing.subtitles.selectedIndex === i
trackTags.push( trackTags.push(
<track <track
key={i} key={i}
@@ -106,10 +106,10 @@ function renderMedia (state) {
} }
// Create the <audio> or <video> tag // Create the <audio> or <video> tag
var MediaTagName = state.playing.type const MediaTagName = state.playing.type
var mediaTag = ( const mediaTag = (
<MediaTagName <MediaTagName
src={state.server.localURL} src={Playlist.getCurrentLocalURL(state)}
onDoubleClick={dispatcher('toggleFullScreen')} onDoubleClick={dispatcher('toggleFullScreen')}
onLoadedMetadata={onLoadedMetadata} onLoadedMetadata={onLoadedMetadata}
onEnded={onEnded} onEnded={onEnded}
@@ -136,21 +136,25 @@ function renderMedia (state) {
// As soon as we know the video dimensions, resize the window // As soon as we know the video dimensions, resize the window
function onLoadedMetadata (e) { function onLoadedMetadata (e) {
if (state.playing.type !== 'video') return if (state.playing.type !== 'video') return
var video = e.target const video = e.target
var dimensions = { const dimensions = {
width: video.videoWidth, width: video.videoWidth,
height: video.videoHeight height: video.videoHeight
} }
dispatch('setDimensions', dimensions) dispatch('setDimensions', dimensions)
} }
// When the video completes, pause the video instead of looping
function onEnded (e) { function onEnded (e) {
state.playing.isPaused = true if (Playlist.hasNext(state)) {
dispatch('nextTrack')
} else {
// When the last video completes, pause the video instead of looping
state.playing.isPaused = true
}
} }
function onCanPlay (e) { function onCanPlay (e) {
var elem = e.target const elem = e.target
if (state.playing.type === 'video' && if (state.playing.type === 'video' &&
elem.webkitVideoDecodedByteCount === 0) { elem.webkitVideoDecodedByteCount === 0) {
dispatch('mediaError', 'Video codec unsupported') dispatch('mediaError', 'Video codec unsupported')
@@ -164,15 +168,15 @@ function renderMedia (state) {
} }
function renderOverlay (state) { function renderOverlay (state) {
var elems = [] const elems = []
var audioMetadataElem = renderAudioMetadata(state) const audioMetadataElem = renderAudioMetadata(state)
var spinnerElem = renderLoadingSpinner(state) const spinnerElem = renderLoadingSpinner(state)
if (audioMetadataElem) elems.push(audioMetadataElem) if (audioMetadataElem) elems.push(audioMetadataElem)
if (spinnerElem) elems.push(spinnerElem) if (spinnerElem) elems.push(spinnerElem)
// Video fills the window, centered with black bars if necessary // Video fills the window, centered with black bars if necessary
// Audio gets a static poster image and a summary of the file metadata. // Audio gets a static poster image and a summary of the file metadata.
var style let style
if (state.playing.type === 'audio') { if (state.playing.type === 'audio') {
style = { backgroundImage: cssBackgroundImagePoster(state) } style = { backgroundImage: cssBackgroundImagePoster(state) }
} else if (elems.length !== 0) { } else if (elems.length !== 0) {
@@ -190,27 +194,27 @@ function renderOverlay (state) {
} }
function renderAudioMetadata (state) { function renderAudioMetadata (state) {
var fileSummary = state.getPlayingFileSummary() const fileSummary = state.getPlayingFileSummary()
if (!fileSummary.audioInfo) return if (!fileSummary.audioInfo) return
var info = fileSummary.audioInfo const info = fileSummary.audioInfo
// Get audio track info // Get audio track info
var title = info.title let title = info.title
if (!title) { if (!title) {
title = fileSummary.name title = fileSummary.name
} }
var artist = info.artist && info.artist[0] let artist = info.artist && info.artist[0]
var album = info.album let album = info.album
if (album && info.year && !album.includes(info.year)) { if (album && info.year && !album.includes(info.year)) {
album += ' (' + info.year + ')' album += ' (' + info.year + ')'
} }
var track let track
if (info.track && info.track.no && info.track.of) { if (info.track && info.track.no && info.track.of) {
track = info.track.no + ' of ' + info.track.of track = info.track.no + ' of ' + info.track.of
} }
// Show a small info box in the middle of the screen with title/album/etc // Show a small info box in the middle of the screen with title/album/etc
var elems = [] const elems = []
if (artist) { if (artist) {
elems.push(( elems.push((
<div key='artist' className='audio-artist'> <div key='artist' className='audio-artist'>
@@ -234,7 +238,7 @@ function renderAudioMetadata (state) {
} }
// Align the title with the other info, if available. Otherwise, center title // Align the title with the other info, if available. Otherwise, center title
var emptyLabel = (<label />) const emptyLabel = (<label />)
elems.unshift(( elems.unshift((
<div key='title' className='audio-title'> <div key='title' className='audio-title'>
{elems.length ? emptyLabel : undefined}{title} {elems.length ? emptyLabel : undefined}{title}
@@ -246,14 +250,14 @@ function renderAudioMetadata (state) {
function renderLoadingSpinner (state) { function renderLoadingSpinner (state) {
if (state.playing.isPaused) return if (state.playing.isPaused) return
var isProbablyStalled = state.playing.isStalled || const isProbablyStalled = state.playing.isStalled ||
(new Date().getTime() - state.playing.lastTimeUpdate > 2000) (new Date().getTime() - state.playing.lastTimeUpdate > 2000)
if (!isProbablyStalled) return if (!isProbablyStalled) return
var prog = state.getPlayingTorrentSummary().progress || {} const prog = state.getPlayingTorrentSummary().progress || {}
var fileProgress = 0 let fileProgress = 0
if (prog.files) { if (prog.files) {
var file = prog.files[state.playing.fileIndex] const file = prog.files[state.playing.fileIndex]
fileProgress = Math.floor(100 * file.numPiecesPresent / file.numPieces) fileProgress = Math.floor(100 * file.numPiecesPresent / file.numPieces)
} }
@@ -270,7 +274,7 @@ function renderLoadingSpinner (state) {
} }
function renderCastScreen (state) { function renderCastScreen (state) {
var castIcon, castType, isCast let castIcon, castType, isCast
if (state.playing.location.startsWith('chromecast')) { if (state.playing.location.startsWith('chromecast')) {
castIcon = 'cast_connected' castIcon = 'cast_connected'
castType = 'Chromecast' castType = 'Chromecast'
@@ -284,11 +288,8 @@ function renderCastScreen (state) {
castType = 'DLNA' castType = 'DLNA'
isCast = true isCast = true
} else if (state.playing.location === 'external') { } else if (state.playing.location === 'external') {
// TODO: get the player name in a more reliable way
var playerPath = state.saved.prefs.externalPlayerPath
var playerName = playerPath ? path.basename(playerPath).split('.')[0] : 'VLC'
castIcon = 'tv' castIcon = 'tv'
castType = playerName castType = state.getExternalPlayerName()
isCast = false isCast = false
} else if (state.playing.location === 'error') { } else if (state.playing.location === 'error') {
castIcon = 'error_outline' castIcon = 'error_outline'
@@ -296,15 +297,15 @@ function renderCastScreen (state) {
isCast = false isCast = false
} }
var isStarting = state.playing.location.endsWith('-pending') const isStarting = state.playing.location.endsWith('-pending')
var castName = state.playing.castName const castName = state.playing.castName
var castStatus let castStatus
if (isCast && isStarting) castStatus = 'Connecting to ' + castName + '...' if (isCast && isStarting) castStatus = 'Connecting to ' + castName + '...'
else if (isCast && !isStarting) castStatus = 'Connected to ' + castName else if (isCast && !isStarting) castStatus = 'Connected to ' + castName
else castStatus = '' else castStatus = ''
// Show a nice title image, if possible // Show a nice title image, if possible
var style = { const style = {
backgroundImage: cssBackgroundImagePoster(state) backgroundImage: cssBackgroundImagePoster(state)
} }
@@ -322,12 +323,12 @@ function renderCastScreen (state) {
function renderCastOptions (state) { function renderCastOptions (state) {
if (!state.devices.castMenu) return if (!state.devices.castMenu) return
var {location, devices} = state.devices.castMenu const {location, devices} = state.devices.castMenu
var player = state.devices[location] const player = state.devices[location]
var items = devices.map(function (device, ix) { const items = devices.map(function (device, ix) {
var isSelected = player.device === device const isSelected = player.device === device
var name = device.name const name = device.name
return ( return (
<li key={ix} onClick={dispatcher('selectCastDevice', ix)}> <li key={ix} onClick={dispatcher('selectCastDevice', ix)}>
<i className='icon'>{isSelected ? 'radio_button_checked' : 'radio_button_unchecked'}</i> <i className='icon'>{isSelected ? 'radio_button_checked' : 'radio_button_unchecked'}</i>
@@ -344,11 +345,11 @@ function renderCastOptions (state) {
} }
function renderSubtitleOptions (state) { function renderSubtitleOptions (state) {
var subtitles = state.playing.subtitles const subtitles = state.playing.subtitles
if (!subtitles.tracks.length || !subtitles.showMenu) return if (!subtitles.tracks.length || !subtitles.showMenu) return
var items = subtitles.tracks.map(function (track, ix) { const items = subtitles.tracks.map(function (track, ix) {
var isSelected = state.playing.subtitles.selectedIndex === ix const isSelected = state.playing.subtitles.selectedIndex === ix
return ( return (
<li key={ix} onClick={dispatcher('selectSubtitle', ix)}> <li key={ix} onClick={dispatcher('selectSubtitle', ix)}>
<i className='icon'>{'radio_button_' + (isSelected ? 'checked' : 'unchecked')}</i> <i className='icon'>{'radio_button_' + (isSelected ? 'checked' : 'unchecked')}</i>
@@ -357,8 +358,8 @@ function renderSubtitleOptions (state) {
) )
}) })
var noneSelected = state.playing.subtitles.selectedIndex === -1 const noneSelected = state.playing.subtitles.selectedIndex === -1
var noneClass = 'radio_button_' + (noneSelected ? 'checked' : 'unchecked') const noneClass = 'radio_button_' + (noneSelected ? 'checked' : 'unchecked')
return ( return (
<ul key='subtitle-options' className='options-list'> <ul key='subtitle-options' className='options-list'>
{items} {items}
@@ -371,32 +372,39 @@ function renderSubtitleOptions (state) {
} }
function renderPlayerControls (state) { function renderPlayerControls (state) {
var positionPercent = 100 * state.playing.currentTime / state.playing.duration const positionPercent = 100 * state.playing.currentTime / state.playing.duration
var playbackCursorStyle = { left: 'calc(' + positionPercent + '% - 3px)' } const playbackCursorStyle = { left: 'calc(' + positionPercent + '% - 3px)' }
var captionsClass = state.playing.subtitles.tracks.length === 0 const captionsClass = state.playing.subtitles.tracks.length === 0
? 'disabled' ? 'disabled'
: state.playing.subtitles.selectedIndex >= 0 : state.playing.subtitles.selectedIndex >= 0
? 'active' ? 'active'
: '' : ''
const prevClass = Playlist.hasPrevious(state) ? '' : 'disabled'
const nextClass = Playlist.hasNext(state) ? '' : 'disabled'
var elements = [ const elements = [
<div key='playback-bar' className='playback-bar'> <div key='playback-bar' className='playback-bar'>
{renderLoadingBar(state)} {renderLoadingBar(state)}
<div <div
key='cursor' key='cursor'
className='playback-cursor' className='playback-cursor'
style={playbackCursorStyle} style={playbackCursorStyle} />
/>
<div <div
key='scrub-bar' key='scrub-bar'
className='scrub-bar' className='scrub-bar'
draggable='true' draggable='true'
onDragStart={handleDragStart} onDragStart={handleDragStart}
onClick={handleScrub} onClick={handleScrub}
onDrag={handleScrub} onDrag={handleScrub} />
/>
</div>, </div>,
<i
key='skip-previous'
className={'icon skip-previous float-left ' + prevClass}
onClick={dispatcher('previousTrack')}>
skip_previous
</i>,
<i <i
key='play' key='play'
className='icon play-pause float-left' className='icon play-pause float-left'
@@ -404,6 +412,13 @@ function renderPlayerControls (state) {
{state.playing.isPaused ? 'play_arrow' : 'pause'} {state.playing.isPaused ? 'play_arrow' : 'pause'}
</i>, </i>,
<i
key='skip-next'
className={'icon skip-next float-left ' + nextClass}
onClick={dispatcher('nextTrack')}>
skip_next
</i>,
<i <i
key='fullscreen' key='fullscreen'
className='icon fullscreen float-right' className='icon fullscreen float-right'
@@ -425,24 +440,24 @@ function renderPlayerControls (state) {
} }
// If we've detected a Chromecast or AppleTV, the user can play video there // If we've detected a Chromecast or AppleTV, the user can play video there
var castTypes = ['chromecast', 'airplay', 'dlna'] const castTypes = ['chromecast', 'airplay', 'dlna']
var isCastingAnywhere = castTypes.some( const isCastingAnywhere = castTypes.some(
(castType) => state.playing.location.startsWith(castType)) (castType) => state.playing.location.startsWith(castType))
// Add the cast buttons. Icons for each cast type, connected/disconnected: // Add the cast buttons. Icons for each cast type, connected/disconnected:
var buttonIcons = { const buttonIcons = {
'chromecast': {true: 'cast_connected', false: 'cast'}, 'chromecast': {true: 'cast_connected', false: 'cast'},
'airplay': {true: 'airplay', false: 'airplay'}, 'airplay': {true: 'airplay', false: 'airplay'},
'dlna': {true: 'tv', false: 'tv'} 'dlna': {true: 'tv', false: 'tv'}
} }
castTypes.forEach(function (castType) { castTypes.forEach(function (castType) {
// Do we show this button (eg. the Chromecast button) at all? // Do we show this button (eg. the Chromecast button) at all?
var isCasting = state.playing.location.startsWith(castType) const isCasting = state.playing.location.startsWith(castType)
var player = state.devices[castType] const player = state.devices[castType]
if ((!player || player.getDevices().length === 0) && !isCasting) return if ((!player || player.getDevices().length === 0) && !isCasting) return
// Show the button. Three options for eg the Chromecast button: // Show the button. Three options for eg the Chromecast button:
var buttonClass, buttonHandler let buttonClass, buttonHandler
if (isCasting) { if (isCasting) {
// Option 1: we are currently connected to Chromecast. Button stops the cast. // Option 1: we are currently connected to Chromecast. Button stops the cast.
buttonClass = 'active' buttonClass = 'active'
@@ -456,7 +471,7 @@ function renderPlayerControls (state) {
buttonClass = '' buttonClass = ''
buttonHandler = dispatcher('toggleCastMenu', castType) buttonHandler = dispatcher('toggleCastMenu', castType)
} }
var buttonIcon = buttonIcons[castType][isCasting] const buttonIcon = buttonIcons[castType][isCasting]
elements.push(( elements.push((
<i <i
@@ -469,13 +484,13 @@ function renderPlayerControls (state) {
}) })
// Render volume slider // Render volume slider
var volume = state.playing.volume const volume = state.playing.volume
var volumeIcon = 'volume_' + ( const volumeIcon = 'volume_' + (
volume === 0 ? 'off' volume === 0 ? 'off'
: volume < 0.3 ? 'mute' : volume < 0.3 ? 'mute'
: volume < 0.6 ? 'down' : volume < 0.6 ? 'down'
: 'up') : 'up')
var volumeStyle = { const volumeStyle = {
background: '-webkit-gradient(linear, left top, right top, ' + background: '-webkit-gradient(linear, left top, right top, ' +
'color-stop(' + (volume * 100) + '%, #eee), ' + 'color-stop(' + (volume * 100) + '%, #eee), ' +
'color-stop(' + (volume * 100) + '%, #727272))' 'color-stop(' + (volume * 100) + '%, #727272))'
@@ -495,14 +510,13 @@ function renderPlayerControls (state) {
type='range' min='0' max='1' step='0.05' type='range' min='0' max='1' step='0.05'
value={volume} value={volume}
onChange={handleVolumeScrub} onChange={handleVolumeScrub}
style={volumeStyle} style={volumeStyle} />
/>
</div> </div>
)) ))
// Show video playback progress // Show video playback progress
var currentTimeStr = formatTime(state.playing.currentTime) const currentTimeStr = formatTime(state.playing.currentTime)
var durationStr = formatTime(state.playing.duration) const durationStr = formatTime(state.playing.duration)
elements.push(( elements.push((
<span key='time' className='time float-left'> <span key='time' className='time float-left'>
{currentTimeStr} / {durationStr} {currentTimeStr} / {durationStr}
@@ -529,7 +543,7 @@ function renderPlayerControls (state) {
function handleDragStart (e) { function handleDragStart (e) {
// Prevent the cursor from changing, eg to a green + icon on Mac // Prevent the cursor from changing, eg to a green + icon on Mac
if (e.dataTransfer) { if (e.dataTransfer) {
var dt = e.dataTransfer const dt = e.dataTransfer
dt.effectAllowed = 'none' dt.effectAllowed = 'none'
} }
} }
@@ -538,9 +552,9 @@ function renderPlayerControls (state) {
function handleScrub (e) { function handleScrub (e) {
if (!e.clientX) return if (!e.clientX) return
dispatch('mediaMouseMoved') dispatch('mediaMouseMoved')
var windowWidth = document.querySelector('body').clientWidth const windowWidth = document.querySelector('body').clientWidth
var fraction = e.clientX / windowWidth const fraction = e.clientX / windowWidth
var position = fraction * state.playing.duration /* seconds */ const position = fraction * state.playing.duration /* seconds */
dispatch('skipTo', position) dispatch('skipTo', position)
} }
@@ -571,18 +585,18 @@ function renderPlayerControls (state) {
// Renders the loading bar. Shows which parts of the torrent are loaded, which // Renders the loading bar. Shows which parts of the torrent are loaded, which
// can be 'spongey' / non-contiguous // can be 'spongey' / non-contiguous
function renderLoadingBar (state) { function renderLoadingBar (state) {
var torrentSummary = state.getPlayingTorrentSummary() const torrentSummary = state.getPlayingTorrentSummary()
if (!torrentSummary.progress) { if (!torrentSummary.progress) {
return [] return []
} }
// Find all contiguous parts of the torrent which are loaded // Find all contiguous parts of the torrent which are loaded
var prog = torrentSummary.progress const prog = torrentSummary.progress
var fileProg = prog.files[state.playing.fileIndex] const fileProg = prog.files[state.playing.fileIndex]
var parts = [] const parts = []
var lastPiecePresent = false let lastPiecePresent = false
for (var i = fileProg.startPiece; i <= fileProg.endPiece; i++) { for (let i = fileProg.startPiece; i <= fileProg.endPiece; i++) {
var partPresent = Bitfield.prototype.get.call(prog.bitfield, i) const partPresent = Bitfield.prototype.get.call(prog.bitfield, i)
if (partPresent && !lastPiecePresent) { if (partPresent && !lastPiecePresent) {
parts.push({start: i - fileProg.startPiece, count: 1}) parts.push({start: i - fileProg.startPiece, count: 1})
} else if (partPresent) { } else if (partPresent) {
@@ -592,8 +606,8 @@ function renderLoadingBar (state) {
} }
// Output some bars to show which parts of the file are loaded // Output some bars to show which parts of the file are loaded
var loadingBarElems = parts.map(function (part, i) { const loadingBarElems = parts.map(function (part, i) {
var style = { const style = {
left: (100 * part.start / fileProg.numPieces) + '%', left: (100 * part.start / fileProg.numPieces) + '%',
width: (100 * part.count / fileProg.numPieces) + '%' width: (100 * part.count / fileProg.numPieces) + '%'
} }
@@ -605,8 +619,8 @@ function renderLoadingBar (state) {
// Returns the CSS background-image string for a poster image + dark vignette // Returns the CSS background-image string for a poster image + dark vignette
function cssBackgroundImagePoster (state) { function cssBackgroundImagePoster (state) {
var torrentSummary = state.getPlayingTorrentSummary() const torrentSummary = state.getPlayingTorrentSummary()
var posterPath = TorrentSummary.getPosterPath(torrentSummary) const posterPath = TorrentSummary.getPosterPath(torrentSummary)
if (!posterPath) return '' if (!posterPath) return ''
return cssBackgroundImageDarkGradient() + `, url('${posterPath}')` return cssBackgroundImageDarkGradient() + `, url('${posterPath}')`
} }
@@ -621,12 +635,12 @@ function formatTime (time) {
return '0:00' return '0:00'
} }
var hours = Math.floor(time / 3600) let hours = Math.floor(time / 3600)
var minutes = Math.floor(time % 3600 / 60) let minutes = Math.floor(time % 3600 / 60)
if (hours > 0) { if (hours > 0) {
minutes = zeroFill(2, minutes) minutes = zeroFill(2, minutes)
} }
var seconds = zeroFill(2, Math.floor(time % 60)) let seconds = zeroFill(2, Math.floor(time % 60))
return (hours > 0 ? hours + ':' : '') + minutes + ':' + seconds return (hours > 0 ? hours + ':' : '') + minutes + ':' + seconds
} }

View File

@@ -1,13 +1,14 @@
const colors = require('material-ui/styles/colors')
const path = require('path') const path = require('path')
const React = require('react') const React = require('react')
const colors = require('material-ui/styles/colors')
const Checkbox = require('material-ui/Checkbox').default const Checkbox = require('material-ui/Checkbox').default
const Heading = require('../components/Heading')
const PathSelector = require('../components/PathSelector')
const RaisedButton = require('material-ui/RaisedButton').default const RaisedButton = require('material-ui/RaisedButton').default
const Heading = require('../components/heading')
const PathSelector = require('../components/path-selector')
const {dispatch} = require('../lib/dispatcher') const {dispatch} = require('../lib/dispatcher')
const config = require('../../config')
class PreferencesPage extends React.Component { class PreferencesPage extends React.Component {
constructor (props) { constructor (props) {
@@ -21,6 +22,9 @@ class PreferencesPage extends React.Component {
this.handleExternalPlayerPathChange = this.handleExternalPlayerPathChange =
this.handleExternalPlayerPathChange.bind(this) this.handleExternalPlayerPathChange.bind(this)
this.handleStartupChange =
this.handleStartupChange.bind(this)
} }
downloadPathSelector () { downloadPathSelector () {
@@ -33,8 +37,7 @@ class PreferencesPage extends React.Component {
}} }}
onChange={this.handleDownloadPathChange} onChange={this.handleDownloadPathChange}
title='Download location' title='Download location'
value={this.props.state.unsaved.prefs.downloadPath} value={this.props.state.unsaved.prefs.downloadPath} />
/>
</Preference> </Preference>
) )
} }
@@ -50,8 +53,7 @@ class PreferencesPage extends React.Component {
className='control' className='control'
checked={!this.props.state.unsaved.prefs.openExternalPlayer} checked={!this.props.state.unsaved.prefs.openExternalPlayer}
label={'Play torrent media files using WebTorrent'} label={'Play torrent media files using WebTorrent'}
onCheck={this.handleOpenExternalPlayerChange} onCheck={this.handleOpenExternalPlayerChange} />
/>
</Preference> </Preference>
) )
} }
@@ -61,9 +63,8 @@ class PreferencesPage extends React.Component {
} }
externalPlayerPathSelector () { externalPlayerPathSelector () {
const playerName = path.basename( const playerPath = this.props.state.unsaved.prefs.externalPlayerPath
this.props.state.unsaved.prefs.externalPlayerPath || 'VLC' const playerName = this.props.state.getExternalPlayerName()
)
const description = this.props.state.unsaved.prefs.openExternalPlayer const description = this.props.state.unsaved.prefs.openExternalPlayer
? `Torrent media files will always play in ${playerName}.` ? `Torrent media files will always play in ${playerName}.`
@@ -81,22 +82,17 @@ class PreferencesPage extends React.Component {
displayValue={playerName} displayValue={playerName}
onChange={this.handleExternalPlayerPathChange} onChange={this.handleExternalPlayerPathChange}
title='External player' title='External player'
value={this.props.state.unsaved.prefs.externalPlayerPath} value={playerPath ? path.dirname(playerPath) : null} />
/>
</Preference> </Preference>
) )
} }
handleExternalPlayerPathChange (filePath) { handleExternalPlayerPathChange (filePath) {
if (path.extname(filePath) === '.app') {
// Mac: Use executable in packaged .app bundle
filePath += '/Contents/MacOS/' + path.basename(filePath, '.app')
}
dispatch('updatePreferences', 'externalPlayerPath', filePath) dispatch('updatePreferences', 'externalPlayerPath', filePath)
} }
setDefaultAppButton () { setDefaultAppButton () {
var isFileHandler = this.props.state.unsaved.prefs.isFileHandler const isFileHandler = this.props.state.unsaved.prefs.isFileHandler
if (isFileHandler) { if (isFileHandler) {
return ( return (
<Preference> <Preference>
@@ -110,24 +106,46 @@ class PreferencesPage extends React.Component {
<RaisedButton <RaisedButton
className='control' className='control'
onClick={this.handleSetDefaultApp} onClick={this.handleSetDefaultApp}
label='Make WebTorrent the default' label='Make WebTorrent the default' />
/>
</Preference> </Preference>
) )
} }
handleStartupChange (e, isChecked) {
dispatch('updatePreferences', 'startup', isChecked)
}
setStartupSection () {
if (config.IS_PORTABLE) {
return
}
return (
<PreferencesSection title='Startup'>
<Preference>
<Checkbox
className='control'
checked={this.props.state.unsaved.prefs.startup}
label={'Open WebTorrent on startup.'}
onCheck={this.handleStartupChange}
/>
</Preference>
</PreferencesSection>
)
}
handleSetDefaultApp () { handleSetDefaultApp () {
dispatch('updatePreferences', 'isFileHandler', true) dispatch('updatePreferences', 'isFileHandler', true)
} }
render () { render () {
var style = { const style = {
color: colors.grey400, color: colors.grey400,
marginLeft: 25, marginLeft: 25,
marginRight: 25 marginRight: 25
} }
return ( return (
<div style={style} > <div style={style}>
<PreferencesSection title='Downloads'> <PreferencesSection title='Downloads'>
{this.downloadPathSelector()} {this.downloadPathSelector()}
</PreferencesSection> </PreferencesSection>
@@ -138,6 +156,7 @@ class PreferencesPage extends React.Component {
<PreferencesSection title='Default torrent app'> <PreferencesSection title='Default torrent app'>
{this.setDefaultAppButton()} {this.setDefaultAppButton()}
</PreferencesSection> </PreferencesSection>
{this.setStartupSection()}
</div> </div>
) )
} }
@@ -151,7 +170,7 @@ class PreferencesSection extends React.Component {
} }
render () { render () {
var style = { const style = {
marginBottom: 25, marginBottom: 25,
marginTop: 25 marginTop: 25
} }
@@ -166,7 +185,7 @@ class PreferencesSection extends React.Component {
class Preference extends React.Component { class Preference extends React.Component {
render () { render () {
var style = { marginBottom: 10 } const style = { marginBottom: 10 }
return (<div style={style}>{this.props.children}</div>) return (<div style={style}>{this.props.children}</div>)
} }
} }

View File

@@ -4,12 +4,13 @@ const prettyBytes = require('prettier-bytes')
const TorrentSummary = require('../lib/torrent-summary') const TorrentSummary = require('../lib/torrent-summary')
const TorrentPlayer = require('../lib/torrent-player') const TorrentPlayer = require('../lib/torrent-player')
const {dispatcher} = require('../lib/dispatcher') const {dispatcher} = require('../lib/dispatcher')
const {InvalidTorrentError} = require('../lib/errors')
module.exports = class TorrentList extends React.Component { module.exports = class TorrentList extends React.Component {
render () { render () {
var state = this.props.state const state = this.props.state
var contents = [] const contents = []
if (state.downloadPathStatus === 'missing') { if (state.downloadPathStatus === 'missing') {
contents.push( contents.push(
<div key='torrent-missing-path'> <div key='torrent-missing-path'>
@@ -21,7 +22,7 @@ module.exports = class TorrentList extends React.Component {
</div> </div>
) )
} }
var torrentElems = state.saved.torrents.map( const torrentElems = state.saved.torrents.map(
(torrentSummary) => this.renderTorrent(torrentSummary) (torrentSummary) => this.renderTorrent(torrentSummary)
) )
contents.push(...torrentElems) contents.push(...torrentElems)
@@ -39,30 +40,29 @@ module.exports = class TorrentList extends React.Component {
} }
renderTorrent (torrentSummary) { renderTorrent (torrentSummary) {
var state = this.props.state const state = this.props.state
var infoHash = torrentSummary.infoHash const infoHash = torrentSummary.infoHash
var isSelected = infoHash && state.selectedInfoHash === infoHash const isSelected = infoHash && state.selectedInfoHash === infoHash
// Background image: show some nice visuals, like a frame from the movie, if possible // Background image: show some nice visuals, like a frame from the movie, if possible
var style = {} const style = {}
if (torrentSummary.posterFileName) { if (torrentSummary.posterFileName) {
var gradient = isSelected const gradient = 'linear-gradient(to bottom, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0) 100%)'
? 'linear-gradient(to bottom, rgba(0, 0, 0, 0.8) 0%, rgba(0, 0, 0, 0.4) 100%)' const posterPath = TorrentSummary.getPosterPath(torrentSummary)
: 'linear-gradient(to bottom, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0) 100%)' style.backgroundImage = `${gradient}, url('${posterPath}')`
var posterPath = TorrentSummary.getPosterPath(torrentSummary)
style.backgroundImage = gradient + `, url('${posterPath}')`
} }
// Foreground: name of the torrent, basic info like size, play button, // Foreground: name of the torrent, basic info like size, play button,
// cast buttons if available, and delete // cast buttons if available, and delete
var classes = ['torrent'] const classes = ['torrent']
// playStatus turns the play button into a loading spinner or error icon // playStatus turns the play button into a loading spinner or error icon
if (torrentSummary.playStatus) classes.push(torrentSummary.playStatus) if (torrentSummary.playStatus) classes.push(torrentSummary.playStatus)
if (isSelected) classes.push('selected') if (isSelected) classes.push('selected')
if (!infoHash) classes.push('disabled') if (!infoHash) classes.push('disabled')
if (!torrentSummary.torrentKey) throw new Error('Missing torrentKey') if (!torrentSummary.torrentKey) throw new InvalidTorrentError('Missing torrentKey')
return ( return (
<div <div
id={torrentSummary.testID && ('torrent-' + torrentSummary.testID)}
key={torrentSummary.torrentKey} key={torrentSummary.torrentKey}
style={style} style={style}
className={classes.join(' ')} className={classes.join(' ')}
@@ -77,13 +77,13 @@ module.exports = class TorrentList extends React.Component {
// Show name, download status, % complete // Show name, download status, % complete
renderTorrentMetadata (torrentSummary) { renderTorrentMetadata (torrentSummary) {
var name = torrentSummary.name || 'Loading torrent...' const name = torrentSummary.name || 'Loading torrent...'
var elements = [( const elements = [(
<div key='name' className='name ellipsis'>{name}</div> <div key='name' className='name ellipsis'>{name}</div>
)] )]
// If it's downloading/seeding then show progress info // If it's downloading/seeding then show progress info
var prog = torrentSummary.progress const prog = torrentSummary.progress
if (torrentSummary.error) { if (torrentSummary.error) {
elements.push( elements.push(
<div key='progress-info' className='ellipsis'> <div key='progress-info' className='ellipsis'>
@@ -93,6 +93,7 @@ module.exports = class TorrentList extends React.Component {
} else if (torrentSummary.status !== 'paused' && prog) { } else if (torrentSummary.status !== 'paused' && prog) {
elements.push( elements.push(
<div key='progress-info' className='ellipsis'> <div key='progress-info' className='ellipsis'>
{renderProgressBar()}
{renderPercentProgress()} {renderPercentProgress()}
{renderTotalProgress()} {renderTotalProgress()}
{renderPeers()} {renderPeers()}
@@ -105,14 +106,19 @@ module.exports = class TorrentList extends React.Component {
return (<div key='metadata' className='metadata'>{elements}</div>) return (<div key='metadata' className='metadata'>{elements}</div>)
function renderProgressBar () {
const progress = Math.floor(100 * prog.progress)
return (<progress value={progress} max='100'>{progress}%</progress>)
}
function renderPercentProgress () { function renderPercentProgress () {
var progress = Math.floor(100 * prog.progress) const progress = Math.floor(100 * prog.progress)
return (<span key='percent-progress'>{progress}%</span>) return (<span key='percent-progress'>{progress}%</span>)
} }
function renderTotalProgress () { function renderTotalProgress () {
var downloaded = prettyBytes(prog.downloaded) const downloaded = prettyBytes(prog.downloaded)
var total = prettyBytes(prog.length || 0) const total = prettyBytes(prog.length || 0)
if (downloaded === total) { if (downloaded === total) {
return (<span key='total-progress'>{downloaded}</span>) return (<span key='total-progress'>{downloaded}</span>)
} else { } else {
@@ -122,7 +128,7 @@ module.exports = class TorrentList extends React.Component {
function renderPeers () { function renderPeers () {
if (prog.numPeers === 0) return if (prog.numPeers === 0) return
var count = prog.numPeers === 1 ? 'peer' : 'peers' const count = prog.numPeers === 1 ? 'peer' : 'peers'
return (<span key='peers'>{prog.numPeers} {count}</span>) return (<span key='peers'>{prog.numPeers} {count}</span>)
} }
@@ -137,22 +143,22 @@ module.exports = class TorrentList extends React.Component {
} }
function renderEta () { function renderEta () {
var downloaded = prog.downloaded const downloaded = prog.downloaded
var total = prog.length || 0 const total = prog.length || 0
var missing = total - downloaded const missing = total - downloaded
var downloadSpeed = prog.downloadSpeed const downloadSpeed = prog.downloadSpeed
if (downloadSpeed === 0 || missing === 0) return if (downloadSpeed === 0 || missing === 0) return
var rawEta = missing / downloadSpeed const rawEta = missing / downloadSpeed
var hours = Math.floor(rawEta / 3600) % 24 const hours = Math.floor(rawEta / 3600) % 24
var minutes = Math.floor(rawEta / 60) % 60 const minutes = Math.floor(rawEta / 60) % 60
var seconds = Math.floor(rawEta % 60) const seconds = Math.floor(rawEta % 60)
// Only display hours and minutes if they are greater than 0 but always // Only display hours and minutes if they are greater than 0 but always
// display minutes if hours is being displayed // display minutes if hours is being displayed
var hoursStr = hours ? hours + 'h' : '' const hoursStr = hours ? hours + 'h' : ''
var minutesStr = (hours || minutes) ? minutes + 'm' : '' const minutesStr = (hours || minutes) ? minutes + 'm' : ''
var secondsStr = seconds + 's' const secondsStr = seconds + 's'
return (<span>ETA: {hoursStr} {minutesStr} {secondsStr}</span>) return (<span>ETA: {hoursStr} {minutesStr} {secondsStr}</span>)
} }
@@ -161,9 +167,9 @@ module.exports = class TorrentList extends React.Component {
// Download button toggles between torrenting (DL/seed) and paused // Download button toggles between torrenting (DL/seed) and paused
// Play button starts streaming the torrent immediately, unpausing if needed // Play button starts streaming the torrent immediately, unpausing if needed
renderTorrentButtons (torrentSummary) { renderTorrentButtons (torrentSummary) {
var infoHash = torrentSummary.infoHash const infoHash = torrentSummary.infoHash
var playIcon, playTooltip, playClass let playIcon, playTooltip, playClass
if (torrentSummary.playStatus === 'timeout') { if (torrentSummary.playStatus === 'timeout') {
playIcon = 'warning' playIcon = 'warning'
playTooltip = 'Playback timed out. No seeds? No internet? Click to try again.' playTooltip = 'Playback timed out. No seeds? No internet? Click to try again.'
@@ -172,7 +178,7 @@ module.exports = class TorrentList extends React.Component {
playTooltip = 'Start streaming' playTooltip = 'Start streaming'
} }
var downloadIcon, downloadTooltip let downloadIcon, downloadTooltip
if (torrentSummary.status === 'seeding') { if (torrentSummary.status === 'seeding') {
downloadIcon = 'file_upload' downloadIcon = 'file_upload'
downloadTooltip = 'Seeding. Click to stop.' downloadTooltip = 'Seeding. Click to stop.'
@@ -185,26 +191,25 @@ module.exports = class TorrentList extends React.Component {
} }
// Only show the play/dowload buttons for torrents that contain playable media // Only show the play/dowload buttons for torrents that contain playable media
var playButton, downloadButton, positionElem let playButton, downloadButton, positionElem
if (!torrentSummary.error) { if (!torrentSummary.error) {
downloadButton = ( downloadButton = (
<i <i
key='download-button' key='download-button'
className={'button-round icon download ' + torrentSummary.status} className={'button-round icon download ' + torrentSummary.status}
title={downloadTooltip} title={downloadTooltip}
onClick={dispatcher('toggleTorrent', infoHash)} onClick={dispatcher('toggleTorrent', infoHash)}>
>
{downloadIcon} {downloadIcon}
</i> </i>
) )
// Do we have a saved position? Show it using a radial progress bar on top // Do we have a saved position? Show it using a radial progress bar on top
// of the play button, unless already showing a spinner there: // of the play button, unless already showing a spinner there:
var willShowSpinner = torrentSummary.playStatus === 'requested' const willShowSpinner = torrentSummary.playStatus === 'requested'
var defaultFile = torrentSummary.files && const mostRecentFile = torrentSummary.files &&
torrentSummary.files[torrentSummary.defaultPlayFileIndex] torrentSummary.files[torrentSummary.mostRecentFileIndex]
if (defaultFile && defaultFile.currentTime && !willShowSpinner) { if (mostRecentFile && mostRecentFile.currentTime && !willShowSpinner) {
var fraction = defaultFile.currentTime / defaultFile.duration const fraction = mostRecentFile.currentTime / mostRecentFile.duration
positionElem = this.renderRadialProgressBar(fraction, 'radial-progress-large') positionElem = this.renderRadialProgressBar(fraction, 'radial-progress-large')
playClass = 'resume-position' playClass = 'resume-position'
} }
@@ -215,8 +220,7 @@ module.exports = class TorrentList extends React.Component {
key='play-button' key='play-button'
title={playTooltip} title={playTooltip}
className={'button-round icon play ' + playClass} className={'button-round icon play ' + playClass}
onClick={dispatcher('playFile', infoHash)} onClick={dispatcher('playFile', infoHash)}>
>
{playIcon} {playIcon}
</i> </i>
) )
@@ -241,9 +245,9 @@ module.exports = class TorrentList extends React.Component {
// Show files, per-file download status and play buttons, and so on // Show files, per-file download status and play buttons, and so on
renderTorrentDetails (torrentSummary) { renderTorrentDetails (torrentSummary) {
var filesElement let filesElement
if (torrentSummary.error || !torrentSummary.files) { if (torrentSummary.error || !torrentSummary.files) {
var message = '' let message = ''
if (torrentSummary.error === 'path-missing') { if (torrentSummary.error === 'path-missing') {
// Special case error: this torrent's download dir or file is missing // Special case error: this torrent's download dir or file is missing
message = 'Missing path: ' + TorrentSummary.getFileOrFolder(torrentSummary) message = 'Missing path: ' + TorrentSummary.getFileOrFolder(torrentSummary)
@@ -264,14 +268,9 @@ module.exports = class TorrentList extends React.Component {
) )
} else { } else {
// We do know the files. List them and show download stats for each one // We do know the files. List them and show download stats for each one
var fileRows = torrentSummary.files const fileRows = torrentSummary.files
.filter((file) => !file.path.includes('/.____padding_file/')) .filter((file) => !file.path.includes('/.____padding_file/'))
.map((file, index) => ({ file, index })) .map((file, index) => ({ file, index }))
.sort(function (a, b) {
if (a.file.name < b.file.name) return -1
if (b.file.name < a.file.name) return 1
return 0
})
.map((object) => this.renderFileRow(torrentSummary, object.file, object.index)) .map((object) => this.renderFileRow(torrentSummary, object.file, object.index))
filesElement = ( filesElement = (
@@ -296,28 +295,28 @@ module.exports = class TorrentList extends React.Component {
renderFileRow (torrentSummary, file, index) { renderFileRow (torrentSummary, file, index) {
// First, find out how much of the file we've downloaded // First, find out how much of the file we've downloaded
// Are we even torrenting it? // Are we even torrenting it?
var isSelected = torrentSummary.selections && torrentSummary.selections[index] const isSelected = torrentSummary.selections && torrentSummary.selections[index]
var isDone = false // Are we finished torrenting it? let isDone = false // Are we finished torrenting it?
var progress = '' let progress = ''
if (torrentSummary.progress && torrentSummary.progress.files && if (torrentSummary.progress && torrentSummary.progress.files &&
torrentSummary.progress.files[index]) { torrentSummary.progress.files[index]) {
var fileProg = torrentSummary.progress.files[index] const fileProg = torrentSummary.progress.files[index]
isDone = fileProg.numPiecesPresent === fileProg.numPieces isDone = fileProg.numPiecesPresent === fileProg.numPieces
progress = Math.round(100 * fileProg.numPiecesPresent / fileProg.numPieces) + '%' progress = Math.round(100 * fileProg.numPiecesPresent / fileProg.numPieces) + '%'
} }
// Second, for media files where we saved our position, show how far we got // Second, for media files where we saved our position, show how far we got
var positionElem let positionElem
if (file.currentTime) { if (file.currentTime) {
// Radial progress bar. 0% = start from 0:00, 270% = 3/4 of the way thru // Radial progress bar. 0% = start from 0:00, 270% = 3/4 of the way thru
positionElem = this.renderRadialProgressBar(file.currentTime / file.duration) positionElem = this.renderRadialProgressBar(file.currentTime / file.duration)
} }
// Finally, render the file as a table row // Finally, render the file as a table row
var isPlayable = TorrentPlayer.isPlayable(file) const isPlayable = TorrentPlayer.isPlayable(file)
var infoHash = torrentSummary.infoHash const infoHash = torrentSummary.infoHash
var icon let icon
var handleClick let handleClick
if (isPlayable) { if (isPlayable) {
icon = 'play_arrow' /* playable? add option to play */ icon = 'play_arrow' /* playable? add option to play */
handleClick = dispatcher('playFile', infoHash, index) handleClick = dispatcher('playFile', infoHash, index)
@@ -328,7 +327,7 @@ module.exports = class TorrentList extends React.Component {
: (e) => e.stopPropagation() // noop if file is not ready : (e) => e.stopPropagation() // noop if file is not ready
} }
// TODO: add a css 'disabled' class to indicate that a file cannot be opened/streamed // TODO: add a css 'disabled' class to indicate that a file cannot be opened/streamed
var rowClass = '' let rowClass = ''
if (!isSelected) rowClass = 'disabled' // File deselected, not being torrented if (!isSelected) rowClass = 'disabled' // File deselected, not being torrented
if (!isDone && !isPlayable) rowClass = 'disabled' // Can't open yet, can't stream if (!isDone && !isPlayable) rowClass = 'disabled' // Can't open yet, can't stream
return ( return (
@@ -348,16 +347,16 @@ module.exports = class TorrentList extends React.Component {
</td> </td>
<td className='col-select' <td className='col-select'
onClick={dispatcher('toggleTorrentFile', infoHash, index)}> onClick={dispatcher('toggleTorrentFile', infoHash, index)}>
<i className='icon'>{isSelected ? 'close' : 'add'}</i> <i className='icon deselect-file'>{isSelected ? 'close' : 'add'}</i>
</td> </td>
</tr> </tr>
) )
} }
renderRadialProgressBar (fraction, cssClass) { renderRadialProgressBar (fraction, cssClass) {
var rotation = 360 * fraction const rotation = 360 * fraction
var transformFill = {transform: 'rotate(' + (rotation / 2) + 'deg)'} const transformFill = {transform: 'rotate(' + (rotation / 2) + 'deg)'}
var transformFix = {transform: 'rotate(' + rotation + 'deg)'} const transformFix = {transform: 'rotate(' + rotation + 'deg)'}
return ( return (
<div key='radial-progress' className={'radial-progress ' + cssClass}> <div key='radial-progress' className={'radial-progress ' + cssClass}>
@@ -377,7 +376,7 @@ module.exports = class TorrentList extends React.Component {
} }
function getErrorMessage (torrentSummary) { function getErrorMessage (torrentSummary) {
var err = torrentSummary.error const err = torrentSummary.error
if (err === 'path-missing') { if (err === 'path-missing') {
return ( return (
<span> <span>

View File

@@ -2,26 +2,27 @@
// process from the main window. // process from the main window.
console.time('init') console.time('init')
var crypto = require('crypto') const crypto = require('crypto')
var deepEqual = require('deep-equal') const deepEqual = require('deep-equal')
var defaultAnnounceList = require('create-torrent').announceList const defaultAnnounceList = require('create-torrent').announceList
var electron = require('electron') const electron = require('electron')
var fs = require('fs-extra') const fs = require('fs-extra')
var musicmetadata = require('musicmetadata') const musicmetadata = require('musicmetadata')
var networkAddress = require('network-address') const networkAddress = require('network-address')
var path = require('path') const path = require('path')
var WebTorrent = require('webtorrent') const WebTorrent = require('webtorrent')
var zeroFill = require('zero-fill') const zeroFill = require('zero-fill')
var crashReporter = require('../crash-reporter') const crashReporter = require('../crash-reporter')
var config = require('../config') const config = require('../config')
var torrentPoster = require('./lib/torrent-poster') const {TorrentKeyNotFoundError} = require('./lib/errors')
const torrentPoster = require('./lib/torrent-poster')
// Report when the process crashes // Report when the process crashes
crashReporter.init() crashReporter.init()
// Send & receive messages from the main window // Send & receive messages from the main window
var ipc = electron.ipcRenderer const ipc = electron.ipcRenderer
// Force use of webtorrent trackers on all torrents // Force use of webtorrent trackers on all torrents
global.WEBTORRENT_ANNOUNCE = defaultAnnounceList global.WEBTORRENT_ANNOUNCE = defaultAnnounceList
@@ -31,7 +32,7 @@ global.WEBTORRENT_ANNOUNCE = defaultAnnounceList
/** /**
* WebTorrent version. * WebTorrent version.
*/ */
var VERSION = require('../../package.json').version const VERSION = require('../../package.json').version
/** /**
* Version number in Azureus-style. Generated from major and minor semver version. * Version number in Azureus-style. Generated from major and minor semver version.
@@ -39,7 +40,7 @@ var VERSION = require('../../package.json').version
* '0.16.1' -> '0016' * '0.16.1' -> '0016'
* '1.2.5' -> '0102' * '1.2.5' -> '0102'
*/ */
var VERSION_STR = VERSION.match(/([0-9]+)/g) const VERSION_STR = VERSION.match(/([0-9]+)/g)
.slice(0, 2) .slice(0, 2)
.map((v) => zeroFill(2, v)) .map((v) => zeroFill(2, v))
.join('') .join('')
@@ -51,19 +52,19 @@ var VERSION_STR = VERSION.match(/([0-9]+)/g)
* For example: * For example:
* '-WW0102-'... * '-WW0102-'...
*/ */
var VERSION_PREFIX = '-WD' + VERSION_STR + '-' const VERSION_PREFIX = '-WD' + VERSION_STR + '-'
// Connect to the WebTorrent and BitTorrent networks. WebTorrent Desktop is a hybrid // Connect to the WebTorrent and BitTorrent networks. WebTorrent Desktop is a hybrid
// client, as explained here: https://webtorrent.io/faq // client, as explained here: https://webtorrent.io/faq
var client = window.client = new WebTorrent({ const client = window.client = new WebTorrent({
peerId: Buffer.from(VERSION_PREFIX + crypto.randomBytes(6).toString('hex')) peerId: Buffer.from(VERSION_PREFIX + crypto.randomBytes(6).toString('hex'))
}) })
// WebTorrent-to-HTTP streaming sever // WebTorrent-to-HTTP streaming sever
var server = null let server = null
// Used for diffing, so we only send progress updates when necessary // Used for diffing, so we only send progress updates when necessary
var prevProgress = null let prevProgress = null
init() init()
@@ -83,8 +84,8 @@ function init () {
generateTorrentPoster(torrentKey)) generateTorrentPoster(torrentKey))
ipc.on('wt-get-audio-metadata', (e, infoHash, index) => ipc.on('wt-get-audio-metadata', (e, infoHash, index) =>
getAudioMetadata(infoHash, index)) getAudioMetadata(infoHash, index))
ipc.on('wt-start-server', (e, infoHash, index) => ipc.on('wt-start-server', (e, infoHash) =>
startServer(infoHash, index)) startServer(infoHash))
ipc.on('wt-stop-server', (e) => ipc.on('wt-stop-server', (e) =>
stopServer()) stopServer())
ipc.on('wt-select-files', (e, infoHash, selections) => ipc.on('wt-select-files', (e, infoHash, selections) =>
@@ -100,12 +101,12 @@ function init () {
console.timeEnd('init') console.timeEnd('init')
} }
// Starts a given TorrentID, which can be an infohash, magnet URI, etc. Returns WebTorrent object // Starts a given TorrentID, which can be an infohash, magnet URI, etc.
// See https://github.com/feross/webtorrent/blob/master/docs/api.md#clientaddtorrentid-opts-function-ontorrent-torrent- // Returns a WebTorrent object. See https://git.io/vik9M
function startTorrenting (torrentKey, torrentID, path, fileModtimes, selections) { function startTorrenting (torrentKey, torrentID, path, fileModtimes, selections) {
console.log('starting torrent %s: %s', torrentKey, torrentID) console.log('starting torrent %s: %s', torrentKey, torrentID)
var torrent = client.add(torrentID, { const torrent = client.add(torrentID, {
path: path, path: path,
fileModtimes: fileModtimes fileModtimes: fileModtimes
}) })
@@ -121,15 +122,15 @@ function startTorrenting (torrentKey, torrentID, path, fileModtimes, selections)
} }
function stopTorrenting (infoHash) { function stopTorrenting (infoHash) {
var torrent = client.get(infoHash) const torrent = client.get(infoHash)
if (torrent) torrent.destroy() if (torrent) torrent.destroy()
} }
// Create a new torrent, start seeding // Create a new torrent, start seeding
function createTorrent (torrentKey, options) { function createTorrent (torrentKey, options) {
console.log('creating torrent', torrentKey, options) console.log('creating torrent', torrentKey, options)
var paths = options.files.map((f) => f.path) const paths = options.files.map((f) => f.path)
var torrent = client.seed(paths, options) const torrent = client.seed(paths, options)
torrent.key = torrentKey torrent.key = torrentKey
addTorrentEvents(torrent) addTorrentEvents(torrent)
ipc.send('wt-new-torrent') ipc.send('wt-new-torrent')
@@ -147,14 +148,14 @@ function addTorrentEvents (torrent) {
torrent.on('done', torrentDone) torrent.on('done', torrentDone)
function torrentMetadata () { function torrentMetadata () {
var info = getTorrentInfo(torrent) const info = getTorrentInfo(torrent)
ipc.send('wt-metadata', torrent.key, info) ipc.send('wt-metadata', torrent.key, info)
updateTorrentProgress() updateTorrentProgress()
} }
function torrentReady () { function torrentReady () {
var info = getTorrentInfo(torrent) const info = getTorrentInfo(torrent)
ipc.send('wt-ready', torrent.key, info) ipc.send('wt-ready', torrent.key, info)
ipc.send('wt-ready-' + torrent.infoHash, torrent.key, info) // TODO: hack ipc.send('wt-ready-' + torrent.infoHash, torrent.key, info) // TODO: hack
@@ -162,7 +163,7 @@ function addTorrentEvents (torrent) {
} }
function torrentDone () { function torrentDone () {
var info = getTorrentInfo(torrent) const info = getTorrentInfo(torrent)
ipc.send('wt-done', torrent.key, info) ipc.send('wt-done', torrent.key, info)
updateTorrentProgress() updateTorrentProgress()
@@ -198,9 +199,9 @@ function getTorrentFileInfo (file) {
// Every time we resolve a magnet URI, save the torrent file so that we never // Every time we resolve a magnet URI, save the torrent file so that we never
// have to download it again. Never ask the DHT the same question twice. // have to download it again. Never ask the DHT the same question twice.
function saveTorrentFile (torrentKey) { function saveTorrentFile (torrentKey) {
var torrent = getTorrent(torrentKey) const torrent = getTorrent(torrentKey)
checkIfTorrentFileExists(torrent.infoHash, function (torrentPath, exists) { checkIfTorrentFileExists(torrent.infoHash, function (torrentPath, exists) {
var fileName = torrent.infoHash + '.torrent' const fileName = torrent.infoHash + '.torrent'
if (exists) { if (exists) {
// We've already saved the file // We've already saved the file
return ipc.send('wt-file-saved', torrentKey, fileName) return ipc.send('wt-file-saved', torrentKey, fileName)
@@ -220,7 +221,7 @@ function saveTorrentFile (torrentKey) {
// Checks whether we've already resolved a given infohash to a torrent file // Checks whether we've already resolved a given infohash to a torrent file
// Calls back with (torrentPath, exists). Logs, does not call back on error // Calls back with (torrentPath, exists). Logs, does not call back on error
function checkIfTorrentFileExists (infoHash, cb) { function checkIfTorrentFileExists (infoHash, cb) {
var torrentPath = path.join(config.TORRENT_PATH, infoHash + '.torrent') const torrentPath = path.join(config.TORRENT_PATH, infoHash + '.torrent')
fs.exists(torrentPath, function (exists) { fs.exists(torrentPath, function (exists) {
cb(torrentPath, exists) cb(torrentPath, exists)
}) })
@@ -229,14 +230,14 @@ function checkIfTorrentFileExists (infoHash, cb) {
// Save a JPG that represents a torrent. // Save a JPG that represents a torrent.
// Auto chooses either a frame from a video file, an image, etc // Auto chooses either a frame from a video file, an image, etc
function generateTorrentPoster (torrentKey) { function generateTorrentPoster (torrentKey) {
var torrent = getTorrent(torrentKey) const torrent = getTorrent(torrentKey)
torrentPoster(torrent, function (err, buf, extension) { torrentPoster(torrent, function (err, buf, extension) {
if (err) return console.log('error generating poster: %o', err) if (err) return console.log('error generating poster: %o', err)
// save it for next time // save it for next time
fs.mkdirp(config.POSTER_PATH, function (err) { fs.mkdirp(config.POSTER_PATH, function (err) {
if (err) return console.log('error creating poster dir: %o', err) if (err) return console.log('error creating poster dir: %o', err)
var posterFileName = torrent.infoHash + extension const posterFileName = torrent.infoHash + extension
var posterFilePath = path.join(config.POSTER_PATH, posterFileName) const posterFilePath = path.join(config.POSTER_PATH, posterFileName)
fs.writeFile(posterFilePath, buf, function (err) { fs.writeFile(posterFilePath, buf, function (err) {
if (err) return console.log('error saving poster: %o', err) if (err) return console.log('error saving poster: %o', err)
// show the poster // show the poster
@@ -247,7 +248,7 @@ function generateTorrentPoster (torrentKey) {
} }
function updateTorrentProgress () { function updateTorrentProgress () {
var progress = getTorrentProgress() const progress = getTorrentProgress()
// TODO: diff torrent-by-torrent, not once for the whole update // TODO: diff torrent-by-torrent, not once for the whole update
if (prevProgress && deepEqual(progress, prevProgress, {strict: true})) { if (prevProgress && deepEqual(progress, prevProgress, {strict: true})) {
return /* don't send heavy object if it hasn't changed */ return /* don't send heavy object if it hasn't changed */
@@ -258,19 +259,19 @@ function updateTorrentProgress () {
function getTorrentProgress () { function getTorrentProgress () {
// First, track overall progress // First, track overall progress
var progress = client.progress const progress = client.progress
var hasActiveTorrents = client.torrents.some(function (torrent) { const hasActiveTorrents = client.torrents.some(function (torrent) {
return torrent.progress !== 1 return torrent.progress !== 1
}) })
// Track progress for every file in each torrent // Track progress for every file in each torrent
// TODO: ideally this would be tracked by WebTorrent, which could do it // TODO: ideally this would be tracked by WebTorrent, which could do it
// more efficiently than looping over torrent.bitfield // more efficiently than looping over torrent.bitfield
var torrentProg = client.torrents.map(function (torrent) { const torrentProg = client.torrents.map(function (torrent) {
var fileProg = torrent.files && torrent.files.map(function (file, index) { const fileProg = torrent.files && torrent.files.map(function (file, index) {
var numPieces = file._endPiece - file._startPiece + 1 const numPieces = file._endPiece - file._startPiece + 1
var numPiecesPresent = 0 let numPiecesPresent = 0
for (var piece = file._startPiece; piece <= file._endPiece; piece++) { for (let piece = file._startPiece; piece <= file._endPiece; piece++) {
if (torrent.bitfield.get(piece)) numPiecesPresent++ if (torrent.bitfield.get(piece)) numPiecesPresent++
} }
return { return {
@@ -301,28 +302,28 @@ function getTorrentProgress () {
} }
} }
function startServer (infoHash, index) { function startServer (infoHash) {
var torrent = client.get(infoHash) const torrent = client.get(infoHash)
if (torrent.ready) startServerFromReadyTorrent(torrent, index) if (torrent.ready) startServerFromReadyTorrent(torrent)
else torrent.once('ready', () => startServerFromReadyTorrent(torrent, index)) else torrent.once('ready', () => startServerFromReadyTorrent(torrent))
} }
function startServerFromReadyTorrent (torrent, index, cb) { function startServerFromReadyTorrent (torrent, cb) {
if (server) return if (server) return
// start the streaming torrent-to-http server // start the streaming torrent-to-http server
server = torrent.createServer() server = torrent.createServer()
server.listen(0, function () { server.listen(0, function () {
var port = server.address().port const port = server.address().port
var urlSuffix = ':' + port + '/' + index const urlSuffix = ':' + port
var info = { const info = {
torrentKey: torrent.key, torrentKey: torrent.key,
localURL: 'http://localhost' + urlSuffix, localURL: 'http://localhost' + urlSuffix,
networkURL: 'http://' + networkAddress() + urlSuffix networkURL: 'http://' + networkAddress() + urlSuffix
} }
ipc.send('wt-server-running', info) ipc.send('wt-server-running', info)
ipc.send('wt-server-' + torrent.infoHash, info) // TODO: hack ipc.send('wt-server-' + torrent.infoHash, info)
}) })
} }
@@ -333,8 +334,8 @@ function stopServer () {
} }
function getAudioMetadata (infoHash, index) { function getAudioMetadata (infoHash, index) {
var torrent = client.get(infoHash) const torrent = client.get(infoHash)
var file = torrent.files[index] const file = torrent.files[index]
musicmetadata(file.createReadStream(), function (err, info) { musicmetadata(file.createReadStream(), function (err, info) {
if (err) return if (err) return
console.log('got audio metadata for %s: %o', file.name, info) console.log('got audio metadata for %s: %o', file.name, info)
@@ -344,12 +345,15 @@ function getAudioMetadata (infoHash, index) {
function selectFiles (torrentOrInfoHash, selections) { function selectFiles (torrentOrInfoHash, selections) {
// Get the torrent object // Get the torrent object
var torrent let torrent
if (typeof torrentOrInfoHash === 'string') { if (typeof torrentOrInfoHash === 'string') {
torrent = client.get(torrentOrInfoHash) torrent = client.get(torrentOrInfoHash)
} else { } else {
torrent = torrentOrInfoHash torrent = torrentOrInfoHash
} }
if (!torrent) {
throw new Error('selectFiles: missing torrent ' + torrentOrInfoHash)
}
// Selections not specified? // Selections not specified?
// Load all files. We still need to replace the default whole-torrent // Load all files. We still need to replace the default whole-torrent
@@ -369,8 +373,8 @@ function selectFiles (torrentOrInfoHash, selections) {
torrent.deselect(0, torrent.pieces.length - 1, false) torrent.deselect(0, torrent.pieces.length - 1, false)
// Add selections (individual files) // Add selections (individual files)
for (var i = 0; i < selections.length; i++) { for (let i = 0; i < selections.length; i++) {
var file = torrent.files[i] const file = torrent.files[i]
if (selections[i]) { if (selections[i]) {
file.select() file.select()
} else { } else {
@@ -383,8 +387,8 @@ function selectFiles (torrentOrInfoHash, selections) {
// Gets a WebTorrent handle by torrentKey // Gets a WebTorrent handle by torrentKey
// Throws an Error if we're not currently torrenting anything w/ that key // Throws an Error if we're not currently torrenting anything w/ that key
function getTorrent (torrentKey) { function getTorrent (torrentKey) {
var ret = client.torrents.find((x) => x.key === torrentKey) const ret = client.torrents.find((x) => x.key === torrentKey)
if (!ret) throw new Error('missing torrent key ' + torrentKey) if (!ret) throw new TorrentKeyNotFoundError(torrentKey)
return ret return ret
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 225 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 233 B

View File

@@ -265,14 +265,9 @@ table {
font-weight: bold; font-weight: bold;
} }
.open-torrent-address-modal input {
width: 100%;
}
.create-torrent { .create-torrent {
padding: 10px 25px; padding: 10px 25px;
overflow: hidden; overflow: hidden;
font: 16px/24px BlinkMacSystemFont, "Helvetica Neue", Helvetica, sans-serif;
} }
.create-torrent .torrent-attribute { .create-torrent .torrent-attribute {
@@ -323,6 +318,15 @@ i:not(.disabled):hover { /* Show they're clickable without pointer: cursor */
text-align: center; text-align: center;
} }
/*
* INPUTS
*/
input,
textarea,
.control {
font-size: 14px !important;
}
/* /*
* TORRENT LIST * TORRENT LIST
*/ */
@@ -346,10 +350,6 @@ i:not(.disabled):hover { /* Show they're clickable without pointer: cursor */
border-bottom: 1px solid rgb(20, 20, 20); border-bottom: 1px solid rgb(20, 20, 20);
} }
.torrent:hover {
-webkit-filter: brightness(1.1);
}
.torrent .metadata { .torrent .metadata {
position: absolute; position: absolute;
top: 25px; top: 25px;
@@ -455,6 +455,20 @@ i:not(.disabled):hover { /* Show they're clickable without pointer: cursor */
line-height: 1.5em; line-height: 1.5em;
} }
progress {
width: 60px;
margin-right: 8px;
-webkit-appearance: none;
}
progress::-webkit-progress-bar {
background-color: #888;
}
progress::-webkit-progress-value {
background-color: #eee;
}
/* /*
* TORRENT LIST: ERRORS * TORRENT LIST: ERRORS
*/ */
@@ -506,6 +520,7 @@ body.drag .app::after {
/* /*
* TORRENT LIST: EXPANDED TORRENT DETAILS * TORRENT LIST: EXPANDED TORRENT DETAILS
*/ */
.torrent.selected { .torrent.selected {
height: auto; height: auto;
} }
@@ -529,10 +544,6 @@ body.drag .app::after {
height: 28px; height: 28px;
} }
.torrent-details tr:hover {
background-color: rgba(200, 200, 200, 0.3);
}
.torrent-details td { .torrent-details td {
overflow: hidden; overflow: hidden;
padding: 0; padding: 0;
@@ -629,11 +640,15 @@ body.drag .app::after {
opacity: 1; opacity: 1;
} }
.player .controls .play-pause { .player .controls .icon.disabled {
opacity: 0.3;
}
.player .controls .icon.skip-previous,
.player .controls .icon.play-pause,
.player .controls .icon.skip-next {
font-size: 28px; font-size: 28px;
margin-top: 5px; margin: 5px;
margin-right: 10px;
margin-left: 15px;
} }
.player .controls .volume-slider { .player .controls .volume-slider {

View File

@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>WebTorrent Desktop</title> <title>Main Window</title>
<link rel="stylesheet" href="main.css"> <link rel="stylesheet" href="main.css">
</head> </head>
<body> <body>

View File

@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>WebTorrent Desktop</title> <title>WebTorrent Hidden Window</title>
<style> <style>
body { body {
background-color: #282828; background-color: #282828;

33
test/index.js Normal file
View File

@@ -0,0 +1,33 @@
const test = require('tape')
const fs = require('fs-extra')
const setup = require('./setup')
console.log('Creating download dir: ' + setup.TEST_DOWNLOAD_DIR)
fs.mkdirpSync(setup.TEST_DOWNLOAD_DIR)
test.onFinish(function () {
console.log('Removing test dir: ' + setup.TEST_DATA_DIR)
fs.removeSync(setup.TEST_DATA_DIR) // includes download dir
})
test('app runs', function (t) {
t.timeoutAfter(10e3)
const app = setup.createApp()
setup.waitForLoad(app, t)
.then(() => setup.wait())
.then(() => setup.screenshotCreateOrCompare(app, t, 'torrent-list-basic'))
.then(() => setup.endTest(app, t),
(err) => setup.endTest(app, t, err || 'error'))
})
console.log('Testing the torrent list (home page)...')
setup.wipeTestDataDir()
require('./test-torrent-list')
// TODO:
// require('./test-add-torrent')
// require('./test-create-torrent')
// require('./test-prefs')
// require('./test-video')
// require('./test-audio')
// require('./test-cast')

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 617 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 617 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 777 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

111
test/setup.js Normal file
View File

@@ -0,0 +1,111 @@
const path = require('path')
const Application = require('spectron').Application
const fs = require('fs-extra')
const TEST_DATA_DIR = path.join(__dirname, 'tempTestData')
const TEST_DOWNLOAD_DIR = path.join(TEST_DATA_DIR, 'Downloads')
module.exports = {
TEST_DATA_DIR,
TEST_DOWNLOAD_DIR,
createApp,
endTest,
screenshotCreateOrCompare,
compareDownloadFolder,
waitForLoad,
wait,
wipeTestDataDir
}
// Runs WebTorrent Desktop.
// Returns a promise that resolves to a Spectron Application once the app has loaded.
// Takes a Tape test. Makes some basic assertions to verify that the app loaded correctly.
function createApp (t) {
return new Application({
path: path.join(__dirname, '..', 'node_modules', '.bin',
'electron' + (process.platform === 'win32' ? '.cmd' : '')),
args: [path.join(__dirname, '..')],
env: {NODE_ENV: 'test'}
})
}
// Starts the app, waits for it to load, returns a promise
function waitForLoad (app, t, opts) {
if (!opts) opts = {}
return app.start().then(function () {
return app.client.waitUntilWindowLoaded()
}).then(function () {
// Offline mode? Disable internet in the webtorrent window
// TODO. For now, just run integration tests with internet turned off.
// Spectron is poorly documented, and contrary to the docs, webContents.session is missing
// That is the correct API (in theory) to put the app in offline mode
}).then(function () {
// Switch to the main window. Index 0 is apparently the hidden webtorrent window...
return app.client.windowByIndex(1)
}).then(function () {
return app.client.waitUntilWindowLoaded()
}).then(function () {
return app.webContents.getTitle()
}).then(function (title) {
// Note the window title is WebTorrent (BETA), this is the HTML <title>
t.equal(title, 'Main Window', 'html title')
})
}
// Returns a promise that resolves after 'ms' milliseconds. Default: 500
function wait (ms) {
if (ms === undefined) ms = 500 // Default: wait long enough for the UI to update
return new Promise(function (resolve, reject) {
setTimeout(resolve, ms)
})
}
// Quit the app, end the test, either in success (!err) or failure (err)
function endTest (app, t, err) {
return app.stop().then(function () {
t.end(err)
})
}
// Takes a screenshot of the app
// If we already have a reference under test/screenshots, assert that they're the same
// Otherwise, create the reference screenshot: test/screenshots/<platform>/<name>.png
function screenshotCreateOrCompare (app, t, name) {
const ssDir = path.join(__dirname, 'screenshots', process.platform)
const ssPath = path.join(ssDir, name + '.png')
fs.ensureFileSync(ssPath)
const ssBuf = fs.readFileSync(ssPath)
return app.browserWindow.capturePage().then(function (buffer) {
if (ssBuf.length === 0) {
console.log('Saving screenshot ' + ssPath)
fs.writeFileSync(ssPath, buffer)
} else {
const match = Buffer.compare(buffer, ssBuf) === 0
t.ok(match, 'screenshot comparison ' + name)
if (!match) {
const ssFailedPath = path.join(ssDir, name + '-failed.png')
console.log('Saving screenshot, failed comparison: ' + ssFailedPath)
fs.writeFileSync(ssFailedPath, buffer)
}
}
})
}
// Resets the test directory, containing config.json, torrents, downloads, etc
function wipeTestDataDir () {
fs.removeSync(TEST_DATA_DIR)
fs.mkdirpSync(TEST_DOWNLOAD_DIR) // Downloads/ is inside of TEST_DATA_DIR
}
function compareDownloadFolder (t, dirname, filenames) {
const dirpath = path.join(TEST_DOWNLOAD_DIR, dirname)
try {
const actualFilenames = fs.readdirSync(dirpath)
const expectedSorted = filenames.slice().sort()
const actualSorted = actualFilenames.slice().sort()
t.deepEqual(actualSorted, expectedSorted, 'download folder contents: ' + dirname)
} catch (e) {
console.error(e)
t.equal(filenames, null, 'download folder missing: ' + dirname)
}
}

104
test/test-torrent-list.js Normal file
View File

@@ -0,0 +1,104 @@
const test = require('tape')
const fs = require('fs-extra')
const setup = require('./setup')
test.skip('torrent-list: show download path missing', function (t) {
setup.wipeTestDataDir()
fs.removeSync(setup.TEST_DOWNLOAD_DIR)
t.timeoutAfter(10e3)
const app = setup.createApp()
setup.waitForLoad(app, t)
.then(() => app.client.getTitle())
.then((text) => console.log('Title ' + text))
.then(() => app.client.waitUntilTextExists('.torrent-list', 'Download path missing'))
.then((err) => t.notOk(err))
.then(() => setup.screenshotCreateOrCompare(app, t, 'torrent-list-download-path-missing'))
.then(() => app.client.click('a'))
.then(() => setup.wait())
.then(() => app.browserWindow.getTitle())
.then((windowTitle) => t.equal(windowTitle, 'Preferences', 'window title'))
.then(() => setup.screenshotCreateOrCompare(app, t, 'prefs-basic'))
.then(() => setup.endTest(app, t),
(err) => setup.endTest(app, t, err || 'error'))
})
test.skip('torrent-list: start, stop, and delete torrents', function (t) {
setup.wipeTestDataDir()
const app = setup.createApp()
setup.waitForLoad(app, t, {offline: true})
.then(() => app.client.waitUntilTextExists('.torrent-list', 'Big Buck Bunny'))
// Mouse over the first torrent
.then(() => app.client.moveToObject('.torrent'))
.then(() => setup.wait())
.then(() => setup.screenshotCreateOrCompare(app, t, 'torrent-list-hover'))
// Click download on the first torrent, start downloading
.then(() => app.client.click('.icon.download'))
.then(() => app.client.waitUntilTextExists('.torrent-list', '276 MB'))
.then(() => setup.screenshotCreateOrCompare(app, t, 'torrent-list-start-download'))
// Click download on the first torrent again, stop downloading
.then(() => app.client.click('.icon.download'))
.then(() => setup.wait())
.then(() => setup.screenshotCreateOrCompare(app, t, 'torrent-list-hover-download'))
// Click delete on the first torrent
.then(() => app.client.click('.icon.delete'))
.then(() => setup.wait())
.then(() => setup.screenshotCreateOrCompare(app, t, 'torrent-list-delete-prompt'))
// Click cancel on the resulting confirmation dialog. Should be same as before.
.then(() => app.client.click('.control.cancel'))
.then(() => setup.wait())
.then(() => setup.screenshotCreateOrCompare(app, t, 'torrent-list-hover'))
// Click delete on the first torrent again
.then(() => app.client.click('.icon.delete'))
.then(() => setup.wait())
.then(() => setup.screenshotCreateOrCompare(app, t, 'torrent-list-delete-prompt'))
// This time, click OK to confirm.
.then(() => app.client.click('.control.ok'))
.then(() => setup.wait())
.then(() => setup.screenshotCreateOrCompare(app, t, 'torrent-list-deleted'))
.then(() => setup.endTest(app, t),
(err) => setup.endTest(app, t, err || 'error'))
})
test('torrent-list: expand torrent, unselect file', function (t) {
setup.wipeTestDataDir()
const app = setup.createApp()
setup.waitForLoad(app, t, {offline: true})
.then(() => app.client.waitUntilTextExists('.torrent-list', 'Big Buck Bunny'))
// Mouse over the torrent
.then(() => app.client.moveToObject('#torrent-cosmos'))
.then(() => setup.wait())
.then(() => setup.screenshotCreateOrCompare(app, t, 'torrent-list-cosmos-hover'))
// Click on the torrent, expand
.then(() => app.client.click('#torrent-cosmos'))
.then(() => setup.wait())
.then(() => setup.screenshotCreateOrCompare(app, t, 'torrent-list-cosmos-expand'))
// Deselect the first file
.then(() => app.client.click('#torrent-cosmos .icon.deselect-file'))
.then(() => setup.wait())
.then(() => setup.screenshotCreateOrCompare(app, t, 'torrent-list-cosmos-expand-deselect'))
// Start the torrent
.then(() => app.client.click('#torrent-cosmos .icon.download'))
.then(() => app.client.waitUntilTextExists('.torrent-list', '0%'))
.then(() => setup.screenshotCreateOrCompare(app, t, 'torrent-list-cosmos-expand-start'))
// Make sure that it creates all files EXCEPT the deslected one
.then(() => setup.compareDownloadFolder(t, 'CosmosLaundromatFirstCycle', [
// TODO: the .gif should NOT be here, since we just deselected it.
// This is a bug. See https://github.com/feross/webtorrent-desktop/issues/719
'Cosmos Laundromat - First Cycle (1080p).gif',
'Cosmos Laundromat - First Cycle (1080p).mp4',
'Cosmos Laundromat - First Cycle (1080p).ogv',
'CosmosLaundromat-FirstCycle1080p.en.srt',
'CosmosLaundromat-FirstCycle1080p.es.srt',
'CosmosLaundromat-FirstCycle1080p.fr.srt',
'CosmosLaundromat-FirstCycle1080p.it.srt',
'CosmosLaundromatFirstCycle_meta.sqlite',
'CosmosLaundromatFirstCycle_meta.xml'
]))
// Make sure that all the files are gone
.then(() => setup.compareDownloadFolder(t, 'CosmosLaundromatFirstCycle', null))
.then(() => setup.endTest(app, t),
(err) => setup.endTest(app, t, err || 'error'))
})