Compare commits

..

1 Commits

Author SHA1 Message Date
Mathias Rasmussen
6656b75c1b WIP: add embedded mkv subtitles 2016-08-30 00:28:37 +02:00
118 changed files with 1515 additions and 2664 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 walk-sync install: npm install standard

View File

@@ -28,10 +28,5 @@
- Rémi Jouannet (remijouannet@gmail.com) - Rémi Jouannet (remijouannet@gmail.com)
- Andrea Tupini (tupini07@gmail.com) - Andrea Tupini (tupini07@gmail.com)
- grunjol (grunjol@gmail.com) - grunjol (grunjol@gmail.com)
- Jason Kurian (jasonk92@gmail.com)
- Vamsi Krishna Avula (vamsi_ism@outlook.com)
- Noam Okman (noamokman@gmail.com)
- PurgingPanda (t3ch0wn3r@gmail.com)
- Kai Curtis (morecode@kcurtis.com)
#### Generated by bin/update-authors.sh. #### Generated by bin/update-authors.sh.

View File

@@ -1,62 +1,5 @@
# WebTorrent Desktop Version History # WebTorrent Desktop Version History
## v0.16.0 - 2016-09-18
### Added
- **Windows 64-bit support!** ([#931](https://github.com/feross/webtorrent-desktop/pull/931))
- Existing 32-bit users will update to 64-bit automatically in next release
- 64-bit reduces likelihood of out-of-memory errors by increasing the address space
## v0.15.0 - 2016-09-16
### Added
- Option to start automatically on login
- Add integration tests
- Add more detailed telemetry to diagnose "buffer allocation failed"
### Changed
- Disable playback controls while in external player (#909)
### Fixed
- Fix several uncaught errors (#889, #891, #892)
- Update to the latest webtorrent.js, fixing some more uncaught errors
- Clicking on the "torrent finished" notification works again (#912)
## 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
### Added
- Support .m4a audio
- Better telemetry: log error versions, report more types of errors
### Changed
- New look - Material UI. Rewrote Create Torrent and Preferences pages.
### Fixed
- Fixed telemetry [object Object] and [object HTMLMediaElement] bugs
- Don't render player controls when playing externally, eg in VLC
- Don't play notification sounds during media playback
## v0.12.0 - 2016-08-23 ## v0.12.0 - 2016-08-23
### Added ### Added

View File

@@ -56,28 +56,6 @@ Restart the app automatically every time code changes. Useful during development
$ npm run watch $ npm run watch
``` ```
### Run linters
```
$ npm test
```
### Run integration tests
```
$ npm run integration-test
```
The integration tests use Spectron and Tape. They click through the app, taking screenshots and comparing each one to a reference. Why screenshots?
* Ad-hoc checking makes the tests a lot more work to write
* Even diffing the whole HTML is not as thorough as screenshot diffing. For example, it wouldn't catch an bug where hitting ESC from a video doesn't correctly restore window size.
* Chrome's own integration tests use screenshot diffing iirc
* Small UI changes will break a few tests, but the fix is as easy as deleting the offending screenshots and running the tests, which will recreate them with the new look.
* The resulting Github PR will then show, pixel by pixel, the exact UI changes that were made! Ses https://github.com/blog/817-behold-image-view-modes
For MacOS, you'll need a Retina screen for the integration tests to pass.
### Package the app ### Package the app
Builds app binaries for Mac, Linux, and Windows. Builds app binaries for Mac, Linux, and Windows.

108
bin/check-deps.js Executable file
View File

@@ -0,0 +1,108 @@
#!/usr/bin/env node
var fs = require('fs')
var cp = require('child_process')
// We can't use `builtin-modules` here since our TravisCI
// setup expects this file to run with no dependencies
var BUILT_IN_NODE_MODULES = [
'assert',
'buffer',
'child_process',
'cluster',
'console',
'constants',
'crypto',
'dgram',
'dns',
'domain',
'events',
'fs',
'http',
'https',
'module',
'net',
'os',
'path',
'process',
'punycode',
'querystring',
'readline',
'repl',
'stream',
'string_decoder',
'timers',
'tls',
'tty',
'url',
'util',
'v8',
'vm',
'zlib'
]
var BUILT_IN_ELECTRON_MODULES = [ 'electron' ]
var BUILT_IN_DEPS = [].concat(BUILT_IN_NODE_MODULES, BUILT_IN_ELECTRON_MODULES)
var EXECUTABLE_DEPS = [
'babel-cli',
'babel-plugin-syntax-jsx',
'babel-plugin-transform-es2015-destructuring',
'babel-plugin-transform-object-rest-spread',
'babel-plugin-transform-react-jsx',
'gh-release',
'nodemon',
'standard'
]
main()
// Scans codebase for missing or unused dependencies. Exits with code 0 on success.
function main () {
if (process.platform === 'win32') {
console.error('Sorry, check-deps only works on Mac and Linux')
return
}
var usedDeps = findUsedDeps()
var packageDeps = findPackageDeps()
var missingDeps = usedDeps.filter(
(dep) => !includes(packageDeps, dep) && !includes(BUILT_IN_DEPS, dep)
)
var unusedDeps = packageDeps.filter(
(dep) => !includes(usedDeps, dep) && !includes(EXECUTABLE_DEPS, dep)
)
if (missingDeps.length > 0) {
console.error('Missing package dependencies: ' + missingDeps)
}
if (unusedDeps.length > 0) {
console.error('Unused package dependencies: ' + unusedDeps)
}
if (missingDeps.length + unusedDeps.length > 0) {
process.exitCode = 1
}
}
// Finds all dependencies specified in `package.json`
function findPackageDeps () {
var pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'))
var deps = Object.keys(pkg.dependencies)
var devDeps = Object.keys(pkg.devDependencies)
var optionalDeps = Object.keys(pkg.optionalDependencies)
return [].concat(deps, devDeps, optionalDeps)
}
// Finds all dependencies that used with `require()`
function findUsedDeps () {
var stdout = cp.execSync('./bin/list-deps.sh')
return stdout.toString().trim().split('\n')
}
function includes (arr, elem) {
return arr.indexOf(elem) >= 0
}

View File

@@ -1,46 +0,0 @@
#!/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!')

10
bin/list-deps.sh Executable file
View File

@@ -0,0 +1,10 @@
#!/bin/sh
# This is a truly heinous hack, but it works pretty nicely.
# Find all modules we're requiring---even conditional requires.
grep "require('" src/ bin/ -R |
grep '.js:' |
sed "s/.*require('\([^'\/]*\).*/\1/" |
grep -v '^\.' |
sort |
uniq

View File

@@ -112,7 +112,7 @@ var darwin = {
// Build for Mac // Build for Mac
platform: 'darwin', platform: 'darwin',
// Build x64 binaries only. // Build 64 bit binaries only.
arch: 'x64', arch: 'x64',
// The bundle identifier to use in the application's plist (Mac only). // The bundle identifier to use in the application's plist (Mac only).
@@ -133,8 +133,8 @@ var win32 = {
// Build for Windows. // Build for Windows.
platform: 'win32', platform: 'win32',
// Build ia32 and x64 binaries. // Build 32 bit binaries only.
arch: 'all', arch: 'ia32',
// Object hash of application metadata to embed into the executable (Windows only) // Object hash of application metadata to embed into the executable (Windows only)
'version-string': { 'version-string': {
@@ -167,7 +167,7 @@ var linux = {
// Build for Linux. // Build for Linux.
platform: 'linux', platform: 'linux',
// Build ia32 and x64 binaries. // Build 32 and 64 bit binaries.
arch: 'all' arch: 'all'
// Note: Application icon for Linux is specified via the BrowserWindow `icon` option. // Note: Application icon for Linux is specified via the BrowserWindow `icon` option.
@@ -388,25 +388,19 @@ function buildWin32 (cb) {
} }
var tasks = [] var tasks = []
buildPath.forEach(function (filesPath) { if (argv.package === 'exe' || argv.package === 'all') {
var destArch = filesPath.split('-').pop() tasks.push((cb) => packageInstaller(cb))
}
if (argv.package === 'exe' || argv.package === 'all') { if (argv.package === 'portable' || argv.package === 'all') {
tasks.push((cb) => packageInstaller(filesPath, destArch, cb)) tasks.push((cb) => packagePortable(cb))
} }
if (argv.package === 'portable' || argv.package === 'all') {
tasks.push((cb) => packagePortable(filesPath, destArch, cb))
}
})
series(tasks, cb) series(tasks, cb)
function packageInstaller (filesPath, destArch, cb) { function packageInstaller (cb) {
console.log(`Windows: Creating ${destArch} installer...`) console.log('Windows: Creating installer...')
var archStr = destArch === 'ia32' ? '-ia32' : ''
installer.createWindowsInstaller({ installer.createWindowsInstaller({
appDirectory: filesPath, appDirectory: buildPath[0],
authors: config.APP_TEAM, authors: config.APP_TEAM,
description: config.APP_NAME, description: config.APP_NAME,
exe: config.APP_NAME + '.exe', exe: config.APP_NAME + '.exe',
@@ -416,21 +410,8 @@ function buildWin32 (cb) {
noMsi: true, noMsi: true,
outputDirectory: DIST_PATH, outputDirectory: DIST_PATH,
productName: config.APP_NAME, productName: config.APP_NAME,
/** remoteReleases: config.GITHUB_URL,
* Only create delta updates for the Windows x64 build because 90% of our setupExe: config.APP_NAME + 'Setup-v' + config.APP_VERSION + '.exe',
* users have Windows x64 and the delta files take a *very* long time to
* generate. Also, the ia32 files on GitHub have non-standard Squirrel
* names (i.e. RELEASES-ia32 instead of RELEASES) and so Squirrel won't
* find them unless we proxy the requests.
*/
remoteReleases: destArch === 'x64'
? config.GITHUB_URL
: undefined,
/**
* If you hit a "GitHub API rate limit exceeded" error, set this token!
*/
remoteToken: process.env.WEBTORRENT_GITHUB_API_TOKEN,
setupExe: config.APP_NAME + 'Setup-v' + config.APP_VERSION + archStr + '.exe',
setupIcon: config.APP_ICON + '.ico', setupIcon: config.APP_ICON + '.ico',
signWithParams: signWithParams, signWithParams: signWithParams,
title: config.APP_NAME, title: config.APP_NAME,
@@ -438,65 +419,23 @@ function buildWin32 (cb) {
version: pkg.version version: pkg.version
}) })
.then(function () { .then(function () {
console.log(`Windows: Created ${destArch} installer.`) console.log('Windows: Created installer.')
/**
* Delete extraneous Squirrel files (i.e. *.nupkg delta files for older
* versions of the app)
*/
fs.readdirSync(DIST_PATH)
.filter((name) => name.endsWith('.nupkg') && !name.includes(pkg.version))
.forEach((filename) => {
fs.unlinkSync(path.join(DIST_PATH, filename))
})
if (destArch === 'ia32') {
console.log('Windows: Renaming ia32 installer files...')
// RELEASES -> RELEASES-ia32
var relPath = path.join(DIST_PATH, 'RELEASES-ia32')
fs.renameSync(
path.join(DIST_PATH, 'RELEASES'),
relPath
)
// WebTorrent-vX.X.X-full.nupkg -> WebTorrent-vX.X.X-ia32-full.nupkg
fs.renameSync(
path.join(DIST_PATH, `${BUILD_NAME}-full.nupkg`),
path.join(DIST_PATH, `${BUILD_NAME}-ia32-full.nupkg`)
)
// Change file name inside RELEASES-ia32 to match renamed file
var relContent = fs.readFileSync(relPath, 'utf8')
var relContent32 = relContent.replace(/full\.nupkg$/, '-ia32-full.nupkg')
fs.writeFileSync(relPath, relContent32)
if (relContent === relContent32) {
// Sanity check
throw new Error('Fixing RELEASE-ia32 failed. Replacement did not modify the file.')
}
console.log('Windows: Renamed ia32 installer files.')
}
cb(null) cb(null)
}) })
.catch(cb) .catch(cb)
} }
function packagePortable (filesPath, destArch, cb) { function packagePortable (cb) {
console.log(`Windows: Creating ${destArch} portable app...`) console.log('Windows: Creating portable app...')
var portablePath = path.join(filesPath, 'Portable Settings') var portablePath = path.join(buildPath[0], 'Portable Settings')
mkdirp.sync(portablePath) mkdirp.sync(portablePath)
var archStr = destArch === 'ia32' ? '-ia32' : '' var inPath = path.join(DIST_PATH, path.basename(buildPath[0]))
var outPath = path.join(DIST_PATH, BUILD_NAME + '-win.zip')
var inPath = path.join(DIST_PATH, path.basename(filesPath))
var outPath = path.join(DIST_PATH, BUILD_NAME + '-win' + archStr + '.zip')
zip.zipSync(inPath, outPath) zip.zipSync(inPath, outPath)
console.log(`Windows: Created ${destArch} portable app.`) console.log('Windows: Created portable app.')
cb(null) cb(null)
} }
}) })
@@ -561,10 +500,8 @@ function buildLinux (cb) {
// Create .zip file for Linux // Create .zip file for Linux
console.log(`Linux: Creating ${destArch} zip...`) console.log(`Linux: Creating ${destArch} zip...`)
var archStr = destArch === 'ia32' ? '-ia32' : ''
var inPath = path.join(DIST_PATH, path.basename(filesPath)) var inPath = path.join(DIST_PATH, path.basename(filesPath))
var outPath = path.join(DIST_PATH, BUILD_NAME + '-linux' + archStr + '.zip') var outPath = path.join(DIST_PATH, BUILD_NAME + '-linux-' + destArch + '.zip')
zip.zipSync(inPath, outPath) zip.zipSync(inPath, outPath)
console.log(`Linux: Created ${destArch} zip.`) console.log(`Linux: Created ${destArch} zip.`)

View File

@@ -7,4 +7,4 @@ npm run package -- --sign
git push git push
git push --tags git push --tags
npm publish npm publish
npm run gh-release ./node_modules/.bin/gh-release

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.16.0", "version": "0.12.0",
"author": { "author": {
"name": "WebTorrent, LLC", "name": "WebTorrent, LLC",
"email": "feross@webtorrent.io", "email": "feross@webtorrent.io",
@@ -16,21 +16,19 @@
"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",
"capture-frame": "^1.0.0",
"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.4.0", "electron": "1.3.3",
"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",
"location-history": "^1.0.0", "location-history": "^1.0.0",
"material-ui": "^0.15.4", "material-ui": "^0.15.4",
"matroska-subtitles": "^2.0.0",
"musicmetadata": "^2.0.2", "musicmetadata": "^2.0.2",
"network-address": "^1.1.0", "network-address": "^1.1.0",
"parse-torrent": "^5.7.3", "parse-torrent": "^5.7.3",
@@ -55,7 +53,6 @@
"babel-plugin-transform-object-rest-spread": "^6.8.0", "babel-plugin-transform-object-rest-spread": "^6.8.0",
"babel-plugin-transform-react-jsx": "^6.8.0", "babel-plugin-transform-react-jsx": "^6.8.0",
"cross-zip": "^2.0.1", "cross-zip": "^2.0.1",
"depcheck": "^0.6.4",
"electron-osx-sign": "^0.3.0", "electron-osx-sign": "^0.3.0",
"electron-packager": "^7.0.0", "electron-packager": "^7.0.0",
"electron-winstaller": "^2.3.0", "electron-winstaller": "^2.3.0",
@@ -66,13 +63,9 @@
"nodemon": "^1.10.2", "nodemon": "^1.10.2",
"open": "0.0.5", "open": "0.0.5",
"plist": "^2.0.1", "plist": "^2.0.1",
"pngjs": "^3.0.0",
"rimraf": "^2.5.2", "rimraf": "^2.5.2",
"run-series": "^1.1.4", "run-series": "^1.1.4",
"spectron": "^3.3.0", "standard": "*"
"standard": "*",
"tape": "^4.6.0",
"walk-sync": "^0.3.1"
}, },
"engines": { "engines": {
"node": ">=4.0.0" "node": ">=4.0.0"
@@ -105,10 +98,8 @@
"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 .",
"integration-test": "npm run build && node ./test", "test": "standard && node ./bin/check-deps.js",
"test": "standard && depcheck --ignores=babel-cli,nodemon,gh-release --ignore-dirs=build,dist && node ./bin/extra-lint.js",
"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,22 +1,12 @@
const appConfig = require('application-config')('WebTorrent') var appConfig = require('application-config')('WebTorrent')
const fs = require('fs') var fs = require('fs')
const path = require('path') var path = require('path')
const electron = require('electron')
const APP_NAME = 'WebTorrent' var APP_NAME = 'WebTorrent'
const APP_TEAM = 'WebTorrent, LLC' var APP_TEAM = 'WebTorrent, LLC'
const APP_VERSION = require('../package.json').version var APP_VERSION = require('../package.json').version
const IS_TEST = isTest() var PORTABLE_PATH = path.join(path.dirname(process.execPath), 'Portable Settings')
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',
@@ -36,31 +26,26 @@ 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'
@@ -77,11 +62,8 @@ module.exports = {
HOME_PAGE_URL: 'https://webtorrent.io', HOME_PAGE_URL: 'https://webtorrent.io',
IS_PORTABLE: IS_PORTABLE, IS_PORTABLE: isPortable(),
IS_PRODUCTION: IS_PRODUCTION, IS_PRODUCTION: isProduction(),
IS_TEST: IS_TEST,
OS_SYSARCH: is64BitOperatingSystem() ? 'x64' : 'ia32',
POSTER_PATH: path.join(getConfigPath(), 'Posters'), POSTER_PATH: path.join(getConfigPath(), 'Posters'),
ROOT_PATH: path.join(__dirname, '..'), ROOT_PATH: path.join(__dirname, '..'),
@@ -97,7 +79,7 @@ module.exports = {
} }
function getConfigPath () { function getConfigPath () {
if (IS_PORTABLE) { if (isPortable()) {
return PORTABLE_PATH return PORTABLE_PATH
} else { } else {
return path.dirname(appConfig.filePath) return path.dirname(appConfig.filePath)
@@ -107,31 +89,22 @@ 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')
} }
}
function getPath (key) { var electron = require('electron')
if (process.type === 'renderer') {
return electron.remote.app.getPath(key)
} else {
return electron.app.getPath(key)
}
}
function isTest () { return process.type === 'renderer'
return process.env.NODE_ENV === 'test' ? electron.remote.app.getPath('downloads')
: electron.app.getPath('downloads')
} }
function isPortable () { function isPortable () {
if (IS_TEST) {
return true
}
try { try {
return process.platform === 'win32' && IS_PRODUCTION && !!fs.statSync(PORTABLE_PATH) return process.platform === 'win32' && isProduction() && !!fs.statSync(PORTABLE_PATH)
} catch (err) { } catch (err) {
return false return false
} }
@@ -151,32 +124,3 @@ function isProduction () {
return !/\/electron$/.test(process.execPath) return !/\/electron$/.test(process.execPath)
} }
} }
/**
* Returns the operating system's CPU architecture. This is different than
* `process.arch` which returns the architecture the binary was compiled for.
*
* On Windows, the most reliable way to detect a 64-bit OS from within a 32-bit
* app is based on the presence of a WOW64 file: %SystemRoot%\SysNative.
*
* Background: https://twitter.com/feross/status/776949077208510464
*/
function is64BitOperatingSystem () {
// This is a 64-bit binary, so the OS clearly supports 64-bit apps
if (process.arch === 'x64') return true
let useEnv = false
try {
useEnv = !!(process.env.SYSTEMROOT && fs.statSync(process.env.SYSTEMROOT))
} catch (err) {}
let sysRoot = useEnv ? process.env.SYSTEMROOT : 'C:\\Windows'
// If %SystemRoot%\SysNative exists, we are in a WOW64 FS Redirected application.
let isWOW64 = false
try {
isWOW64 = !!fs.statSync(path.join(sysRoot, 'sysnative'))
} catch (err) {}
return isWOW64
}

View File

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

View File

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

View File

@@ -6,10 +6,10 @@ module.exports = {
openFiles openFiles
} }
const electron = require('electron') var electron = require('electron')
const log = require('./log') var log = require('./log')
const windows = require('./windows') var windows = require('./windows')
/** /**
* Show open dialog to create a single-file torrent. * Show open dialog to create a single-file torrent.
@@ -17,11 +17,16 @@ const windows = require('./windows')
function openSeedFile () { function openSeedFile () {
if (!windows.main.win) return if (!windows.main.win) return
log('openSeedFile') log('openSeedFile')
const opts = { var opts = {
title: 'Select a file for the torrent.', title: 'Select a file for the torrent.',
properties: [ 'openFile' ] properties: [ 'openFile' ]
} }
showOpenSeed(opts) setTitle(opts.title)
electron.dialog.showOpenDialog(windows.main.win, opts, function (selectedPaths) {
resetTitle()
if (!Array.isArray(selectedPaths)) return
windows.main.dispatch('showCreateTorrent', selectedPaths)
})
} }
/* /*
@@ -32,7 +37,7 @@ function openSeedFile () {
function openSeedDirectory () { function openSeedDirectory () {
if (!windows.main.win) return if (!windows.main.win) return
log('openSeedDirectory') log('openSeedDirectory')
const opts = process.platform === 'darwin' var 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' ]
@@ -41,7 +46,12 @@ function openSeedDirectory () {
title: 'Select a folder for the torrent.', title: 'Select a folder for the torrent.',
properties: [ 'openDirectory' ] properties: [ 'openDirectory' ]
} }
showOpenSeed(opts) setTitle(opts.title)
electron.dialog.showOpenDialog(windows.main.win, opts, function (selectedPaths) {
resetTitle()
if (!Array.isArray(selectedPaths)) return
windows.main.dispatch('showCreateTorrent', selectedPaths)
})
} }
/* /*
@@ -51,7 +61,7 @@ function openSeedDirectory () {
function openFiles () { function openFiles () {
if (!windows.main.win) return if (!windows.main.win) return
log('openFiles') log('openFiles')
const opts = process.platform === 'darwin' var 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' ]
@@ -74,7 +84,7 @@ function openFiles () {
function openTorrentFile () { function openTorrentFile () {
if (!windows.main.win) return if (!windows.main.win) return
log('openTorrentFile') log('openTorrentFile')
const opts = { var 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' ]
@@ -109,16 +119,3 @@ function setTitle (title) {
function resetTitle () { function resetTitle () {
windows.main.dispatch('resetTitle') windows.main.dispatch('resetTitle')
} }
/**
* Pops up an Open File dialog with the given options.
* After the user selects files / folders, shows the Create Torrent page.
*/
function showOpenSeed (opts) {
setTitle(opts.title)
electron.dialog.showOpenDialog(windows.main.win, opts, function (selectedPaths) {
resetTitle()
if (!Array.isArray(selectedPaths)) return
windows.main.dispatch('showCreateTorrent', selectedPaths)
})
}

View File

@@ -4,17 +4,19 @@ module.exports = {
setBadge setBadge
} }
const {app, Menu} = require('electron') var electron = require('electron')
const dialog = require('./dialog') var app = electron.app
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
const menu = Menu.buildFromTemplate(getMenuTemplate()) var menu = electron.Menu.buildFromTemplate(getMenuTemplate())
app.dock.setMenu(menu) app.dock.setMenu(menu)
} }
@@ -31,11 +33,8 @@ 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) {
if (process.platform === 'darwin' || log(`setBadge: ${count}`)
process.platform === 'linux' && app.isUnityRunning()) { app.setBadgeCount(Number(count))
log(`setBadge: ${count}`)
app.setBadgeCount(Number(count))
}
} }
function getMenuTemplate () { function getMenuTemplate () {

View File

@@ -4,36 +4,29 @@ module.exports = {
checkInstall checkInstall
} }
const cp = require('child_process') var cp = require('child_process')
const path = require('path') var vlcCommand = require('vlc-command')
const vlcCommand = require('vlc-command')
const log = require('./log') var log = require('./log')
const windows = require('./windows') var 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
let proc = null var proc
function checkInstall (playerPath, cb) { function checkInstall (path, 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 (playerPath == null) return vlcCommand((err) => cb(!err)) if (path == null) return vlcCommand((err) => cb(!err))
process.nextTick(() => cb(true)) process.nextTick(() => cb(true))
} }
function spawn (playerPath, url, title) { function spawn (path, url, title) {
if (playerPath != null) return spawnExternal(playerPath, [url]) if (path != null) return spawnExternal(path, [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')
const args = [ var args = ['--play-and-exit', '--video-on-top', '--quiet', `--meta-title=${JSON.stringify(title)}`, url]
'--play-and-exit',
'--video-on-top',
'--quiet',
`--meta-title=${JSON.stringify(title)}`,
url
]
spawnExternal(vlcPath, args) spawnExternal(vlcPath, args)
}) })
} }
@@ -45,18 +38,13 @@ function kill () {
proc = null proc = null
} }
function spawnExternal (playerPath, args) { function spawnExternal (path, args) {
log('Running external media player:', playerPath + ' ' + args.join(' ')) log('Running external media player:', path + ' ' + args.join(' '))
if (path.extname(playerPath) === '.app') { proc = cp.spawn(path, args, {stdio: 'ignore'})
// 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
const closeModalTimeout = setTimeout(() => var 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
} }
const config = require('../config') var config = require('../config')
const path = require('path') var 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 () {
const electron = require('electron') var electron = require('electron')
const app = electron.app var 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 () {}
const EXEC_COMMAND = [ process.execPath ] var 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 () {
const Registry = require('winreg') var Registry = require('winreg')
const log = require('./log') var log = require('./log')
const iconPath = path.join( var 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) {
const protocolKey = new Registry({ var 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)
const iconKey = new Registry({ var 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)
const commandKey = new Registry({ var 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 () {
const extKey = new Registry({ var 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)
const idKey = new Registry({ var 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)
const iconKey = new Registry({ var 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)
const commandKey = new Registry({ var 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 () {
const Registry = require('winreg') var 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 () {
const commandKey = new Registry({ var 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 () {
const protocolKey = new Registry({ var 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 () {
const idKey = new Registry({ var 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 () {
const extKey = new Registry({ var 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 () {
const extKey = new Registry({ var 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 () {
const fs = require('fs-extra') var fs = require('fs-extra')
const os = require('os') var os = require('os')
const path = require('path') var path = require('path')
const config = require('../config') var config = require('../config')
const log = require('./log') var 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 () {
const templatePath = path.join( var 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)
const appPath = config.IS_PRODUCTION var 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)
const desktopFilePath = path.join( var desktopFilePath = path.join(
os.homedir(), os.homedir(),
'.local', '.local',
'share', 'share',
@@ -319,14 +319,14 @@ function installLinux () {
} }
function installIconFile () { function installIconFile () {
const iconStaticPath = path.join(config.STATIC_PATH, 'WebTorrent.png') var 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)
const iconFilePath = path.join( var iconFilePath = path.join(
os.homedir(), os.homedir(),
'.local', '.local',
'share', 'share',
@@ -341,11 +341,11 @@ function installLinux () {
} }
function uninstallLinux () { function uninstallLinux () {
const os = require('os') var os = require('os')
const path = require('path') var path = require('path')
const fs = require('fs-extra') var fs = require('fs-extra')
const desktopFilePath = path.join( var desktopFilePath = path.join(
os.homedir(), os.homedir(),
'.local', '.local',
'share', 'share',
@@ -354,7 +354,7 @@ function uninstallLinux () {
) )
fs.removeSync(desktopFilePath) fs.removeSync(desktopFilePath)
const iconFilePath = path.join( var iconFilePath = path.join(
os.homedir(), os.homedir(),
'.local', '.local',
'share', 'share',

View File

@@ -1,30 +1,26 @@
console.time('init') console.time('init')
const electron = require('electron') var electron = require('electron')
const app = electron.app
const ipcMain = electron.ipcMain
const announcement = require('./announcement') var app = electron.app
const config = require('../config') var ipcMain = electron.ipcMain
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')
let shouldQuit = false var announcement = require('./announcement')
let argv = sliceArgv(process.argv) var config = require('../config')
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')
// Start the app without showing the main window when auto launching on login var shouldQuit = false
// (On Windows and Linux, we get a flag. On MacOS, we get special API.) var argv = sliceArgv(process.argv)
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
@@ -55,7 +51,7 @@ function init () {
app.setPath('userData', config.CONFIG_PATH) app.setPath('userData', config.CONFIG_PATH)
} }
let isReady = false // app ready, windows can be created var 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
@@ -72,7 +68,7 @@ function init () {
app.on('ready', function () { app.on('ready', function () {
isReady = true isReady = true
windows.main.init({hidden: hidden}) windows.main.init()
windows.webtorrent.init() windows.webtorrent.init()
menu.init() menu.init()
@@ -82,7 +78,7 @@ function init () {
// Report uncaught exceptions // Report uncaught exceptions
process.on('uncaughtException', (err) => { process.on('uncaughtException', (err) => {
console.error(err) console.error(err)
const error = {message: err.message, stack: err.stack} var error = {message: err.message, stack: err.stack}
windows.main.dispatch('uncaughtError', 'main', error) windows.main.dispatch('uncaughtError', 'main', error)
}) })
}) })
@@ -147,18 +143,12 @@ function onAppOpen (newArgv) {
} }
} }
// Remove leading args.
// Production: 1 arg, eg: /Applications/WebTorrent.app/Contents/MacOS/WebTorrent
// Development: 2 args, eg: electron .
// Test: 4 args, eg: electron -r .../mocks.js .
function sliceArgv (argv) { function sliceArgv (argv) {
return argv.slice(config.IS_PRODUCTION ? 1 return argv.slice(config.IS_PRODUCTION ? 1 : 2)
: config.IS_TEST ? 4
: 2)
} }
function processArgv (argv) { function processArgv (argv) {
let torrentIds = [] var torrentIds = []
argv.forEach(function (arg) { argv.forEach(function (arg) {
if (arg === '-n') { if (arg === '-n') {
dialog.openSeedDirectory() dialog.openSeedDirectory()
@@ -166,15 +156,9 @@ 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,28 +2,27 @@ module.exports = {
init init
} }
const electron = require('electron') var electron = require('electron')
const app = electron.app var app = electron.app
const dialog = require('./dialog') var dialog = require('./dialog')
const dock = require('./dock') var dock = require('./dock')
const handlers = require('./handlers') var handlers = require('./handlers')
const log = require('./log') var log = require('./log')
const menu = require('./menu') var menu = require('./menu')
const powerSaveBlocker = require('./power-save-blocker') var powerSaveBlocker = require('./power-save-blocker')
const shell = require('./shell') var shell = require('./shell')
const shortcuts = require('./shortcuts') var shortcuts = require('./shortcuts')
const externalPlayer = require('./external-player') var externalPlayer = require('./external-player')
const windows = require('./windows') var windows = require('./windows')
const thumbar = require('./thumbar') var 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
const messageQueueMainToWebTorrent = [] var messageQueueMainToWebTorrent = []
function init () { function init () {
const ipc = electron.ipcMain var ipc = electron.ipcMain
ipc.once('ipcReady', function (e) { ipc.once('ipcReady', function (e) {
app.ipcReady = true app.ipcReady = true
@@ -59,19 +58,14 @@ function init () {
*/ */
ipc.on('onPlayerOpen', function () { ipc.on('onPlayerOpen', function () {
menu.togglePlaybackControls(true) menu.setPlayerOpen(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.togglePlaybackControls(false) menu.setPlayerOpen(false)
powerSaveBlocker.disable() powerSaveBlocker.disable()
shortcuts.disable() shortcuts.disable()
thumbar.disable() thumbar.disable()
@@ -98,26 +92,16 @@ function init () {
/** /**
* File handlers * File handlers
*/ */
ipc.on('setDefaultFileHandler', (e, flag) => { ipc.on('setDefaultFileHandler', (e, flag) => {
if (flag) handlers.install() if (flag) handlers.install()
else handlers.uninstall() else handlers.uninstall()
}) })
/**
* Auto start on login
*/
ipc.on('setStartup', (e, flag) => {
if (flag) startup.install()
else startup.uninstall()
})
/** /**
* Windows: Main * Windows: Main
*/ */
const main = windows.main var 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))
@@ -137,19 +121,11 @@ function init () {
}) })
}) })
ipc.on('openExternalPlayer', (e, ...args) => { ipc.on('openExternalPlayer', (e, ...args) => externalPlayer.spawn(...args))
menu.togglePlaybackControls(false)
thumbar.disable()
externalPlayer.spawn(...args)
})
ipc.on('quitExternalPlayer', () => externalPlayer.kill()) ipc.on('quitExternalPlayer', () => externalPlayer.kill())
/** // Capture all events
* Message passing 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.
*/ */
const electron = require('electron') var electron = require('electron')
const windows = require('./windows') var windows = require('./windows')
const app = electron.app var app = electron.app
function log (...args) { function log (...args) {
if (app.ipcReady) { if (app.ipcReady) {

View File

@@ -1,33 +1,30 @@
module.exports = { module.exports = {
init, init,
togglePlaybackControls, setPlayerOpen,
setWindowFocus, setWindowFocus,
setAllowNav, setAllowNav,
onPlayerUpdate,
onToggleAlwaysOnTop, onToggleAlwaysOnTop,
onToggleFullScreen onToggleFullScreen
} }
const electron = require('electron') var electron = require('electron')
const app = electron.app var app = electron.app
const config = require('../config') var config = require('../config')
const dialog = require('./dialog') var dialog = require('./dialog')
const shell = require('./shell') var shell = require('./shell')
const windows = require('./windows') var windows = require('./windows')
let menu = null var menu
function init () { function init () {
menu = electron.Menu.buildFromTemplate(getMenuTemplate()) menu = electron.Menu.buildFromTemplate(getMenuTemplate())
electron.Menu.setApplicationMenu(menu) electron.Menu.setApplicationMenu(menu)
} }
function togglePlaybackControls (flag) { function setPlayerOpen (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
@@ -35,16 +32,6 @@ function togglePlaybackControls (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) {
@@ -72,8 +59,8 @@ function onToggleFullScreen (flag) {
} }
function getMenuItem (label) { function getMenuItem (label) {
for (let i = 0; i < menu.items.length; i++) { for (var i = 0; i < menu.items.length; i++) {
const menuItem = menu.items[i].submenu.items.find(function (item) { var 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
@@ -81,7 +68,7 @@ function getMenuItem (label) {
} }
function getMenuTemplate () { function getMenuTemplate () {
const template = [ var template = [
{ {
label: 'File', label: 'File',
submenu: [ submenu: [
@@ -200,21 +187,6 @@ 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
} }
const electron = require('electron') var electron = require('electron')
const log = require('./log') var log = require('./log')
let blockId = 0 var 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
} }
const electron = require('electron') var electron = require('electron')
const log = require('./log') var 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
} }
const electron = require('electron') var electron = require('electron')
const windows = require('./windows') var 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,19 +12,9 @@ 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
} }
const cp = require('child_process') var cp = require('child_process')
const electron = require('electron') var electron = require('electron')
const fs = require('fs') var fs = require('fs')
const os = require('os') var os = require('os')
const path = require('path') var path = require('path')
const app = electron.app var app = electron.app
const handlers = require('./handlers') var handlers = require('./handlers')
const EXE_NAME = path.basename(process.execPath) var EXE_NAME = path.basename(process.execPath)
const UPDATE_EXE = path.join(process.execPath, '..', '..', 'Update.exe') var 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) {
let stdout = '' var stdout = ''
let error = null
let child = null var child
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) {
const homeDir = os.homedir() var homeDir = os.homedir()
if (homeDir) { if (homeDir) {
const desktopShortcutPath = path.join(homeDir, 'Desktop', 'WebTorrent.lnk') var 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) {
const desktopShortcutExists = !err var desktopShortcutExists = !err
createShortcuts(function () { createShortcuts(function () {
if (desktopShortcutExists) { if (desktopShortcutExists) {
cb() cb()

View File

@@ -1,36 +0,0 @@
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,8 +2,7 @@ module.exports = {
disable, disable,
enable, enable,
onPlayerPause, onPlayerPause,
onPlayerPlay, onPlayerPlay
onPlayerUpdate
} }
/** /**
@@ -12,80 +11,44 @@ module.exports = {
* or activating the window. * or activating the window.
*/ */
const path = require('path') var path = require('path')
const config = require('../config') var config = require('../config')
const windows = require('./windows') var 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 () {
buttons = [ update(false)
{
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 () {
buttons = [] windows.main.win.setThumbarButtons([])
update()
} }
function onPlayerPause () { function onPlayerPause () {
if (!isEnabled()) return update(true)
buttons[PLAY_PAUSE].tooltip = 'Play'
buttons[PLAY_PAUSE].icon = PLAY_ICON
update()
} }
function onPlayerPlay () { function onPlayerPlay () {
if (!isEnabled()) return update(false)
buttons[PLAY_PAUSE].tooltip = 'Pause'
buttons[PLAY_PAUSE].icon = PAUSE_ICON
update()
} }
function onPlayerUpdate (state) { function update (isPaused) {
if (!isEnabled()) return var icon = isPaused
buttons[PREV].flags = [ state.hasPrevious ? 'enabled' : 'disabled' ] ? 'PlayThumbnailBarButton.png'
buttons[NEXT].flags = [ state.hasNext ? 'enabled' : 'disabled' ] : 'PauseThumbnailBarButton.png'
update()
}
function isEnabled () { var buttons = [
return buttons.length > 0 {
} tooltip: isPaused ? 'Play' : 'Pause',
icon: path.join(config.STATIC_PATH, icon),
function update () { click: () => windows.main.dispatch('playPause')
}
]
windows.main.win.setThumbarButtons(buttons) windows.main.win.setThumbarButtons(buttons)
} }

View File

@@ -4,14 +4,14 @@ module.exports = {
setWindowFocus setWindowFocus
} }
const electron = require('electron') var electron = require('electron')
const app = electron.app var app = electron.app
const config = require('../config') var config = require('../config')
const windows = require('./windows') var windows = require('./windows')
let tray var 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) {
const cp = require('child_process') var 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 () {
const contextMenu = electron.Menu.buildFromTemplate(getMenuTemplate()) var contextMenu = electron.Menu.buildFromTemplate(getMenuTemplate())
tray.setContextMenu(contextMenu) tray.setContextMenu(contextMenu)
} }

View File

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

View File

@@ -2,9 +2,9 @@ module.exports = {
init init
} }
const electron = require('electron') var electron = require('electron')
const app = electron.app var 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 @@
const about = module.exports = { var about = module.exports = {
init, init,
win: null win: null
} }
const config = require('../../config') var config = require('../../config')
const electron = require('electron') var electron = require('electron')
function init () { function init () {
if (about.win) { if (about.win) {
return about.win.show() return about.win.show()
} }
const win = about.win = new electron.BrowserWindow({ var win = about.win = new electron.BrowserWindow({
backgroundColor: '#ECECEC', backgroundColor: '#ECECEC',
center: true, center: true,
fullscreen: false, fullscreen: false,

View File

@@ -1,4 +1,4 @@
const main = module.exports = { var main = module.exports = {
dispatch, dispatch,
hide, hide,
init, init,
@@ -14,23 +14,23 @@ const main = module.exports = {
win: null win: null
} }
const electron = require('electron') var electron = require('electron')
const app = electron.app var app = electron.app
const config = require('../../config') var config = require('../../config')
const log = require('../log') var log = require('../log')
const menu = require('../menu') var menu = require('../menu')
const tray = require('../tray') var tray = require('../tray')
const HEADER_HEIGHT = 38 var HEADER_HEIGHT = 38
const TORRENT_HEIGHT = 100 var TORRENT_HEIGHT = 100
function init (options) { function init () {
if (main.win) { if (main.win) {
return main.win.show() return main.win.show()
} }
const win = main.win = new electron.BrowserWindow({ var 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,8 +40,7 @@ function init (options) {
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)
@@ -115,7 +114,7 @@ function setBounds (bounds, maximize) {
} }
// Maximize or minimize, if the second argument is present // Maximize or minimize, if the second argument is present
let willBeMaximized var willBeMaximized
if (maximize === true) { if (maximize === true) {
if (!main.win.isMaximized()) { if (!main.win.isMaximized()) {
log('setBounds: maximizing') log('setBounds: maximizing')
@@ -137,7 +136,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
const scr = electron.screen.getDisplayMatching(main.win.getBounds()) var 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 @@
const webtorrent = module.exports = { var webtorrent = module.exports = {
init, init,
send, send,
show, show,
@@ -6,13 +6,13 @@ const webtorrent = module.exports = {
win: null win: null
} }
const electron = require('electron') var electron = require('electron')
const config = require('../../config') var config = require('../../config')
const log = require('../log') var log = require('../log')
function init () { function init () {
const win = webtorrent.win = new electron.BrowserWindow({ var win = webtorrent.win = new electron.BrowserWindow({
backgroundColor: '#1E1E1E', backgroundColor: '#1E1E1E',
center: true, center: true,
fullscreen: false, fullscreen: false,

View File

@@ -17,14 +17,15 @@ 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 style={style}> <HeadingTag
style={{
color: colors.grey100,
fontSize: 20,
marginBottom: 15,
marginTop: 30
}}
>
{this.props.children} {this.props.children}
</HeadingTag> </HeadingTag>
) )

View File

@@ -7,9 +7,6 @@ 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 {
@@ -29,7 +26,7 @@ class PathSelector extends React.Component {
} }
handleClick () { handleClick () {
const opts = Object.assign({ var opts = Object.assign({
defaultPath: this.props.value, defaultPath: this.props.value,
properties: [ 'openFile', 'openDirectory' ] properties: [ 'openFile', 'openDirectory' ]
}, this.props.dialog) }, this.props.dialog)
@@ -46,38 +43,48 @@ 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 className={this.props.className} style={wrapperStyle}> <div
<div className='label' style={labelStyle}> className={this.props.className}
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 className='control' disabled id={id} value={text} <TextField
inputStyle={textareaStyle} style={textFieldStyle} /> className='control'
<RaisedButton className='control' label='Change' onClick={this.handleClick} disabled
style={buttonStyle} /> id={id}
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,6 +1,6 @@
const React = require('react') const React = require('react')
const RaisedButton = require('material-ui/RaisedButton').default const FlatButton = require('material-ui/FlatButton').default
class ShowMore extends React.Component { class ShowMore extends React.Component {
static get propTypes () { static get propTypes () {
@@ -39,12 +39,14 @@ class ShowMore extends React.Component {
? this.props.hideLabel ? this.props.hideLabel
: this.props.showLabel : this.props.showLabel
return ( return (
<div className='show-more' style={this.props.style}> <div
style={this.props.style}
>
{this.state.expanded ? this.props.children : null} {this.state.expanded ? this.props.children : null}
<RaisedButton <FlatButton
className='control'
onClick={this.handleClick} onClick={this.handleClick}
label={label} /> label={label}
/>
</div> </div>
) )
} }

View File

@@ -4,9 +4,9 @@ const {dispatcher} = require('../lib/dispatcher')
class Header extends React.Component { class Header extends React.Component {
render () { render () {
const loc = this.props.state.location var loc = this.props.state.location
return ( return (
<div className='header' onMouseMove={dispatcher('mediaMouseMoved')}> <div className='header'>
{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
const state = this.props.state var state = this.props.state
return (<div className='title ellipsis'>{state.window.title}</div>) return (<div className='title ellipsis'>{state.window.title}</div>)
} }
getAddButton () { getAddButton () {
const state = this.props.state var state = this.props.state
if (state.location.url() !== 'home') return null if (state.location.url() !== 'home') return null
return ( return (
<i <i

View File

@@ -1,24 +0,0 @@
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,41 +1,32 @@
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>
<div> <p>
<TextField <input id='add-torrent-url' type='text' onKeyPress={handleKeyPress} />
id='torrent-address-field' </p>
className='control' <p className='float-right'>
ref={(c) => { this.torrentURL = c }} <button className='button button-flat' onClick={dispatcher('exitModal')}>Cancel</button>
fullWidth <button className='button button-raised' onClick={handleOK}>OK</button>
onKeyDown={handleKeyDown.bind(this)} /> </p>
</div> <script>document.querySelector('#add-torrent-url').focus()</script>
<ModalOKCancel
cancelText='CANCEL'
onCancel={dispatcher('exitModal')}
okText='OK'
onOK={handleOK.bind(this)} />
</div> </div>
) )
} }
componentDidMount () {
this.torrentURL.input.focus()
}
} }
function handleKeyDown (e) { function handleKeyPress (e) {
if (e.which === 13) handleOK.call(this) /* hit Enter to submit */ if (e.which === 13) handleOK() /* hit Enter to submit */
} }
function handleOK () { function handleOK () {
dispatch('exitModal') dispatch('exitModal')
dispatch('addTorrent', this.torrentURL.input.value) // TODO: dcposch use React refs instead
dispatch('addTorrent', document.querySelector('#add-torrent-url').value)
} }

View File

@@ -1,24 +1,22 @@
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 () {
const state = this.props.state var state = this.props.state
const message = state.modal.deleteData var 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?'
const buttonText = state.modal.deleteData ? 'REMOVE DATA' : 'REMOVE' var buttonText = state.modal.deleteData ? 'Remove Data' : 'Remove'
return ( return (
<div> <div>
<p><strong>{message}</strong></p> <p><strong>{message}</strong></p>
<ModalOKCancel <p className='float-right'>
cancelText='CANCEL' <button className='button button-flat' onClick={dispatcher('exitModal')}>Cancel</button>
onCancel={dispatcher('exitModal')} <button className='button button-raised' onClick={handleRemove}>{buttonText}</button>
okText={buttonText} </p>
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 () {
const state = this.props.state var state = this.props.state
const err = state.modal.error var err = state.modal.error
const message = (err && err.getMessage) var message = (err && err.getMessage)
? err.getMessage() ? err.getMessage()
: err : err
const onAction = state.modal.externalPlayerInstalled var playerPath = state.saved.prefs.externalPlayerPath
? dispatcher('openExternalPlayer') var playerName = playerPath
: () => this.onInstall() ? path.basename(playerPath).split('.')[0]
const actionText = state.modal.externalPlayerInstalled : 'VLC'
? 'PLAY IN ' + state.getExternalPlayerName().toUpperCase() var actionButton = state.modal.externalPlayerInstalled
: 'INSTALL VLC' ? (<button className='button-raised' onClick={dispatcher('openExternalPlayer')}>Play in {playerName}</button>)
const errorMessage = state.modal.externalPlayerNotFound : (<button className='button-raised' onClick={() => this.onInstall}>Install VLC</button>)
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>
<ModalOKCancel <p className='float-right'>
cancelText='CANCEL' <button className='button-flat' onClick={dispatcher('backToList')}>Cancel</button>
onCancel={dispatcher('backToList')} {actionButton}
okText={actionText} </p>
onOK={onAction} /> <p className='error-text'>{playerMessage}</p>
<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
const state = this.props.state var 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,24 +1,19 @@
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 () {
const state = this.props.state var 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> <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>
We have an auto-updater for Windows and Mac. <p className='float-right'>
We don't have one for Linux yet, so you'll have to download the new version manually. <button className='button button-flat' onClick={handleSkip}>Skip This Release</button>
<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,8 +2,6 @@ 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 {
@@ -20,7 +18,7 @@ module.exports = class MediaController {
} }
mediaError (error) { mediaError (error) {
const state = this.state var 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'
@@ -45,17 +43,13 @@ module.exports = class MediaController {
} }
openExternalPlayer () { openExternalPlayer () {
const state = this.state var state = this.state
const mediaURL = Playlist.getCurrentLocalURL(this.state) ipcRenderer.send('openExternalPlayer', state.saved.prefs.externalPlayerPath, state.server.localURL, state.window.title)
ipcRenderer.send('openExternalPlayer',
state.saved.prefs.externalPlayerPath,
mediaURL,
state.window.title)
state.playing.location = 'external' state.playing.location = 'external'
} }
externalPlayerNotFound () { externalPlayerNotFound () {
const modal = this.state.modal var 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,12 +4,10 @@ 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 {UnplayableFileError, UnplayableTorrentError, const errors = require('../lib/errors')
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
@@ -26,43 +24,24 @@ 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, restore the most recently viewed file or autoplay the first // * If no file index is provided, pick the default file to play
playFile (infoHash, index /* optional */) { playFile (infoHash, index /* optional */) {
const state = this.state this.state.location.go({
if (state.location.url() === 'player') { url: 'player',
this.updatePlayer(infoHash, index, false, (err) => { setup: (cb) => {
if (err) dispatch('error', err) this.play()
else this.play() this.openPlayer(infoHash, index, cb)
}) },
} else { destroy: () => this.closePlayer()
let initialized = false }, (err) => {
state.location.go({ if (err) dispatch('error', err)
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) {
const torrentSummary = TorrentSummary.getByKey(this.state, infoHash) var torrentSummary = TorrentSummary.getByKey(this.state, infoHash)
const filePath = path.join( var filePath = path.join(
torrentSummary.path, torrentSummary.path,
torrentSummary.files[index].path) torrentSummary.files[index].path)
ipcRenderer.send('openItem', filePath) ipcRenderer.send('openItem', filePath)
@@ -70,12 +49,12 @@ module.exports = class PlaybackController {
// Toggle (play or pause) the currently playing media // Toggle (play or pause) the currently playing media
playPause () { playPause () {
const state = this.state var 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
const mediaTag = document.querySelector('video,audio') var 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()
@@ -85,33 +64,9 @@ 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 () {
const state = this.state var 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)) {
@@ -122,7 +77,7 @@ module.exports = class PlaybackController {
// Pause the currently playing media // Pause the currently playing media
pause () { pause () {
const state = this.state var 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)) {
@@ -151,22 +106,19 @@ 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) {
const state = this.state var state = this.state
let rate = state.playing.playbackRate var 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) { } else if (direction < 0 && rate === 0.25) { /* when we set playback rate at 0 in html 5, playback hangs ;( */
// 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) || } else if ((direction > 0 && rate >= 1 && rate < 16) || (direction < 0 && rate > -16 && rate <= -1)) {
(direction < 0 && rate > -16 && rate <= -1)) {
rate *= 2 rate *= 2
} else if ((direction < 0 && rate > 1 && rate <= 16) || } else if ((direction < 0 && rate > 1 && rate <= 16) || (direction > 0 && rate >= -16 && rate < -1)) {
(direction > 0 && rate >= -16 && rate < -1)) {
rate /= 2 rate /= 2
} }
state.playing.playbackRate = rate state.playing.playbackRate = rate
@@ -187,7 +139,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))
const state = this.state var state = this.state
if (isCasting(state)) { if (isCasting(state)) {
Cast.setVolume(volume) Cast.setVolume(volume)
} else { } else {
@@ -201,8 +153,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 () {
const state = this.state var state = this.state
const hideControls = state.location.url() === 'player' && var 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 &&
@@ -217,10 +169,12 @@ 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) {
const state = this.state var torrentSummary = TorrentSummary.getByKey(this.state, infoHash)
const torrentSummary = TorrentSummary.getByKey(state, infoHash)
state.playing.infoHash = torrentSummary.infoHash // automatically choose which file in the torrent to play, if necessary
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')
@@ -228,78 +182,43 @@ module.exports = class PlaybackController {
torrentSummary.playStatus = 'requested' torrentSummary.playStatus = 'requested'
this.update() this.update()
const timeout = setTimeout(() => { var 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 PlaybackTimedOutError()) cb(new Error('Playback timed out. Try again.'))
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,
() => onTorrentReady()) () => this.openPlayerFromActiveTorrent(torrentSummary, index, timeout, cb))
} else { } else {
onTorrentReady() this.openPlayerFromActiveTorrent(torrentSummary, index, timeout, cb)
}
function onTorrentReady () {
ipcRenderer.send('wt-start-server', torrentSummary.infoHash)
ipcRenderer.once('wt-server-' + torrentSummary.infoHash, () => cb())
} }
} }
// Called each time the current file changes openPlayerFromActiveTorrent (torrentSummary, index, timeout, cb) {
updatePlayer (infoHash, index, resume, cb) { var fileSummary = torrentSummary.files[index]
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
state.playing.infoHash = infoHash var state = this.state
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
let jumpToTime = 0 if (fileSummary.currentTime) {
if (resume && fileSummary.currentTime) { var fraction = fileSummary.currentTime / fileSummary.duration
const fraction = fileSummary.currentTime / fileSummary.duration var secondsLeft = fileSummary.duration - fileSummary.currentTime
const secondsLeft = fileSummary.duration - fileSummary.currentTime
if (fraction < 0.9 && secondsLeft > 10) { if (fraction < 0.9 && secondsLeft > 10) {
jumpToTime = fileSummary.currentTime state.playing.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) {
@@ -314,28 +233,41 @@ module.exports = class PlaybackController {
dispatch('addSubtitles', [fileSummary.selectedSubtitle], true) dispatch('addSubtitles', [fileSummary.selectedSubtitle], true)
} }
state.window.title = fileSummary.name ipcRenderer.send('wt-start-server', torrentSummary.infoHash, index)
ipcRenderer.once('wt-server-' + torrentSummary.infoHash, (e, info) => {
clearTimeout(timeout)
// play in VLC if set as default player (Preferences / Playback / Play in VLC) // if we timed out (user clicked play a long time ago), don't autoplay
if (this.state.saved.prefs.openExternalPlayer) { var timedOut = torrentSummary.playStatus === 'timeout'
dispatch('openExternalPlayer') delete torrentSummary.playStatus
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
const state = this.state var state = this.state
if (isCasting(state)) { if (isCasting(state)) {
Cast.stop() Cast.stop()
} }
@@ -347,10 +279,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
const result = state.playing.result // 'success' or 'error' var 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 gave up waiting else if (result === undefined) telemetry.logPlayAttempt('abandoned') // user exited before first frame
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,15 +11,13 @@ module.exports = class PrefsController {
// Goes to the Preferences screen // Goes to the Preferences screen
show () { show () {
const state = this.state var 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 || {}, { state.unsaved = Object.assign(state.unsaved || {}, {prefs: state.saved.prefs || {}})
prefs: Object.assign({}, state.saved.prefs)
})
ipcRenderer.send('setAllowNav', false) ipcRenderer.send('setAllowNav', false)
cb() cb()
}, },
@@ -34,27 +32,23 @@ 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) {
const path = property.split('.') var path = property.split('.')
let obj = this.state.unsaved.prefs var key = this.state.unsaved.prefs
let i for (var i = 0; i < path.length - 1; i++) {
for (i = 0; i < path.length - 1; i++) { if (typeof key[path[i]] === 'undefined') {
if (typeof obj[path[i]] === 'undefined') { key[path[i]] = {}
obj[path[i]] = {}
} }
obj = obj[path[i]] key = key[path[i]]
} }
obj[path[i]] = value key[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 () {
const state = this.state var 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

@@ -2,8 +2,10 @@ const electron = require('electron')
const fs = require('fs-extra') const fs = require('fs-extra')
const path = require('path') const path = require('path')
const parallel = require('run-parallel') const parallel = require('run-parallel')
const zeroFill = require('zero-fill')
const remote = electron.remote const remote = electron.remote
const ipcRenderer = electron.ipcRenderer
const {dispatch} = require('../lib/dispatcher') const {dispatch} = require('../lib/dispatcher')
@@ -28,35 +30,36 @@ module.exports = class SubtitlesController {
} }
toggleSubtitlesMenu () { toggleSubtitlesMenu () {
const subtitles = this.state.playing.subtitles var 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 (this.state.playing.type !== 'video') return if (state.playing.type !== 'video') return
if (files.length === 0) return if (files.length === 0) return
const subtitles = this.state.playing.subtitles var subtitles = state.playing.subtitles
// Read the files concurrently, then add all resulting subtitle tracks // Read the files concurrently, then add all resulting subtitle tracks
const tasks = files.map((file) => (cb) => loadSubtitle(file, cb)) var 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 (let i = 0; i < tracks.length; i++) { for (var i = 0; i < tracks.length; i++) {
// No dupes allowed // No dupes allowed
const track = tracks[i] var track = tracks[i]
let trackIndex = subtitles.tracks.findIndex((t) => var trackIndex = state.playing.subtitles.tracks
track.filePath === t.filePath) .findIndex((t) => track.filePath === t.filePath)
// Add the track // Add the track
if (trackIndex === -1) { if (trackIndex === -1) {
trackIndex = subtitles.tracks.push(track) - 1 trackIndex = state.playing.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))) {
subtitles.selectedIndex = trackIndex state.playing.subtitles.selectedIndex = trackIndex
} }
} }
@@ -67,46 +70,62 @@ module.exports = class SubtitlesController {
checkForSubtitles () { checkForSubtitles () {
if (this.state.playing.type !== 'video') return if (this.state.playing.type !== 'video') return
const torrentSummary = this.state.getPlayingTorrentSummary() var 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
const file = torrentSummary.files[ix] var file = torrentSummary.files[ix]
if (this.state.playing.fileIndex === ix) return this.checkForEmbeddedMKVSubtitles(file)
if (!this.isSubtitle(file.name)) return if (!this.isSubtitle(file.name)) return
const filePath = path.join(torrentSummary.path, file.path) var filePath = path.join(torrentSummary.path, file.path)
this.addSubtitles([filePath], false) this.addSubtitles([filePath], false)
}) })
} }
isSubtitle (file) { isSubtitle (file) {
const name = typeof file === 'string' ? file : file.name var name = typeof file === 'string' ? file : file.name
const ext = path.extname(name).toLowerCase() var ext = path.extname(name).toLowerCase()
return ext === '.srt' || ext === '.vtt' return ext === '.srt' || ext === '.vtt'
} }
checkForEmbeddedMKVSubtitles (file) {
var playing = this.state.playing
// var playingFile = this.state.getPlayingFileSummary()
// var playingPath = path.join(torrentSummary.path, playingFile.path)
if (path.extname(file.name).toLowerCase() === '.mkv') {
ipcRenderer.send('wt-get-mkv-subtitles', playing.infoHash, playing.fileIndex)
ipcRenderer.once('wt-mkv-subtitles', function (e, tracks) {
tracks.forEach(function (trackEntry) {
var track = loadEmbeddedSubtitle(trackEntry)
console.log('loaded emb subs', track)
playing.subtitles.tracks.push(track)
})
if (tracks.length > 0) relabelSubtitles(playing.subtitles)
})
}
}
} }
function loadSubtitle (file, cb) { function loadSubtitle (file, cb) {
// Lazy load to keep startup fast // Lazy load to keep startup fast
const concat = require('simple-concat') var concat = require('simple-concat')
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
const filePath = file.path || file var filePath = file.path || file
const vttStream = fs.createReadStream(filePath).pipe(srtToVtt()) var 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 var langDetected = detectVTTLanguage(buf)
const vttContents = buf.toString().replace(/(.*-->.*)/g, '')
let langDetected = (new LanguageDetect()).detect(vttContents, 2)
langDetected = langDetected.length ? langDetected[0][0] : 'subtitle'
langDetected = langDetected.slice(0, 1).toUpperCase() + langDetected.slice(1)
const track = { var 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,
@@ -120,19 +139,65 @@ 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) {
const iso639 = require('iso-639-1') var iso639 = require('iso-639-1')
const osLangISO = window.navigator.language.split('-')[0] // eg 'en' var osLangISO = window.navigator.language.split('-')[0] // eg 'en'
const langIso = iso639.getCode(language) // eg 'de' if language is 'German' var 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) {
const counts = {} var counts = {}
subtitles.tracks.forEach(function (track) { subtitles.tracks.forEach(function (track) {
const lang = track.language var 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
}) })
} }
function detectVTTLanguage (buffer) {
var LanguageDetect = require('languagedetect')
// Detect what language the subtitles are in
var vttContents = buffer.toString().replace(/(.*-->.*)/g, '') // remove numbers?
var langDetected = (new LanguageDetect()).detect(vttContents, 2)
langDetected = langDetected.length ? langDetected[0][0] : 'subtitle'
langDetected = langDetected.slice(0, 1).toUpperCase() + langDetected.slice(1)
return langDetected
}
function loadEmbeddedSubtitle (trackEntry) {
// convert to .vtt format
var vtt = 'WEBVTT FILE\r\n\r\n'
trackEntry.subtitles.forEach(function (sub, i) {
vtt += `${i + 1}\r\n`
vtt += `${msToTime(sub.time)} --> ${msToTime(sub.time + sub.duration)}\r\n`
vtt += `${sub.text}\r\n\r\n`
})
function msToTime (s) {
var ms = s % 1000
s = (s - ms) / 1000
var secs = s % 60
s = (s - secs) / 60
var mins = s % 60
var hrs = (s - mins) / 60
var z = zeroFill
return z(2, hrs) + ':' + z(2, mins) + ':' + z(2, secs) + '.' + z(3, ms)
}
var buf = new Buffer(vtt)
var langDetected = detectVTTLanguage(buf)
var track = {
buffer: 'data:text/vtt;base64,' + buf.toString('base64'),
language: langDetected,
label: langDetected,
filePath: null
}
return track
}

View File

@@ -2,6 +2,7 @@ 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')
@@ -11,12 +12,12 @@ module.exports = class TorrentController {
} }
torrentInfoHash (torrentKey, infoHash) { torrentInfoHash (torrentKey, infoHash) {
let torrentSummary = this.getTorrentSummary(torrentKey) var 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) {
const torrents = this.state.saved.torrents var 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)) {
@@ -48,7 +49,7 @@ module.exports = class TorrentController {
} }
dispatch('error', message) dispatch('error', message)
const torrentSummary = this.getTorrentSummary(torrentKey) var 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'
@@ -58,20 +59,21 @@ module.exports = class TorrentController {
torrentMetadata (torrentKey, torrentInfo) { torrentMetadata (torrentKey, torrentInfo) {
// Summarize torrent // Summarize torrent
const torrentSummary = this.getTorrentSummary(torrentKey) var 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:
const hasDetailedFileInfo = torrentSummary.files && torrentSummary.files[0].path var 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
@@ -83,7 +85,7 @@ module.exports = class TorrentController {
torrentDone (torrentKey, torrentInfo) { torrentDone (torrentKey, torrentInfo) {
// Update the torrent summary // Update the torrent summary
const torrentSummary = this.getTorrentSummary(torrentKey) var 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.
@@ -100,18 +102,22 @@ module.exports = class TorrentController {
} }
torrentProgress (progressInfo) { torrentProgress (progressInfo) {
// Overall progress across all active torrents, 0 to 1, or -1 to hide the progress bar // Overall progress across all active torrents, 0 to 1
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%
const progress = (!progressInfo.hasActiveTorrents || progressInfo.progress === 1) // TODO: isn't this equivalent to: if (progress === 1) ?
? -1 if (!hasActiveTorrents || progress === 1) {
: progressInfo.progress progress = -1
}
// 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) => {
const torrentSummary = this.getTorrentSummary(p.torrentKey) var 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
@@ -126,28 +132,27 @@ module.exports = class TorrentController {
} }
torrentFileModtimes (torrentKey, fileModtimes) { torrentFileModtimes (torrentKey, fileModtimes) {
const torrentSummary = this.getTorrentSummary(torrentKey) var 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)
const torrentSummary = this.getTorrentSummary(torrentKey) var torrentSummary = this.getTorrentSummary(torrentKey)
torrentSummary.torrentFileName = torrentFileName torrentSummary.torrentFileName = torrentFileName
dispatch('saveStateThrottled') dispatch('saveStateThrottled')
} }
torrentPosterSaved (torrentKey, posterFileName) { torrentPosterSaved (torrentKey, posterFileName) {
const torrentSummary = this.getTorrentSummary(torrentKey) var torrentSummary = this.getTorrentSummary(torrentKey)
torrentSummary.posterFileName = posterFileName torrentSummary.posterFileName = posterFileName
dispatch('saveStateThrottled') dispatch('saveStateThrottled')
} }
torrentAudioMetadata (infoHash, index, info) { torrentAudioMetadata (infoHash, index, info) {
const torrentSummary = this.getTorrentSummary(infoHash) var torrentSummary = this.getTorrentSummary(infoHash)
const fileSummary = torrentSummary.files[index] var fileSummary = torrentSummary.files[index]
fileSummary.audioInfo = info fileSummary.audioInfo = info
dispatch('update') dispatch('update')
} }
@@ -164,7 +169,7 @@ module.exports = class TorrentController {
} }
function getTorrentPath (torrentSummary) { function getTorrentPath (torrentSummary) {
let itemPath = TorrentSummary.getFileOrFolder(torrentSummary) var itemPath = TorrentSummary.getFileOrFolder(torrentSummary)
if (torrentSummary.files.length > 1) { if (torrentSummary.files.length > 1) {
itemPath = path.dirname(itemPath) itemPath = path.dirname(itemPath)
} }
@@ -172,12 +177,12 @@ function getTorrentPath (torrentSummary) {
} }
function showDoneNotification (torrent) { function showDoneNotification (torrent) {
const notif = new window.Notification('Download Complete', { var 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,7 +3,6 @@ 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')
@@ -18,8 +17,8 @@ module.exports = class TorrentListController {
this.state = state this.state = state
} }
// Adds a torrent to the list, starts downloading/seeding. // Adds a torrent to the list, starts downloading/seeding. TorrentID can be a
// TorrentID can be a magnet URI, infohash, or torrent file: https://git.io/vik9M // magnet URI, infohash, or torrent file: https://github.com/feross/webtorrent#clientaddtorrentid-opts-function-ontorrent-torrent-
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
@@ -31,8 +30,8 @@ module.exports = class TorrentListController {
torrentId = torrentId.slice(torrentId.indexOf('#') + 1) torrentId = torrentId.slice(torrentId.indexOf('#') + 1)
} }
const torrentKey = this.state.nextTorrentKey++ var torrentKey = this.state.nextTorrentKey++
const path = this.state.saved.prefs.downloadPath var path = this.state.saved.prefs.downloadPath
ipcRenderer.send('wt-start-torrenting', torrentKey, torrentId, path) ipcRenderer.send('wt-start-torrenting', torrentKey, torrentId, path)
@@ -67,16 +66,16 @@ module.exports = class TorrentListController {
// Creates a new torrent and start seeeding // Creates a new torrent and start seeeding
createTorrent (options) { createTorrent (options) {
const state = this.state var state = this.state
const torrentKey = state.nextTorrentKey++ var 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) {
const s = TorrentSummary.getByKey(this.state, torrentKey) var s = TorrentSummary.getByKey(this.state, torrentKey)
if (!s) throw new TorrentKeyNotFoundError(torrentKey) if (!s) throw new Error('Missing key: ' + torrentKey)
// New torrent: give it a path // New torrent: give it a path
if (!s.path) { if (!s.path) {
@@ -85,13 +84,8 @@ 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(fileOrFolder, function (err) { fs.stat(TorrentSummary.getFileOrFolder(s), function (err) {
if (err) { if (err) {
s.error = 'path-missing' s.error = 'path-missing'
return return
@@ -111,7 +105,7 @@ module.exports = class TorrentListController {
// TODO: use torrentKey, not infoHash // TODO: use torrentKey, not infoHash
toggleTorrent (infoHash) { toggleTorrent (infoHash) {
const torrentSummary = TorrentSummary.getByKey(this.state, infoHash) var 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)
@@ -124,13 +118,11 @@ module.exports = class TorrentListController {
} }
toggleTorrentFile (infoHash, index) { toggleTorrentFile (infoHash, index) {
const torrentSummary = TorrentSummary.getByKey(this.state, infoHash) var 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
if (torrentSummary.status !== 'paused') { ipcRenderer.send('wt-select-files', infoHash, torrentSummary.selections)
ipcRenderer.send('wt-select-files', infoHash, torrentSummary.selections)
}
} }
confirmDeleteTorrent (infoHash, deleteData) { confirmDeleteTorrent (infoHash, deleteData) {
@@ -145,14 +137,14 @@ module.exports = class TorrentListController {
deleteTorrent (infoHash, deleteData) { deleteTorrent (infoHash, deleteData) {
ipcRenderer.send('wt-stop-torrenting', infoHash) ipcRenderer.send('wt-stop-torrenting', infoHash)
const index = this.state.saved.torrents.findIndex((x) => x.infoHash === infoHash) var index = this.state.saved.torrents.findIndex((x) => x.infoHash === infoHash)
if (index > -1) { if (index > -1) {
const summary = this.state.saved.torrents[index] var 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)) deleteFile(TorrentSummary.getPosterPath(summary)) // TODO: will the css path hack affect windows?
// optionally delete the torrent data // optionally delete the torrent data
if (deleteData) moveItemToTrash(summary) if (deleteData) moveItemToTrash(summary)
@@ -162,8 +154,7 @@ module.exports = class TorrentListController {
State.saveThrottled(this.state) State.saveThrottled(this.state)
} }
// prevent user from going forward to a deleted torrent this.state.location.clearForward('player') // prevent user from going forward to a deleted torrent
this.state.location.clearForward('player')
sound.play('DELETE') sound.play('DELETE')
} }
@@ -176,8 +167,8 @@ module.exports = class TorrentListController {
} }
openTorrentContextMenu (infoHash) { openTorrentContextMenu (infoHash) {
const torrentSummary = TorrentSummary.getByKey(this.state, infoHash) var torrentSummary = TorrentSummary.getByKey(this.state, infoHash)
const menu = new electron.remote.Menu() var 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',
@@ -215,52 +206,22 @@ module.exports = class TorrentListController {
menu.append(new electron.remote.MenuItem({ menu.append(new electron.remote.MenuItem({
label: 'Save Torrent File As...', label: 'Save Torrent File As...',
click: () => dispatch('saveTorrentFileAs', torrentSummary.torrentKey) click: () => saveTorrentFileAs(torrentSummary)
})) }))
menu.popup(electron.remote.getCurrentWindow()) menu.popup(electron.remote.getCurrentWindow())
} }
// Takes a torrentSummary or torrentKey
// Shows a Save File dialog, then saves the .torrent file wherever the user requests
saveTorrentFileAs (torrentKey) {
const torrentSummary = TorrentSummary.getByKey(this.state, torrentKey)
if (!torrentSummary) throw new Error('Missing torrentKey: ' + torrentKey)
const downloadPath = this.state.saved.prefs.downloadPath
const newFileName = path.parse(torrentSummary.name).name + '.torrent'
const win = electron.remote.getCurrentWindow()
const opts = {
title: 'Save Torrent File',
defaultPath: path.join(downloadPath, newFileName),
filters: [
{ name: 'Torrent Files', extensions: ['torrent'] },
{ name: 'All Files', extensions: ['*'] }
]
}
electron.remote.dialog.showSaveDialog(win, opts, function (savePath) {
console.log('Saving torrent ' + torrentKey + ' to ' + savePath)
if (!savePath) return // They clicked Cancel
const torrentPath = TorrentSummary.getTorrentPath(torrentSummary)
fs.readFile(torrentPath, function (err, torrentFile) {
if (err) return dispatch('error', err)
fs.writeFile(savePath, torrentFile, function (err) {
if (err) return dispatch('error', err)
})
})
})
}
} }
// Recursively finds {name, path, size} for all files in a folder // Recursively finds {name, path, size} for all files in a folder
// 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) {
let numComplete = 0 var numComplete = 0
let ret = [] var ret = []
paths.forEach(function (path) { paths.forEach(function (path) {
findFilesRecursive([path], function (fileObjs) { findFilesRecursive([path], function (fileObjs) {
ret.push(...fileObjs) ret = ret.concat(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)
@@ -270,13 +231,13 @@ function findFilesRecursive (paths, cb) {
return return
} }
const fileOrFolder = paths[0] var 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()) {
const filePath = fileOrFolder var filePath = fileOrFolder
return cb([{ return cb([{
name: path.basename(filePath), name: path.basename(filePath),
path: filePath, path: filePath,
@@ -285,10 +246,10 @@ function findFilesRecursive (paths, cb) {
} }
// Folders: recurse, make a list of all the files // Folders: recurse, make a list of all the files
const folderPath = fileOrFolder var 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)
const paths = fileNames.map((fileName) => path.join(folderPath, fileName)) var paths = fileNames.map((fileName) => path.join(folderPath, fileName))
findFilesRecursive(paths, cb) findFilesRecursive(paths, cb)
}) })
}) })
@@ -303,10 +264,33 @@ function deleteFile (path) {
// Delete all files in a torrent // Delete all files in a torrent
function moveItemToTrash (torrentSummary) { function moveItemToTrash (torrentSummary) {
const filePath = TorrentSummary.getFileOrFolder(torrentSummary) var filePath = TorrentSummary.getFileOrFolder(torrentSummary)
if (filePath) ipcRenderer.send('moveItemToTrash', filePath) if (filePath) ipcRenderer.send('moveItemToTrash', filePath)
} }
function showItemInFolder (torrentSummary) { function showItemInFolder (torrentSummary) {
ipcRenderer.send('showItemInFolder', TorrentSummary.getFileOrFolder(torrentSummary)) ipcRenderer.send('showItemInFolder', TorrentSummary.getFileOrFolder(torrentSummary))
} }
function saveTorrentFileAs (torrentSummary) {
var downloadPath = this.state.saved.prefs.downloadPath
var newFileName = path.parse(torrentSummary.name).name + '.torrent'
var opts = {
title: 'Save Torrent File',
defaultPath: path.join(downloadPath, newFileName),
filters: [
{ name: 'Torrent Files', extensions: ['torrent'] },
{ name: 'All Files', extensions: ['*'] }
]
}
electron.remote.dialog.showSaveDialog(electron.remote.getCurrentWindow(), opts, function (savePath) {
if (!savePath) return // They clicked Cancel
var torrentPath = TorrentSummary.getTorrentPath(torrentSummary)
fs.readFile(torrentPath, function (err, torrentFile) {
if (err) return dispatch('error', err)
fs.writeFile(savePath, torrentFile, function (err) {
if (err) return dispatch('error', 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) {
const skipped = this.state.saved.skippedVersions var 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) {
let skipped = this.state.saved.skippedVersions var 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

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

View File

@@ -13,35 +13,25 @@ module.exports = {
setRate setRate
} }
const config = require('../../config')
const {CastingError} = require('./errors')
// Lazy load these for a ~300ms improvement in startup time // Lazy load these for a ~300ms improvement in startup time
let airplayer, chromecasts, dlnacasts var airplayer, chromecasts, dlnacasts
var config = require('../../config')
// 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
let state var state
// Callback to notify module users when state has changed // Callback to notify module users when state has changed
let update var update
// setInterval() for updating cast status // setInterval() for updating cast status
let statusInterval = null var 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) {
state = appState state = appState
update = callback update = callback
// Don't actually cast during integration tests
// (Otherwise you'd need a physical Chromecast + AppleTV + DLNA TV to run them.)
if (config.IS_TEST) {
state.devices.chromecast = testPlayer('chromecast')
state.devices.airplay = testPlayer('airplay')
state.devices.dlna = testPlayer('dlna')
return
}
// Load modules, scan the network for devices // Load modules, scan the network for devices
airplayer = require('airplayer')() airplayer = require('airplayer')()
chromecasts = require('chromecasts')() chromecasts = require('chromecasts')()
@@ -67,35 +57,9 @@ function init (appState, callback) {
}) })
} }
// integration test player implementation
function testPlayer (type) {
return {
getDevices,
open,
play,
pause,
stop,
status,
seek,
volume
}
function getDevices () {
return [{name: type + '-1'}, {name: type + '-2'}]
}
function open () {}
function play () {}
function pause () {}
function stop () {}
function status () {}
function seek () {}
function volume () {}
}
// chromecast player implementation // chromecast player implementation
function chromecastPlayer () { function chromecastPlayer () {
const ret = { var ret = {
device: null, device: null,
addDevice, addDevice,
getDevices, getDevices,
@@ -131,8 +95,8 @@ function chromecastPlayer () {
} }
function open () { function open () {
const torrentSummary = state.saved.torrents.find((x) => x.infoHash === state.playing.infoHash) var torrentSummary = state.saved.torrents.find((x) => x.infoHash === state.playing.infoHash)
ret.device.play(state.server.networkURL + '/' + state.playing.fileIndex, { ret.device.play(state.server.networkURL, {
type: 'video/mp4', type: 'video/mp4',
title: config.APP_NAME + ' - ' + torrentSummary.name title: config.APP_NAME + ' - ' + torrentSummary.name
}, function (err) { }, function (err) {
@@ -162,7 +126,13 @@ function chromecastPlayer () {
} }
function status () { function status () {
ret.device.status(handleStatus) ret.device.status(function (err, status) {
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) {
@@ -176,7 +146,7 @@ function chromecastPlayer () {
// airplay player implementation // airplay player implementation
function airplayPlayer () { function airplayPlayer () {
const ret = { var ret = {
device: null, device: null,
addDevice, addDevice,
getDevices, getDevices,
@@ -213,7 +183,7 @@ function airplayPlayer () {
} }
function open () { function open () {
ret.device.play(state.server.networkURL + '/' + state.playing.fileIndex, function (err, res) { ret.device.play(state.server.networkURL, function (err, res) {
if (err) { if (err) {
state.playing.location = 'local' state.playing.location = 'local'
state.errors.push({ state.errors.push({
@@ -268,7 +238,7 @@ function airplayPlayer () {
// DLNA player implementation // DLNA player implementation
function dlnaPlayer (player) { function dlnaPlayer (player) {
const ret = { var ret = {
device: null, device: null,
addDevice, addDevice,
getDevices, getDevices,
@@ -304,8 +274,8 @@ function dlnaPlayer (player) {
} }
function open () { function open () {
const torrentSummary = state.saved.torrents.find((x) => x.infoHash === state.playing.infoHash) var torrentSummary = state.saved.torrents.find((x) => x.infoHash === state.playing.infoHash)
ret.device.play(state.server.networkURL + '/' + state.playing.fileIndex, { ret.device.play(state.server.networkURL, {
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
@@ -336,7 +306,13 @@ function dlnaPlayer (player) {
} }
function status () { function status () {
ret.device.status(handleStatus) ret.device.status(function (err, status) {
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) {
@@ -352,22 +328,10 @@ 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 () {
const player = getPlayer() var player = getPlayer()
if (player) player.status() if (player) player.status()
}, 1000) }, 1000)
} }
@@ -386,26 +350,23 @@ 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 CastingError( throw new Error('You can\'t connect to ' + location + ' when already connected to another device')
`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
const player = getPlayer(location) var player = getPlayer(location)
const devices = player ? player.getDevices() : [] var devices = player ? player.getDevices() : []
if (devices.length === 0) { if (devices.length === 0) throw new Error('No ' + location + ' devices available')
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) {
const {location, devices} = state.devices.castMenu var {location, devices} = state.devices.castMenu
// Start casting // Start casting
const player = getPlayer(location) var player = getPlayer(location)
player.device = devices[index] player.device = devices[index]
player.open() player.open()
@@ -421,7 +382,7 @@ function selectDevice (index) {
// Stops casting, move video back to local screen // Stops casting, move video back to local screen
function stop () { function stop () {
const player = getPlayer() var player = getPlayer()
if (player) { if (player) {
player.stop(function () { player.stop(function () {
player.device = null player.device = null
@@ -456,18 +417,18 @@ function getPlayer (location) {
} }
function play () { function play () {
const player = getPlayer() var player = getPlayer()
if (player) player.play(castCallback) if (player) player.play(castCallback)
} }
function pause () { function pause () {
const player = getPlayer() var player = getPlayer()
if (player) player.pause(castCallback) if (player) player.pause(castCallback)
} }
function setRate (rate) { function setRate (rate) {
let player var player
let result = true var 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()
@@ -482,12 +443,12 @@ function setRate (rate) {
} }
function seek (time) { function seek (time) {
const player = getPlayer() var player = getPlayer()
if (player) player.seek(time, castCallback) if (player) player.seek(time, castCallback)
} }
function setVolume (volume) { function setVolume (volume) {
const player = getPlayer() var player = getPlayer()
if (player) player.volume(volume, castCallback) if (player) player.volume(volume, castCallback)
} }

View File

@@ -4,8 +4,8 @@ module.exports = {
setDispatch setDispatch
} }
const dispatchers = {} var dispatchers = {}
let _dispatch = function () {} var _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) {
const str = JSON.stringify(args) var str = JSON.stringify(args)
let handler = dispatchers[str] var 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,49 +1,8 @@
const ExtendableError = require('es6-error')
/* 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') }
}
class UnplayableFileError extends PlaybackError {
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 {}
module.exports = { module.exports = {
CastingError, UnplayableError
PlaybackError,
SoundError,
TorrentError,
UnplayableTorrentError,
UnplayableFileError,
PlaybackTimedOutError,
InvalidSoundNameError,
TorrentKeyNotFoundError,
InvalidTorrentError
} }
function UnplayableError () {
this.message = 'Can\'t play any files in torrent'
}
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
} }
const version = state.saved.version var 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,20 +35,16 @@ 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) {
const fs = require('fs-extra') var fs = require('fs-extra')
const path = require('path') var path = require('path')
saved.torrents.forEach(function (ts) { saved.torrents.forEach(function (ts) {
const infoHash = ts.infoHash var 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:
@@ -56,7 +52,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
let src, dst var 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
@@ -74,7 +70,7 @@ function migrate_0_7_0 (saved) {
// Replace posterURL with posterFileName // Replace posterURL with posterFileName
if (ts.posterURL) { if (ts.posterURL) {
const extension = path.extname(ts.posterURL) var 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)
@@ -120,7 +116,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
const defaultTorrentFiles = [ var defaultTorrentFiles = [
'6a9759bffd5c0af65319979fb7832189f4f3c35d.torrent', '6a9759bffd5c0af65319979fb7832189f4f3c35d.torrent',
'88594aaacbde40ef3e2510c47374ec0aa396c08e.torrent', '88594aaacbde40ef3e2510c47374ec0aa396c08e.torrent',
'6a02592d2bbc069628cd5ed8a54f88ee06ac0ba5.torrent', '6a02592d2bbc069628cd5ed8a54f88ee06ac0ba5.torrent',
@@ -129,7 +125,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
const fileOrFolder = TorrentSummary.getFileOrFolder(torrentSummary) var fileOrFolder = TorrentSummary.getFileOrFolder(torrentSummary)
if (!fileOrFolder) return if (!fileOrFolder) return
try { try {
fs.statSync(fileOrFolder) fs.statSync(fileOrFolder)
@@ -139,9 +135,3 @@ function migrate_0_12_0 (saved) {
} }
}) })
} }
function migrate_0_14_0 (saved) {
saved.torrents.forEach(function (ts) {
delete ts.defaultPlayFileIndex
})
}

View File

@@ -1,85 +0,0 @@
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,16 +3,15 @@ module.exports = {
play play
} }
const config = require('../../config') var config = require('../../config')
const {InvalidSoundNameError} = require('./errors') var path = require('path')
const path = require('path')
const VOLUME = 0.15 var VOLUME = 0.15
/* Cache of Audio elements, for instant playback */ /* Cache of Audio elements, for instant playback */
const cache = {} var cache = {}
const sounds = { var 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
@@ -48,10 +47,10 @@ const sounds = {
} }
function preload () { function preload () {
for (let name in sounds) { for (var name in sounds) {
if (!cache[name]) { if (!cache[name]) {
const sound = sounds[name] var sound = sounds[name]
const audio = cache[name] = new window.Audio() var audio = cache[name] = new window.Audio()
audio.volume = sound.volume audio.volume = sound.volume
audio.src = sound.url audio.src = sound.url
} }
@@ -59,11 +58,11 @@ function preload () {
} }
function play (name) { function play (name) {
let audio = cache[name] var audio = cache[name]
if (!audio) { if (!audio) {
const sound = sounds[name] var sound = sounds[name]
if (!sound) { if (!sound) {
throw new InvalidSoundNameError(name) throw new Error('Invalid sound 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 @@
const appConfig = require('application-config')('WebTorrent') var appConfig = require('application-config')('WebTorrent')
const path = require('path') var path = require('path')
const {EventEmitter} = require('events') var {EventEmitter} = require('events')
const config = require('../../config') var config = require('../../config')
const migrations = require('./migrations') var migrations = require('./migrations')
const State = module.exports = Object.assign(new EventEmitter(), { var State = module.exports = Object.assign(new EventEmitter(), {
getDefaultPlayState, getDefaultPlayState,
load, load,
save, save,
@@ -15,7 +15,7 @@ const 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 () {
const LocationHistory = require('location-history') var LocationHistory = require('location-history')
return { return {
/* /*
@@ -68,8 +68,7 @@ function getDefaultState () {
* Getters, for convenience * Getters, for convenience
*/ */
getPlayingTorrentSummary, getPlayingTorrentSummary,
getPlayingFileSummary, getPlayingFileSummary
getExternalPlayerName
} }
} }
@@ -99,26 +98,25 @@ 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) {
const fs = require('fs-extra') var fs = require('fs-extra')
const parseTorrent = require('parse-torrent') var parseTorrent = require('parse-torrent')
const parallel = require('run-parallel') var parallel = require('run-parallel')
const saved = { var 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 */
} }
const tasks = [] var tasks = []
config.DEFAULT_TORRENTS.map(function (t, i) { config.DEFAULT_TORRENTS.map(function (t, i) {
const infoHash = saved.torrents[i].infoHash var 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),
@@ -141,8 +139,8 @@ function setupSavedState (cb) {
}) })
function createTorrentObject (t) { function createTorrentObject (t) {
const torrent = fs.readFileSync(path.join(config.STATIC_PATH, t.torrentFileName)) var torrent = fs.readFileSync(path.join(config.STATIC_PATH, t.torrentFileName))
const parsedTorrent = parseTorrent(torrent) var parsedTorrent = parseTorrent(torrent)
return { return {
status: 'paused', status: 'paused',
@@ -153,31 +151,24 @@ 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 () {
const infoHash = this.playing.infoHash var 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 () {
const torrentSummary = this.getPlayingTorrentSummary() var 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) {
const state = getDefaultState() var state = getDefaultState()
appConfig.read(function (err, saved) { appConfig.read(function (err, saved) {
if (err || !saved.version) { if (err || !saved.version) {
@@ -202,14 +193,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
const copy = Object.assign({}, state.saved) var 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) {
const torrent = {} var torrent = {}
for (let key in x) { for (var 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')
let telemetry var telemetry
function init (state) { function init (state) {
telemetry = state.saved.telemetry telemetry = state.saved.telemetry
@@ -23,14 +23,13 @@ function init (state) {
reset() reset()
} }
const now = new Date() var 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.torrentStats = getTorrentStats(state) telemetry.approxNumTorrents = getApproxNumTorrents(state)
telemetry.approxNumTorrents = telemetry.torrentStats.approxCount
if (config.IS_PRODUCTION) { if (config.IS_PRODUCTION) {
postToServer() postToServer()
@@ -57,17 +56,17 @@ function reset () {
function postToServer () { function postToServer () {
// Serialize the telemetry summary // Serialize the telemetry summary
const payload = new Buffer(JSON.stringify(telemetry), 'utf8') var payload = new Buffer(JSON.stringify(telemetry), 'utf8')
// POST to our server // POST to our server
const options = url.parse(config.TELEMETRY_URL) var 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
} }
const req = https.request(options, function (res) { var 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()
@@ -86,7 +85,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
const userID = crypto.randomBytes(32).toString('hex') var userID = crypto.randomBytes(32).toString('hex')
return { userID } return { userID }
} }
@@ -105,64 +104,18 @@ function getSystemInfo () {
osPlatform: process.platform, osPlatform: process.platform,
osRelease: os.type() + ' ' + os.release(), osRelease: os.type() + ' ' + os.release(),
architecture: os.arch(), architecture: os.arch(),
systemArchitecture: config.OS_SYSARCH, totalMemoryMB: os.totalmem() / (1 << 20),
totalMemoryMB: roundPow2(os.totalmem() / (1 << 20)),
numCores: os.cpus().length numCores: os.cpus().length
} }
} }
// Get stats like the # of torrents currently active, # in list, total size // Get the number of torrents, rounded to the nearest power of two
function getTorrentStats (state) { function getApproxNumTorrents (state) {
const count = state.saved.torrents.length var exactNum = state.saved.torrents.length
let sizeMB = 0 if (exactNum === 0) return 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
const log2 = Math.log(n) / Math.log(2) var log2 = Math.log(exactNum) / Math.log(2)
return Math.pow(2, Math.round(log2)) return 1 << 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
@@ -171,10 +124,12 @@ 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
let message var message
let stack = '' var stack = ''
if (e == null) { if (e.message) {
message = 'Unexpected undefined error' // 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) {
@@ -183,10 +138,6 @@ 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) {
@@ -198,14 +149,11 @@ function logUncaughtError (procName, e) {
} }
} }
if (typeof stack !== 'string') stack = 'Unexpected stack: ' + stack
if (typeof message !== 'string') message = 'Unexpected message: ' + message
// Remove the first part of each file path in the stack trace. // Remove the first part of each file path in the stack trace.
// - 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, '(...') if (stack && typeof stack === 'string') stack = stack.replace(/\(.*app.asar/g, '(...')
stack = stack.replace(/at .*app.asar/g, 'at ...') else if (stack) stack = 'Unexpected stack: ' + stack
// 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
@@ -213,14 +161,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*
const version = config.APP_VERSION var 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) {
let ret = elem.tagName var ret = elem.tagName
try { try {
ret += '.' + Array.from(elem.classList).join('.') ret += '.' + Array.from(elem.classList).join('.')
} catch (e) {} } catch (e) {}
@@ -234,7 +182,7 @@ function logPlayAttempt (result) {
return console.error('Unknown play attempt result', result) return console.error('Unknown play attempt result', result)
} }
const attempts = telemetry.playAttempts var 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,10 +3,11 @@ module.exports = {
isVideo, isVideo,
isAudio, isAudio,
isTorrent, isTorrent,
isPlayableTorrentSummary isPlayableTorrentSummary,
pickFileToPlay
} }
const path = require('path') var 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
@@ -46,16 +47,38 @@ 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) {
const isTorrentFile = getFileExtension(file) === '.torrent' var isTorrentFile = getFileExtension(file) === '.torrent'
const isMagnet = typeof file === 'string' && /^(stream-)?magnet:/.test(file) var isMagnet = typeof file === 'string' && /^(stream-)?magnet:/.test(file)
return isTorrentFile || isMagnet return isTorrentFile || isMagnet
} }
function getFileExtension (file) { function getFileExtension (file) {
const name = typeof file === 'string' ? file : file.name var 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
const captureFrame = require('capture-frame') var captureVideoFrame = require('./capture-video-frame')
const path = require('path') var 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
const posterFile = torrent.files.filter(function (file) { var 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
const videoFile = getLargestFileByExtension(torrent, ['.mp4', '.m4v', '.webm', '.mov', '.mkv']) var 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
const imgFile = getLargestFileByExtension(torrent, ['.gif', '.jpg', '.jpeg', '.png']) var 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) {
const files = torrent.files.filter(function (file) { var files = torrent.files.filter(function (file) {
const extname = path.extname(file.name).toLowerCase() var 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) {
const index = torrent.files.indexOf(file) var index = torrent.files.indexOf(file)
const server = torrent.createServer(0) var server = torrent.createServer(0)
server.listen(0, onListening) server.listen(0, onListening)
function onListening () { function onListening () {
const port = server.address().port var port = server.address().port
const url = 'http://localhost:' + port + '/' + index var url = 'http://localhost:' + port + '/' + index
const video = document.createElement('video') var 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)
const buf = captureFrame(video) var 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) {
const extname = path.extname(file.name) var 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
} }
const path = require('path') var path = require('path')
const config = require('../../config') var 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
const posterPath = path.join(config.POSTER_PATH, torrentSummary.posterFileName) var 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) {
const s = torrentSummary var 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) {
const ts = torrentSummary var 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
let controllers = null var controllers = null
// This dependency is the slowest-loading, so we lazy load it // This dependency is the slowest-loading, so we lazy load it
let Cast = null var 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.
let state var state
// Root React component // Root React component
let app var app
State.load(onState) State.load(onState)
@@ -58,14 +58,6 @@ 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 = {
@@ -122,10 +114,13 @@ 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
if (electron.remote.getCurrentWindow().isVisible()) { sound.play('STARTUP')
sound.play('STARTUP')
}
console.timeEnd('init') console.timeEnd('init')
} }
@@ -133,6 +128,7 @@ 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
@@ -182,26 +178,16 @@ 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) => 'toggleTorrentFile': (infoHash, index) => controllers.torrentList.toggleTorrentFile(infoHash, index),
controllers.torrentList.toggleTorrentFile(infoHash, index), 'confirmDeleteTorrent': (infoHash, deleteData) => controllers.torrentList.confirmDeleteTorrent(infoHash, deleteData),
'confirmDeleteTorrent': (infoHash, deleteData) => 'deleteTorrent': (infoHash, deleteData) => controllers.torrentList.deleteTorrent(infoHash, deleteData),
controllers.torrentList.confirmDeleteTorrent(infoHash, deleteData), 'toggleSelectTorrent': (infoHash) => controllers.torrentList.toggleSelectTorrent(infoHash),
'deleteTorrent': (infoHash, deleteData) => 'openTorrentContextMenu': (infoHash) => controllers.torrentList.openTorrentContextMenu(infoHash),
controllers.torrentList.deleteTorrent(infoHash, deleteData), 'startTorrentingSummary': (torrentKey) => controllers.torrentList.startTorrentingSummary(torrentKey),
'toggleSelectTorrent': (infoHash) =>
controllers.torrentList.toggleSelectTorrent(infoHash),
'openTorrentContextMenu': (infoHash) =>
controllers.torrentList.openTorrentContextMenu(infoHash),
'startTorrentingSummary': (torrentKey) =>
controllers.torrentList.startTorrentingSummary(torrentKey),
'saveTorrentFileAs': (torrentKey) =>
controllers.torrentList.saveTorrentFileAs(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),
@@ -269,7 +255,7 @@ function dispatch (action, ...args) {
console.log('dispatch: %s %o', action, args) console.log('dispatch: %s %o', action, args)
} }
const handler = dispatchHandlers[action] var 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)
@@ -289,7 +275,7 @@ function setupIpc () {
ipcRenderer.on('fullscreenChanged', onFullscreenChanged) ipcRenderer.on('fullscreenChanged', onFullscreenChanged)
const tc = controllers.torrent var 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))
@@ -316,7 +302,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
const contentTag = document.querySelector('.content') var contentTag = document.querySelector('.content')
if (contentTag) contentTag.scrollTop = 0 if (contentTag) contentTag.scrollTop = 0
}) })
} }
@@ -363,18 +349,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
const screenWidth = window.screen.width var screenWidth = window.screen.width
const screenHeight = window.screen.height var screenHeight = window.screen.height
const aspectRatio = dimensions.width / dimensions.height var aspectRatio = dimensions.width / dimensions.height
const scaleFactor = Math.min( var 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)
) )
const width = Math.max( var width = Math.max(
Math.floor(dimensions.width * scaleFactor), Math.floor(dimensions.width * scaleFactor),
config.WINDOW_MIN_WIDTH config.WINDOW_MIN_WIDTH
) )
const height = Math.max( var height = Math.max(
Math.floor(dimensions.height * scaleFactor), Math.floor(dimensions.height * scaleFactor),
config.WINDOW_MIN_HEIGHT config.WINDOW_MIN_HEIGHT
) )
@@ -389,9 +375,9 @@ function setDimensions (dimensions) {
function onOpen (files) { function onOpen (files) {
if (!Array.isArray(files)) files = [ files ] if (!Array.isArray(files)) files = [ files ]
const url = state.location.url() var url = state.location.url()
const allTorrents = files.every(TorrentPlayer.isTorrent) var allTorrents = files.every(TorrentPlayer.isTorrent)
const allSubtitles = files.every(controllers.subtitles.isSubtitle) var 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
@@ -427,7 +413,7 @@ function onError (err) {
function onPaste (e) { function onPaste (e) {
if (e.target.tagName.toLowerCase() === 'input') return if (e.target.tagName.toLowerCase() === 'input') return
const torrentIds = electron.clipboard.readText().split('\n') var 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,17 +2,16 @@ 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('./torrent-list-page'), 'home': require('./TorrentListPage'),
'player': require('./player-page'), 'player': require('./PlayerPage'),
'create-torrent': require('./create-torrent-page'), 'create-torrent': require('./CreateTorrentPage'),
'preferences': require('./preferences-page') 'preferences': require('./PreferencesPage')
} }
const Modals = { const Modals = {
@@ -22,11 +21,9 @@ const Modals = {
'unsupported-media-modal': require('../components/unsupported-media-modal') 'unsupported-media-modal': require('../components/unsupported-media-modal')
} }
const fontFamily = process.platform === 'win32' darkBaseTheme.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
@@ -36,21 +33,21 @@ darkBaseTheme.palette.accent3Color = colors.redA100
class App extends React.Component { class App extends React.Component {
render () { render () {
const state = this.props.state var 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
const hideControls = state.location.url() === 'player' && var 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
const cls = [ var 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 */
] ]
@@ -58,7 +55,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')
const vdom = ( var 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} />
@@ -73,12 +70,12 @@ class App extends React.Component {
} }
getErrorPopover () { getErrorPopover () {
const state = this.props.state var state = this.props.state
const now = new Date().getTime() var now = new Date().getTime()
const recentErrors = state.errors.filter((x) => now - x.time < 5000) var recentErrors = state.errors.filter((x) => now - x.time < 5000)
const hasErrors = recentErrors.length > 0 var hasErrors = recentErrors.length > 0
const errorElems = recentErrors.map(function (error, i) { var 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 (
@@ -91,24 +88,22 @@ class App extends React.Component {
} }
getModal () { getModal () {
const state = this.props.state var state = this.props.state
if (!state.modal) return if (!state.modal) return
const ModalContents = Modals[state.modal.id] var ModalContents = Modals[state.modal.id]
return ( return (
<MuiThemeProvider muiTheme={getMuiTheme(lightBaseTheme)}> <div key='modal' className='modal'>
<div key='modal' className='modal'> <div key='modal-background' className='modal-background' />
<div key='modal-background' className='modal-background' /> <div key='modal-content' className='modal-content'>
<div key='modal-content' className='modal-content'> <ModalContents state={state} />
<ModalContents state={state} />
</div>
</div> </div>
</MuiThemeProvider> </div>
) )
} }
getView () { getView () {
const state = this.props.state var state = this.props.state
const View = Views[state.location.url()] var View = Views[state.location.url()]
return (<View state={state} />) return (<View state={state} />)
} }
} }

View File

@@ -0,0 +1,171 @@
const createTorrent = require('create-torrent')
const path = require('path')
const prettyBytes = require('prettier-bytes')
const React = require('react')
const {dispatch, dispatcher} = require('../lib/dispatcher')
const FlatButton = require('material-ui/FlatButton').default
const RaisedButton = require('material-ui/RaisedButton').default
const TextField = require('material-ui/TextField').default
const CreateTorrentErrorPage = require('../components/create-torrent-error-page')
const Heading = require('../components/Heading')
const ShowMore = require('../components/ShowMore')
class CreateTorrentPage extends React.Component {
constructor (props) {
super(props)
var state = this.props.state
var info = state.location.current()
// Preprocess: exclude .DS_Store and other dotfiles
var files = info.files
.filter((f) => !f.name.startsWith('.'))
.map((f) => ({name: f.name, path: f.path, size: f.size}))
if (files.length === 0) return (<CreateTorrentErrorPage state={state} />)
// First, extract the base folder that the files are all in
var pathPrefix = info.folderPath
if (!pathPrefix) {
pathPrefix = files.map((x) => x.path).reduce(findCommonPrefix)
if (!pathPrefix.endsWith('/') && !pathPrefix.endsWith('\\')) {
pathPrefix = path.dirname(pathPrefix)
}
}
// Sanity check: show the number of files and total size
var numFiles = files.length
var totalBytes = files
.map((f) => f.size)
.reduce((a, b) => a + b, 0)
var torrentInfo = `${numFiles} files, ${prettyBytes(totalBytes)}`
// 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.
var defaultName, basePath
if (files.length === 1) {
// Single file torrent: /a/b/foo.jpg -> torrent name 'foo.jpg', path '/a/b'
defaultName = files[0].name
basePath = pathPrefix
} else {
// Multi file torrent: /a/b/{foo, bar}.jpg -> torrent name 'b', path '/a'
defaultName = path.basename(pathPrefix)
basePath = path.dirname(pathPrefix)
}
var maxFileElems = 100
var fileElems = files.slice(0, maxFileElems).map(function (file, i) {
var relativePath = files.length === 0 ? file.name : path.relative(pathPrefix, file.path)
return (<div key={i}>{relativePath}</div>)
})
if (files.length > maxFileElems) {
fileElems.push(<div key='more'>+ {maxFileElems - files.length} more</div>)
}
var trackers = createTorrent.announceList.join('\n')
this.state = {
basePath,
defaultName,
fileElems,
torrentInfo,
trackers
}
}
handleSubmit () {
var announceList = document.querySelector('.torrent-trackers').value
.split('\n')
.map((s) => s.trim())
.filter((s) => s !== '')
var isPrivate = document.querySelector('.torrent-is-private').checked
var comment = document.querySelector('.torrent-comment').value.trim()
var options = {
// 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.
// If we ever want to add support for that:
// name: document.querySelector('.torrent-name').value
name: this.state.defaultName,
path: this.state.basePath,
files: this.state.files,
announce: announceList,
private: isPrivate,
comment: comment
}
dispatch('createTorrent', options)
}
render () {
return (
<div className='create-torrent'>
<Heading level={1}>
Create torrent "{this.state.defaultName}"
</Heading>
<div className='torrent-info'>
{this.state.torrentInfo}
</div>
<div className='torrent-attribute'>
<label>Path:</label>
<div className='torrent-attribute'>{this.state.pathPrefix}</div>
</div>
<ShowMore
style={{
marginBottom: 10
}}
hideLabel='Hide advanced settings...'
showLabel='Show advanced settings...'
>
<div key='advanced' className='create-torrent-advanced'>
<div key='private' className='torrent-attribute'>
<label>Private:</label>
<input type='checkbox' className='torrent-is-private' value='torrent-is-private' />
</div>
<Heading level={2}>Trackers:</Heading>
<TextField
className='torrent-trackers'
hintText='Tracker'
multiLine
rows={2}
rowsMax={10}
defaultValue={this.state.trackers}
/>
<div key='comment' className='torrent-attribute'>
<label>Comment:</label>
<textarea className='torrent-attribute torrent-comment' />
</div>
<div key='files' className='torrent-attribute'>
<label>Files:</label>
<div>{this.state.fileElems}</div>
</div>
</div>
</ShowMore>
<div className='float-right'>
<FlatButton
label='Cancel'
style={{
marginRight: 10
}}
onClick={dispatcher('cancel')}
/>
<RaisedButton
label='Create Torrent'
primary
onClick={this.handleSubmit}
/>
</div>
</div>
)
}
}
// Finds the longest common prefix
function findCommonPrefix (a, b) {
for (var i = 0; i < a.length && i < b.length; i++) {
if (a.charCodeAt(i) !== b.charCodeAt(i)) break
}
if (i === a.length) return a
if (i === b.length) return b
return a.substring(0, i)
}
module.exports = CreateTorrentPage

View File

@@ -2,20 +2,19 @@ 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')
const config = require('../../config')
// Shows a streaming video player. Standard features + Chromecast + Airplay // Shows a streaming video player. Standard features + Chromecast + Airplay
module.exports = class Player extends React.Component { 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
const state = this.props.state var state = this.props.state
const showVideo = state.playing.location === 'local' var showVideo = state.playing.location === 'local'
const showControls = state.playing.location !== 'external' var showControls = state.playing.location !== 'external'
return ( return (
<div <div
className='player' className='player'
@@ -39,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
const mediaElement = document.querySelector(state.playing.type) var 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()
@@ -67,20 +66,20 @@ function renderMedia (state) {
} }
// Switch to the newly added subtitle track, if available // Switch to the newly added subtitle track, if available
const tracks = mediaElement.textTracks || [] var tracks = mediaElement.textTracks || []
for (let j = 0; j < tracks.length; j++) { for (var j = 0; j < tracks.length; j++) {
const isSelectedTrack = j === state.playing.subtitles.selectedIndex var isSelectedTrack = j === state.playing.subtitles.selectedIndex
tracks[j].mode = isSelectedTrack ? 'showing' : 'hidden' tracks[j].mode = isSelectedTrack ? 'showing' : 'hidden'
} }
// Save video position // Save video position
const file = state.getPlayingFileSummary() var 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) {
const index = state.playing.subtitles.selectedIndex var 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
@@ -90,11 +89,11 @@ function renderMedia (state) {
} }
// Add subtitles to the <video> tag // Add subtitles to the <video> tag
const trackTags = [] var trackTags = []
if (state.playing.subtitles.selectedIndex >= 0) { if (state.playing.subtitles.selectedIndex >= 0) {
for (let i = 0; i < state.playing.subtitles.tracks.length; i++) { for (var i = 0; i < state.playing.subtitles.tracks.length; i++) {
const track = state.playing.subtitles.tracks[i] var track = state.playing.subtitles.tracks[i]
const isSelected = state.playing.subtitles.selectedIndex === i var isSelected = state.playing.subtitles.selectedIndex === i
trackTags.push( trackTags.push(
<track <track
key={i} key={i}
@@ -107,10 +106,10 @@ function renderMedia (state) {
} }
// Create the <audio> or <video> tag // Create the <audio> or <video> tag
const MediaTagName = state.playing.type var MediaTagName = state.playing.type
const mediaTag = ( var mediaTag = (
<MediaTagName <MediaTagName
src={Playlist.getCurrentLocalURL(state)} src={state.server.localURL}
onDoubleClick={dispatcher('toggleFullScreen')} onDoubleClick={dispatcher('toggleFullScreen')}
onLoadedMetadata={onLoadedMetadata} onLoadedMetadata={onLoadedMetadata}
onEnded={onEnded} onEnded={onEnded}
@@ -137,25 +136,21 @@ 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
const video = e.target var video = e.target
const dimensions = { var 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) {
if (Playlist.hasNext(state)) { state.playing.isPaused = true
dispatch('nextTrack')
} else {
// When the last video completes, pause the video instead of looping
state.playing.isPaused = true
}
} }
function onCanPlay (e) { function onCanPlay (e) {
const elem = e.target var 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')
@@ -169,15 +164,15 @@ function renderMedia (state) {
} }
function renderOverlay (state) { function renderOverlay (state) {
const elems = [] var elems = []
const audioMetadataElem = renderAudioMetadata(state) var audioMetadataElem = renderAudioMetadata(state)
const spinnerElem = renderLoadingSpinner(state) var 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.
let style var 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) {
@@ -195,27 +190,27 @@ function renderOverlay (state) {
} }
function renderAudioMetadata (state) { function renderAudioMetadata (state) {
const fileSummary = state.getPlayingFileSummary() var fileSummary = state.getPlayingFileSummary()
if (!fileSummary.audioInfo) return if (!fileSummary.audioInfo) return
const info = fileSummary.audioInfo var info = fileSummary.audioInfo
// Get audio track info // Get audio track info
let title = info.title var title = info.title
if (!title) { if (!title) {
title = fileSummary.name title = fileSummary.name
} }
let artist = info.artist && info.artist[0] var artist = info.artist && info.artist[0]
let album = info.album var 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 + ')'
} }
let track var 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
const elems = [] var elems = []
if (artist) { if (artist) {
elems.push(( elems.push((
<div key='artist' className='audio-artist'> <div key='artist' className='audio-artist'>
@@ -239,7 +234,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
const emptyLabel = (<label />) var 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}
@@ -251,14 +246,14 @@ function renderAudioMetadata (state) {
function renderLoadingSpinner (state) { function renderLoadingSpinner (state) {
if (state.playing.isPaused) return if (state.playing.isPaused) return
const isProbablyStalled = state.playing.isStalled || var isProbablyStalled = state.playing.isStalled ||
(new Date().getTime() - state.playing.lastTimeUpdate > 2000) (new Date().getTime() - state.playing.lastTimeUpdate > 2000)
if (!isProbablyStalled) return if (!isProbablyStalled) return
const prog = state.getPlayingTorrentSummary().progress || {} var prog = state.getPlayingTorrentSummary().progress || {}
let fileProgress = 0 var fileProgress = 0
if (prog.files) { if (prog.files) {
const file = prog.files[state.playing.fileIndex] var file = prog.files[state.playing.fileIndex]
fileProgress = Math.floor(100 * file.numPiecesPresent / file.numPieces) fileProgress = Math.floor(100 * file.numPiecesPresent / file.numPieces)
} }
@@ -275,7 +270,7 @@ function renderLoadingSpinner (state) {
} }
function renderCastScreen (state) { function renderCastScreen (state) {
let castIcon, castType, isCast var 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'
@@ -289,8 +284,11 @@ 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 = state.getExternalPlayerName() castType = playerName
isCast = false isCast = false
} else if (state.playing.location === 'error') { } else if (state.playing.location === 'error') {
castIcon = 'error_outline' castIcon = 'error_outline'
@@ -298,15 +296,15 @@ function renderCastScreen (state) {
isCast = false isCast = false
} }
const isStarting = state.playing.location.endsWith('-pending') var isStarting = state.playing.location.endsWith('-pending')
const castName = state.playing.castName var castName = state.playing.castName
let castStatus var 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
const style = { var style = {
backgroundImage: cssBackgroundImagePoster(state) backgroundImage: cssBackgroundImagePoster(state)
} }
@@ -324,12 +322,12 @@ function renderCastScreen (state) {
function renderCastOptions (state) { function renderCastOptions (state) {
if (!state.devices.castMenu) return if (!state.devices.castMenu) return
const {location, devices} = state.devices.castMenu var {location, devices} = state.devices.castMenu
const player = state.devices[location] var player = state.devices[location]
const items = devices.map(function (device, ix) { var items = devices.map(function (device, ix) {
const isSelected = player.device === device var isSelected = player.device === device
const name = device.name var 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>
@@ -346,11 +344,11 @@ function renderCastOptions (state) {
} }
function renderSubtitleOptions (state) { function renderSubtitleOptions (state) {
const subtitles = state.playing.subtitles var subtitles = state.playing.subtitles
if (!subtitles.tracks.length || !subtitles.showMenu) return if (!subtitles.tracks.length || !subtitles.showMenu) return
const items = subtitles.tracks.map(function (track, ix) { var items = subtitles.tracks.map(function (track, ix) {
const isSelected = state.playing.subtitles.selectedIndex === ix var 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>
@@ -359,8 +357,8 @@ function renderSubtitleOptions (state) {
) )
}) })
const noneSelected = state.playing.subtitles.selectedIndex === -1 var noneSelected = state.playing.subtitles.selectedIndex === -1
const noneClass = 'radio_button_' + (noneSelected ? 'checked' : 'unchecked') var noneClass = 'radio_button_' + (noneSelected ? 'checked' : 'unchecked')
return ( return (
<ul key='subtitle-options' className='options-list'> <ul key='subtitle-options' className='options-list'>
{items} {items}
@@ -373,39 +371,32 @@ function renderSubtitleOptions (state) {
} }
function renderPlayerControls (state) { function renderPlayerControls (state) {
const positionPercent = 100 * state.playing.currentTime / state.playing.duration var positionPercent = 100 * state.playing.currentTime / state.playing.duration
const playbackCursorStyle = { left: 'calc(' + positionPercent + '% - 3px)' } var playbackCursorStyle = { left: 'calc(' + positionPercent + '% - 3px)' }
const captionsClass = state.playing.subtitles.tracks.length === 0 var 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'
const elements = [ var 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'
@@ -413,13 +404,6 @@ 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'
@@ -441,24 +425,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
const castTypes = ['chromecast', 'airplay', 'dlna'] var castTypes = ['chromecast', 'airplay', 'dlna']
const isCastingAnywhere = castTypes.some( var 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:
const buttonIcons = { var 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?
const isCasting = state.playing.location.startsWith(castType) var isCasting = state.playing.location.startsWith(castType)
const player = state.devices[castType] var 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:
let buttonClass, buttonHandler var 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'
@@ -472,7 +456,7 @@ function renderPlayerControls (state) {
buttonClass = '' buttonClass = ''
buttonHandler = dispatcher('toggleCastMenu', castType) buttonHandler = dispatcher('toggleCastMenu', castType)
} }
const buttonIcon = buttonIcons[castType][isCasting] var buttonIcon = buttonIcons[castType][isCasting]
elements.push(( elements.push((
<i <i
@@ -485,13 +469,13 @@ function renderPlayerControls (state) {
}) })
// Render volume slider // Render volume slider
const volume = state.playing.volume var volume = state.playing.volume
const volumeIcon = 'volume_' + ( var 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')
const volumeStyle = { var 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))'
@@ -511,13 +495,14 @@ 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
const currentTimeStr = formatTime(state.playing.currentTime) var currentTimeStr = formatTime(state.playing.currentTime)
const durationStr = formatTime(state.playing.duration) var 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}
@@ -544,7 +529,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) {
const dt = e.dataTransfer var dt = e.dataTransfer
dt.effectAllowed = 'none' dt.effectAllowed = 'none'
} }
} }
@@ -553,9 +538,9 @@ function renderPlayerControls (state) {
function handleScrub (e) { function handleScrub (e) {
if (!e.clientX) return if (!e.clientX) return
dispatch('mediaMouseMoved') dispatch('mediaMouseMoved')
const windowWidth = document.querySelector('body').clientWidth var windowWidth = document.querySelector('body').clientWidth
const fraction = e.clientX / windowWidth var fraction = e.clientX / windowWidth
const position = fraction * state.playing.duration /* seconds */ var position = fraction * state.playing.duration /* seconds */
dispatch('skipTo', position) dispatch('skipTo', position)
} }
@@ -586,20 +571,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) {
if (config.IS_TEST) return // Don't integration test the loading bar. Screenshots won't match. 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
const prog = torrentSummary.progress var prog = torrentSummary.progress
const fileProg = prog.files[state.playing.fileIndex] var fileProg = prog.files[state.playing.fileIndex]
const parts = [] var parts = []
let lastPiecePresent = false var lastPiecePresent = false
for (let i = fileProg.startPiece; i <= fileProg.endPiece; i++) { for (var i = fileProg.startPiece; i <= fileProg.endPiece; i++) {
const partPresent = Bitfield.prototype.get.call(prog.bitfield, i) var 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) {
@@ -609,8 +592,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
const loadingBarElems = parts.map(function (part, i) { var loadingBarElems = parts.map(function (part, i) {
const style = { var 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) + '%'
} }
@@ -622,8 +605,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) {
const torrentSummary = state.getPlayingTorrentSummary() var torrentSummary = state.getPlayingTorrentSummary()
const posterPath = TorrentSummary.getPosterPath(torrentSummary) var posterPath = TorrentSummary.getPosterPath(torrentSummary)
if (!posterPath) return '' if (!posterPath) return ''
return cssBackgroundImageDarkGradient() + `, url('${posterPath}')` return cssBackgroundImageDarkGradient() + `, url('${posterPath}')`
} }
@@ -638,12 +621,12 @@ function formatTime (time) {
return '0:00' return '0:00'
} }
let hours = Math.floor(time / 3600) var hours = Math.floor(time / 3600)
let minutes = Math.floor(time % 3600 / 60) var minutes = Math.floor(time % 3600 / 60)
if (hours > 0) { if (hours > 0) {
minutes = zeroFill(2, minutes) minutes = zeroFill(2, minutes)
} }
let seconds = zeroFill(2, Math.floor(time % 60)) var seconds = zeroFill(2, Math.floor(time % 60))
return (hours > 0 ? hours + ':' : '') + minutes + ':' + seconds return (hours > 0 ? hours + ':' : '') + minutes + ':' + seconds
} }

View File

@@ -1,14 +1,13 @@
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) {
@@ -22,9 +21,6 @@ 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 () {
@@ -37,7 +33,8 @@ 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>
) )
} }
@@ -53,7 +50,8 @@ 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>
) )
} }
@@ -63,8 +61,9 @@ class PreferencesPage extends React.Component {
} }
externalPlayerPathSelector () { externalPlayerPathSelector () {
const playerPath = this.props.state.unsaved.prefs.externalPlayerPath const playerName = path.basename(
const playerName = this.props.state.getExternalPlayerName() this.props.state.unsaved.prefs.externalPlayerPath || 'VLC'
)
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}.`
@@ -82,70 +81,48 @@ class PreferencesPage extends React.Component {
displayValue={playerName} displayValue={playerName}
onChange={this.handleExternalPlayerPathChange} onChange={this.handleExternalPlayerPathChange}
title='External player' title='External player'
value={playerPath ? path.dirname(playerPath) : null} /> value={this.props.state.unsaved.prefs.externalPlayerPath}
/>
</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 () {
const isFileHandler = this.props.state.unsaved.prefs.isFileHandler
if (isFileHandler) {
return (
<Preference>
<p>WebTorrent is your default torrent app. Hooray!</p>
</Preference>
)
}
return ( return (
<Preference> <Preference>
<p>WebTorrent is not currently the default torrent app.</p> <p>WebTorrent is not currently the default torrent app.</p>
<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) window.alert('TODO')
// var isFileHandler = state.unsaved.prefs.isFileHandler
// dispatch('updatePreferences', 'isFileHandler', !isFileHandler)
} }
render () { render () {
const style = {
color: colors.grey400,
marginLeft: 25,
marginRight: 25
}
return ( return (
<div style={style}> <div
style={{
color: colors.grey400,
marginLeft: 25,
marginRight: 25
}}
>
<PreferencesSection title='Downloads'> <PreferencesSection title='Downloads'>
{this.downloadPathSelector()} {this.downloadPathSelector()}
</PreferencesSection> </PreferencesSection>
@@ -156,7 +133,6 @@ 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>
) )
} }
@@ -170,12 +146,13 @@ class PreferencesSection extends React.Component {
} }
render () { render () {
const style = {
marginBottom: 25,
marginTop: 25
}
return ( return (
<div style={style}> <div
style={{
marginBottom: 25,
marginTop: 25
}}
>
<Heading level={2}>{this.props.title}</Heading> <Heading level={2}>{this.props.title}</Heading>
{this.props.children} {this.props.children}
</div> </div>
@@ -185,8 +162,15 @@ class PreferencesSection extends React.Component {
class Preference extends React.Component { class Preference extends React.Component {
render () { render () {
const style = { marginBottom: 10 } return (
return (<div style={style}>{this.props.children}</div>) <div
style={{
marginBottom: 10
}}
>
{this.props.children}
</div>
)
} }
} }

View File

@@ -4,13 +4,12 @@ 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 () {
const state = this.props.state var state = this.props.state
const contents = [] var contents = []
if (state.downloadPathStatus === 'missing') { if (state.downloadPathStatus === 'missing') {
contents.push( contents.push(
<div key='torrent-missing-path'> <div key='torrent-missing-path'>
@@ -22,7 +21,7 @@ module.exports = class TorrentList extends React.Component {
</div> </div>
) )
} }
const torrentElems = state.saved.torrents.map( var torrentElems = state.saved.torrents.map(
(torrentSummary) => this.renderTorrent(torrentSummary) (torrentSummary) => this.renderTorrent(torrentSummary)
) )
contents.push(...torrentElems) contents.push(...torrentElems)
@@ -40,29 +39,30 @@ module.exports = class TorrentList extends React.Component {
} }
renderTorrent (torrentSummary) { renderTorrent (torrentSummary) {
const state = this.props.state var state = this.props.state
const infoHash = torrentSummary.infoHash var infoHash = torrentSummary.infoHash
const isSelected = infoHash && state.selectedInfoHash === infoHash var 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
const style = {} var style = {}
if (torrentSummary.posterFileName) { if (torrentSummary.posterFileName) {
const gradient = 'linear-gradient(to bottom, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0) 100%)' var gradient = isSelected
const posterPath = TorrentSummary.getPosterPath(torrentSummary) ? 'linear-gradient(to bottom, rgba(0, 0, 0, 0.8) 0%, rgba(0, 0, 0, 0.4) 100%)'
style.backgroundImage = `${gradient}, url('${posterPath}')` : 'linear-gradient(to bottom, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0) 100%)'
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
const classes = ['torrent'] var 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 InvalidTorrentError('Missing torrentKey') if (!torrentSummary.torrentKey) throw new Error('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) {
const name = torrentSummary.name || 'Loading torrent...' var name = torrentSummary.name || 'Loading torrent...'
const elements = [( var 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
const prog = torrentSummary.progress var 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,7 +93,6 @@ 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()}
@@ -106,19 +105,14 @@ 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 () {
const progress = Math.floor(100 * prog.progress) var progress = Math.floor(100 * prog.progress)
return (<span key='percent-progress'>{progress}%</span>) return (<span key='percent-progress'>{progress}%</span>)
} }
function renderTotalProgress () { function renderTotalProgress () {
const downloaded = prettyBytes(prog.downloaded) var downloaded = prettyBytes(prog.downloaded)
const total = prettyBytes(prog.length || 0) var 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 {
@@ -128,7 +122,7 @@ module.exports = class TorrentList extends React.Component {
function renderPeers () { function renderPeers () {
if (prog.numPeers === 0) return if (prog.numPeers === 0) return
const count = prog.numPeers === 1 ? 'peer' : 'peers' var count = prog.numPeers === 1 ? 'peer' : 'peers'
return (<span key='peers'>{prog.numPeers} {count}</span>) return (<span key='peers'>{prog.numPeers} {count}</span>)
} }
@@ -143,22 +137,22 @@ module.exports = class TorrentList extends React.Component {
} }
function renderEta () { function renderEta () {
const downloaded = prog.downloaded var downloaded = prog.downloaded
const total = prog.length || 0 var total = prog.length || 0
const missing = total - downloaded var missing = total - downloaded
const downloadSpeed = prog.downloadSpeed var downloadSpeed = prog.downloadSpeed
if (downloadSpeed === 0 || missing === 0) return if (downloadSpeed === 0 || missing === 0) return
const rawEta = missing / downloadSpeed var rawEta = missing / downloadSpeed
const hours = Math.floor(rawEta / 3600) % 24 var hours = Math.floor(rawEta / 3600) % 24
const minutes = Math.floor(rawEta / 60) % 60 var minutes = Math.floor(rawEta / 60) % 60
const seconds = Math.floor(rawEta % 60) var 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
const hoursStr = hours ? hours + 'h' : '' var hoursStr = hours ? hours + 'h' : ''
const minutesStr = (hours || minutes) ? minutes + 'm' : '' var minutesStr = (hours || minutes) ? minutes + 'm' : ''
const secondsStr = seconds + 's' var secondsStr = seconds + 's'
return (<span>ETA: {hoursStr} {minutesStr} {secondsStr}</span>) return (<span>ETA: {hoursStr} {minutesStr} {secondsStr}</span>)
} }
@@ -167,9 +161,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) {
const infoHash = torrentSummary.infoHash var infoHash = torrentSummary.infoHash
let playIcon, playTooltip, playClass var 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.'
@@ -178,7 +172,7 @@ module.exports = class TorrentList extends React.Component {
playTooltip = 'Start streaming' playTooltip = 'Start streaming'
} }
let downloadIcon, downloadTooltip var 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.'
@@ -191,25 +185,26 @@ 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
let playButton, downloadButton, positionElem var 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:
const willShowSpinner = torrentSummary.playStatus === 'requested' var willShowSpinner = torrentSummary.playStatus === 'requested'
const mostRecentFile = torrentSummary.files && var defaultFile = torrentSummary.files &&
torrentSummary.files[torrentSummary.mostRecentFileIndex] torrentSummary.files[torrentSummary.defaultPlayFileIndex]
if (mostRecentFile && mostRecentFile.currentTime && !willShowSpinner) { if (defaultFile && defaultFile.currentTime && !willShowSpinner) {
const fraction = mostRecentFile.currentTime / mostRecentFile.duration var fraction = defaultFile.currentTime / defaultFile.duration
positionElem = this.renderRadialProgressBar(fraction, 'radial-progress-large') positionElem = this.renderRadialProgressBar(fraction, 'radial-progress-large')
playClass = 'resume-position' playClass = 'resume-position'
} }
@@ -220,7 +215,8 @@ 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>
) )
@@ -245,9 +241,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) {
let filesElement var filesElement
if (torrentSummary.error || !torrentSummary.files) { if (torrentSummary.error || !torrentSummary.files) {
let message = '' var 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)
@@ -268,9 +264,14 @@ 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
const fileRows = torrentSummary.files var 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 = (
@@ -295,28 +296,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?
const isSelected = torrentSummary.selections && torrentSummary.selections[index] var isSelected = torrentSummary.selections && torrentSummary.selections[index]
let isDone = false // Are we finished torrenting it? var isDone = false // Are we finished torrenting it?
let progress = '' var progress = ''
if (torrentSummary.progress && torrentSummary.progress.files && if (torrentSummary.progress && torrentSummary.progress.files &&
torrentSummary.progress.files[index]) { torrentSummary.progress.files[index]) {
const fileProg = torrentSummary.progress.files[index] var 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
let positionElem var 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
const isPlayable = TorrentPlayer.isPlayable(file) var isPlayable = TorrentPlayer.isPlayable(file)
const infoHash = torrentSummary.infoHash var infoHash = torrentSummary.infoHash
let icon var icon
let handleClick var 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)
@@ -327,7 +328,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
let rowClass = '' var 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 (
@@ -347,16 +348,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 deselect-file'>{isSelected ? 'close' : 'add'}</i> <i className='icon'>{isSelected ? 'close' : 'add'}</i>
</td> </td>
</tr> </tr>
) )
} }
renderRadialProgressBar (fraction, cssClass) { renderRadialProgressBar (fraction, cssClass) {
const rotation = 360 * fraction var rotation = 360 * fraction
const transformFill = {transform: 'rotate(' + (rotation / 2) + 'deg)'} var transformFill = {transform: 'rotate(' + (rotation / 2) + 'deg)'}
const transformFix = {transform: 'rotate(' + rotation + 'deg)'} var transformFix = {transform: 'rotate(' + rotation + 'deg)'}
return ( return (
<div key='radial-progress' className={'radial-progress ' + cssClass}> <div key='radial-progress' className={'radial-progress ' + cssClass}>
@@ -376,7 +377,7 @@ module.exports = class TorrentList extends React.Component {
} }
function getErrorMessage (torrentSummary) { function getErrorMessage (torrentSummary) {
const err = torrentSummary.error var err = torrentSummary.error
if (err === 'path-missing') { if (err === 'path-missing') {
return ( return (
<span> <span>

View File

@@ -1,216 +0,0 @@
const createTorrent = require('create-torrent')
const path = require('path')
const prettyBytes = require('prettier-bytes')
const React = require('react')
const {dispatch, dispatcher} = require('../lib/dispatcher')
const FlatButton = require('material-ui/FlatButton').default
const RaisedButton = require('material-ui/RaisedButton').default
const TextField = require('material-ui/TextField').default
const Checkbox = require('material-ui/Checkbox').default
const CreateTorrentErrorPage = require('../components/create-torrent-error-page')
const Heading = require('../components/heading')
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 {
constructor (props) {
super(props)
const state = this.props.state
const info = state.location.current()
// First, extract the base folder that the files are all in
let pathPrefix = info.folderPath
if (!pathPrefix) {
pathPrefix = info.files.map((x) => x.path).reduce(findCommonPrefix)
if (!pathPrefix.endsWith('/') && !pathPrefix.endsWith('\\')) {
pathPrefix = path.dirname(pathPrefix)
}
}
// Then, exclude .DS_Store and other dotfiles
const files = info.files
.filter((f) => !containsDots(f.path, pathPrefix))
.map((f) => ({name: f.name, path: f.path, size: f.size}))
if (files.length === 0) return (<CreateTorrentErrorPage state={state} />)
// 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.
let defaultName, basePath
if (files.length === 1) {
// Single file torrent: /a/b/foo.jpg -> torrent name 'foo.jpg', path '/a/b'
defaultName = files[0].name
basePath = pathPrefix
} else {
// Multi file torrent: /a/b/{foo, bar}.jpg -> torrent name 'b', path '/a'
defaultName = path.basename(pathPrefix)
basePath = path.dirname(pathPrefix)
}
// Default trackers
const trackers = createTorrent.announceList.join('\n')
this.state = {
comment: '',
isPrivate: false,
pathPrefix,
basePath,
defaultName,
files,
trackers
}
// Create React event handlers only once
this.setIsPrivate = (_, isPrivate) => this.setState({isPrivate})
this.setComment = (_, comment) => this.setState({comment})
this.setTrackers = (_, trackers) => this.setState({trackers})
this.handleSubmit = handleSubmit.bind(this)
}
render () {
const files = this.state.files
// Sanity check: show the number of files and total size
const numFiles = files.length
const totalBytes = files
.map((f) => f.size)
.reduce((a, b) => a + b, 0)
const torrentInfo = `${numFiles} files, ${prettyBytes(totalBytes)}`
return (
<div className='create-torrent'>
<Heading level={1}>Create torrent {this.state.defaultName}</Heading>
<div className='torrent-info'>{torrentInfo}</div>
<div className='torrent-attribute'>
<label>Path:</label>
<div>{this.state.pathPrefix}</div>
</div>
<ShowMore
style={{
marginBottom: 10
}}
hideLabel='Hide advanced settings...'
showLabel='Show advanced settings...'>
{this.renderAdvanced()}
</ShowMore>
<div className='float-right'>
<FlatButton
className='control cancel'
label='Cancel'
style={{
marginRight: 10
}}
onClick={dispatcher('cancel')} />
<RaisedButton
className='control create-torrent'
label='Create Torrent'
primary
onClick={this.handleSubmit} />
</div>
</div>
)
}
// Renders everything after clicking Show Advanced...:
// * Is Private? (private torrents, not announced to DHT)
// * Announce list (trackers)
// * Comment
renderAdvanced () {
// Create file list
const maxFileElems = 100
const files = this.state.files
const fileElems = files.slice(0, maxFileElems).map((file, i) => {
const relativePath = path.relative(this.state.pathPrefix, file.path)
return (<div key={i}>{relativePath}</div>)
})
if (files.length > maxFileElems) {
fileElems.push(<div key='more'>+ {files.length - maxFileElems} more</div>)
}
// Align the text fields
const textFieldStyle = { width: '' }
const textareaStyle = { margin: 0 }
return (
<div key='advanced' className='create-torrent-advanced'>
<div key='private' className='torrent-attribute'>
<label>Private:</label>
<Checkbox
className='torrent-is-private control'
style={{display: ''}}
checked={this.state.isPrivate}
onCheck={this.setIsPrivate} />
</div>
<div key='trackers' className='torrent-attribute'>
<label>Trackers:</label>
<TextField
className='torrent-trackers control'
style={textFieldStyle}
textareaStyle={textareaStyle}
multiLine
rows={2}
rowsMax={10}
value={this.state.trackers}
onChange={this.setTrackers} />
</div>
<div key='comment' className='torrent-attribute'>
<label>Comment:</label>
<TextField
className='torrent-comment control'
style={textFieldStyle}
textareaStyle={textareaStyle}
hintText='Optionally describe your torrent...'
multiLine
rows={2}
rowsMax={10}
value={this.state.comment}
onChange={this.setComment} />
</div>
<div key='files' className='torrent-attribute'>
<label>Files:</label>
<div>{fileElems}</div>
</div>
</div>
)
}
}
function handleSubmit () {
const announceList = this.state.trackers
.split('\n')
.map((s) => s.trim())
.filter((s) => s !== '')
const options = {
// 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.
name: this.state.defaultName,
path: this.state.basePath,
files: this.state.files,
announce: announceList,
private: this.state.isPrivate,
comment: this.state.comment.trim()
}
dispatch('createTorrent', options)
}
// Finds the longest common prefix
function findCommonPrefix (a, b) {
let i
for (i = 0; i < a.length && i < b.length; i++) {
if (a.charCodeAt(i) !== b.charCodeAt(i)) break
}
if (i === a.length) return a
if (i === b.length) return b
return a.substring(0, i)
}
function containsDots (path, pathPrefix) {
const suffix = path.substring(pathPrefix.length).replace(/\\/g, '/')
return ('/' + suffix).includes('/.')
}
module.exports = CreateTorrentPage

View File

@@ -2,27 +2,26 @@
// process from the main window. // process from the main window.
console.time('init') console.time('init')
const crypto = require('crypto') var crypto = require('crypto')
const deepEqual = require('deep-equal') var deepEqual = require('deep-equal')
const defaultAnnounceList = require('create-torrent').announceList var defaultAnnounceList = require('create-torrent').announceList
const electron = require('electron') var electron = require('electron')
const fs = require('fs-extra') var fs = require('fs-extra')
const musicmetadata = require('musicmetadata') var musicmetadata = require('musicmetadata')
const networkAddress = require('network-address') var networkAddress = require('network-address')
const path = require('path') var path = require('path')
const WebTorrent = require('webtorrent') var WebTorrent = require('webtorrent')
const zeroFill = require('zero-fill') var zeroFill = require('zero-fill')
const crashReporter = require('../crash-reporter') var crashReporter = require('../crash-reporter')
const config = require('../config') var config = require('../config')
const {TorrentKeyNotFoundError} = require('./lib/errors') var torrentPoster = require('./lib/torrent-poster')
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
const ipc = electron.ipcRenderer var 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
@@ -32,7 +31,7 @@ global.WEBTORRENT_ANNOUNCE = defaultAnnounceList
/** /**
* WebTorrent version. * WebTorrent version.
*/ */
const VERSION = require('../../package.json').version var 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.
@@ -40,7 +39,7 @@ const VERSION = require('../../package.json').version
* '0.16.1' -> '0016' * '0.16.1' -> '0016'
* '1.2.5' -> '0102' * '1.2.5' -> '0102'
*/ */
const VERSION_STR = VERSION.match(/([0-9]+)/g) var 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('')
@@ -52,31 +51,25 @@ const VERSION_STR = VERSION.match(/([0-9]+)/g)
* For example: * For example:
* '-WW0102-'... * '-WW0102-'...
*/ */
const VERSION_PREFIX = '-WD' + VERSION_STR + '-' var VERSION_PREFIX = '-WD' + VERSION_STR + '-'
/**
* Generate an ephemeral peer ID each time.
* Once there are around 2^24 = ~8 million WebTorrent Desktops online at the same time,
* ID collisions will start happening. Birthday paradox.
* This is fine, though. Bad peers can already clone someone else's peer ID.
* The network is robust to occasional collisions.
*/
const PEER_ID = Buffer.from(VERSION_PREFIX + crypto.randomBytes(6).toString('hex'))
// 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
let client = window.client = new WebTorrent({ peerId: PEER_ID }) var client = window.client = new WebTorrent({
peerId: Buffer.from(VERSION_PREFIX + crypto.randomBytes(6).toString('hex'))
})
// WebTorrent-to-HTTP streaming sever // WebTorrent-to-HTTP streaming sever
let server = null var server = null
// Used for diffing, so we only send progress updates when necessary // Used for diffing, so we only send progress updates when necessary
let prevProgress = null var prevProgress = null
init() init()
function init () { function init () {
listenToClientEvents() client.on('warning', (err) => ipc.send('wt-warning', null, err.message))
client.on('error', (err) => ipc.send('wt-error', null, err.message))
ipc.on('wt-start-torrenting', (e, torrentKey, torrentID, path, fileModtimes, selections) => ipc.on('wt-start-torrenting', (e, torrentKey, torrentID, path, fileModtimes, selections) =>
startTorrenting(torrentKey, torrentID, path, fileModtimes, selections)) startTorrenting(torrentKey, torrentID, path, fileModtimes, selections))
@@ -90,8 +83,10 @@ 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) => ipc.on('wt-get-mkv-subtitles', (e, infoHash, index) =>
startServer(infoHash)) getMKVSubtitles(infoHash, index))
ipc.on('wt-start-server', (e, infoHash, index) =>
startServer(infoHash, index))
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) =>
@@ -107,17 +102,12 @@ function init () {
console.timeEnd('init') console.timeEnd('init')
} }
function listenToClientEvents () { // Starts a given TorrentID, which can be an infohash, magnet URI, etc. Returns WebTorrent object
client.on('warning', (err) => ipc.send('wt-warning', null, err.message)) // See https://github.com/feross/webtorrent/blob/master/docs/api.md#clientaddtorrentid-opts-function-ontorrent-torrent-
client.on('error', (err) => ipc.send('wt-error', null, err.message))
}
// Starts a given TorrentID, which can be an infohash, magnet URI, etc.
// 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)
const torrent = client.add(torrentID, { var torrent = client.add(torrentID, {
path: path, path: path,
fileModtimes: fileModtimes fileModtimes: fileModtimes
}) })
@@ -133,15 +123,15 @@ function startTorrenting (torrentKey, torrentID, path, fileModtimes, selections)
} }
function stopTorrenting (infoHash) { function stopTorrenting (infoHash) {
const torrent = client.get(infoHash) var 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)
const paths = options.files.map((f) => f.path) var paths = options.files.map((f) => f.path)
const torrent = client.seed(paths, options) var 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')
@@ -159,22 +149,22 @@ function addTorrentEvents (torrent) {
torrent.on('done', torrentDone) torrent.on('done', torrentDone)
function torrentMetadata () { function torrentMetadata () {
const info = getTorrentInfo(torrent) var info = getTorrentInfo(torrent)
ipc.send('wt-metadata', torrent.key, info) ipc.send('wt-metadata', torrent.key, info)
updateTorrentProgress() updateTorrentProgress()
} }
function torrentReady () { function torrentReady () {
const info = getTorrentInfo(torrent) var 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) ipc.send('wt-ready-' + torrent.infoHash, torrent.key, info) // TODO: hack
updateTorrentProgress() updateTorrentProgress()
} }
function torrentDone () { function torrentDone () {
const info = getTorrentInfo(torrent) var info = getTorrentInfo(torrent)
ipc.send('wt-done', torrent.key, info) ipc.send('wt-done', torrent.key, info)
updateTorrentProgress() updateTorrentProgress()
@@ -210,9 +200,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) {
const torrent = getTorrent(torrentKey) var torrent = getTorrent(torrentKey)
checkIfTorrentFileExists(torrent.infoHash, function (torrentPath, exists) { checkIfTorrentFileExists(torrent.infoHash, function (torrentPath, exists) {
const fileName = torrent.infoHash + '.torrent' var 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)
@@ -232,7 +222,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) {
const torrentPath = path.join(config.TORRENT_PATH, infoHash + '.torrent') var torrentPath = path.join(config.TORRENT_PATH, infoHash + '.torrent')
fs.exists(torrentPath, function (exists) { fs.exists(torrentPath, function (exists) {
cb(torrentPath, exists) cb(torrentPath, exists)
}) })
@@ -241,14 +231,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) {
const torrent = getTorrent(torrentKey) var 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)
const posterFileName = torrent.infoHash + extension var posterFileName = torrent.infoHash + extension
const posterFilePath = path.join(config.POSTER_PATH, posterFileName) var 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
@@ -259,7 +249,7 @@ function generateTorrentPoster (torrentKey) {
} }
function updateTorrentProgress () { function updateTorrentProgress () {
const progress = getTorrentProgress() var 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 */
@@ -270,19 +260,19 @@ function updateTorrentProgress () {
function getTorrentProgress () { function getTorrentProgress () {
// First, track overall progress // First, track overall progress
const progress = client.progress var progress = client.progress
const hasActiveTorrents = client.torrents.some(function (torrent) { var 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
const torrentProg = client.torrents.map(function (torrent) { var torrentProg = client.torrents.map(function (torrent) {
const fileProg = torrent.files && torrent.files.map(function (file, index) { var fileProg = torrent.files && torrent.files.map(function (file, index) {
const numPieces = file._endPiece - file._startPiece + 1 var numPieces = file._endPiece - file._startPiece + 1
let numPiecesPresent = 0 var numPiecesPresent = 0
for (let piece = file._startPiece; piece <= file._endPiece; piece++) { for (var piece = file._startPiece; piece <= file._endPiece; piece++) {
if (torrent.bitfield.get(piece)) numPiecesPresent++ if (torrent.bitfield.get(piece)) numPiecesPresent++
} }
return { return {
@@ -313,28 +303,28 @@ function getTorrentProgress () {
} }
} }
function startServer (infoHash) { function startServer (infoHash, index) {
const torrent = client.get(infoHash) var torrent = client.get(infoHash)
if (torrent.ready) startServerFromReadyTorrent(torrent) if (torrent.ready) startServerFromReadyTorrent(torrent, index)
else torrent.once('ready', () => startServerFromReadyTorrent(torrent)) else torrent.once('ready', () => startServerFromReadyTorrent(torrent, index))
} }
function startServerFromReadyTorrent (torrent, cb) { function startServerFromReadyTorrent (torrent, index, 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 () {
const port = server.address().port var port = server.address().port
const urlSuffix = ':' + port var urlSuffix = ':' + port + '/' + index
const info = { var 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) ipc.send('wt-server-' + torrent.infoHash, info) // TODO: hack
}) })
} }
@@ -345,8 +335,8 @@ function stopServer () {
} }
function getAudioMetadata (infoHash, index) { function getAudioMetadata (infoHash, index) {
const torrent = client.get(infoHash) var torrent = client.get(infoHash)
const file = torrent.files[index] var 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)
@@ -354,17 +344,40 @@ function getAudioMetadata (infoHash, index) {
}) })
} }
function getMKVSubtitles (infoHash, index) {
var torrent = client.get(infoHash)
var file = torrent.files[index]
var MatroskaSubtitles = require('matroska-subtitles')
var subtitleParser = new MatroskaSubtitles()
var textTracks = new Map()
subtitleParser.once('tracks', function (tracks) {
tracks.forEach(function (track) {
textTracks.set(track.number, { track: track, subtitles: [] })
})
})
subtitleParser.on('subtitle', function (subtitle, trackNumber) {
textTracks.get(trackNumber).subtitles.push(subtitle)
})
subtitleParser.on('finish', function () {
ipc.send('wt-mkv-subtitles', Array.from(textTracks.values()))
})
file.createReadStream().pipe(subtitleParser)
}
function selectFiles (torrentOrInfoHash, selections) { function selectFiles (torrentOrInfoHash, selections) {
// Get the torrent object // Get the torrent object
let torrent var 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
@@ -384,8 +397,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 (let i = 0; i < selections.length; i++) { for (var i = 0; i < selections.length; i++) {
const file = torrent.files[i] var file = torrent.files[i]
if (selections[i]) { if (selections[i]) {
file.select() file.select()
} else { } else {
@@ -398,25 +411,11 @@ 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) {
const ret = client.torrents.find((x) => x.key === torrentKey) var ret = client.torrents.find((x) => x.key === torrentKey)
if (!ret) throw new TorrentKeyNotFoundError(torrentKey) if (!ret) throw new Error('missing torrent key ' + torrentKey)
return ret return ret
} }
function onError (err) { function onError (err) {
console.log(err) console.log(err)
} }
// TODO: remove this once the following bugs are fixed:
// https://bugs.chromium.org/p/chromium/issues/detail?id=490143
// https://github.com/electron/electron/issues/7212
window.testOfflineMode = function () {
console.log('Test, going OFFLINE')
client = window.client = new WebTorrent({
peerId: PEER_ID,
tracker: false,
dht: false,
webSeeds: false
})
listenToClientEvents()
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 225 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 233 B

View File

@@ -32,7 +32,6 @@
<p> <p>
Version <script>document.write(require('../package.json').version)</script> Version <script>document.write(require('../package.json').version)</script>
(<script>document.write(require('webtorrent/package.json').version)</script>) (<script>document.write(require('webtorrent/package.json').version)</script>)
(<script>document.write(process.arch === 'x64' ? '64-bit' : '32-bit')</script>)
</p> </p>
<p><script>document.write(require('../config').APP_COPYRIGHT)</script></p> <p><script>document.write(require('../config').APP_COPYRIGHT)</script></p>
</body> </body>

View File

@@ -265,6 +265,10 @@ 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;
@@ -272,7 +276,6 @@ table {
.create-torrent .torrent-attribute { .create-torrent .torrent-attribute {
white-space: nowrap; white-space: nowrap;
margin: 8px 0;
} }
.create-torrent .torrent-attribute>* { .create-torrent .torrent-attribute>* {
@@ -280,12 +283,13 @@ table {
} }
.create-torrent .torrent-attribute label { .create-torrent .torrent-attribute label {
width: 100px; width: 60px;
margin-right: 10px;
vertical-align: top; vertical-align: top;
} }
.create-torrent .torrent-attribute>div { .create-torrent .torrent-attribute>div {
width: calc(100% - 100px); width: calc(100% - 90px);
} }
.create-torrent .torrent-attribute div { .create-torrent .torrent-attribute div {
@@ -294,6 +298,18 @@ table {
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.create-torrent .torrent-attribute textarea {
width: calc(100% - 80px);
height: 80px;
color: #eee;
background-color: transparent;
line-height: 1.5;
font-size: 14px;
font-family: inherit;
border-radius: 2px;
padding: 4px 6px;
}
/* /*
* BUTTONS * BUTTONS
*/ */
@@ -318,15 +334,6 @@ 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
*/ */
@@ -350,6 +357,10 @@ textarea,
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,20 +466,6 @@ textarea,
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
*/ */
@@ -520,7 +517,6 @@ body.drag .app::after {
/* /*
* TORRENT LIST: EXPANDED TORRENT DETAILS * TORRENT LIST: EXPANDED TORRENT DETAILS
*/ */
.torrent.selected { .torrent.selected {
height: auto; height: auto;
} }
@@ -544,6 +540,10 @@ 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;
@@ -640,15 +640,11 @@ body.drag .app::after {
opacity: 1; opacity: 1;
} }
.player .controls .icon.disabled { .player .controls .play-pause {
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: 5px; margin-top: 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>Main Window</title> <title>WebTorrent Desktop</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 Hidden Window</title> <title>WebTorrent Desktop</title>
<style> <style>
body { body {
background-color: #282828; background-color: #282828;

View File

@@ -1,14 +0,0 @@
const path = require('path')
const TEST_DIR = path.join(__dirname, 'tempTestData')
const TEST_DIR_DOWNLOAD = path.join(TEST_DIR, 'Downloads')
const TEST_DIR_DESKTOP = path.join(TEST_DIR, 'Desktop')
module.exports = {
TORRENT_FILES: [path.join(__dirname, 'resources', '1.torrent')],
SEED_FILES: [path.join(TEST_DIR_DESKTOP, 'tmp.jpg')],
SAVED_TORRENT_FILE: path.join(TEST_DIR_DESKTOP, 'saved.torrent'),
TEST_DIR,
TEST_DIR_DOWNLOAD,
TEST_DIR_DESKTOP
}

View File

@@ -1,20 +0,0 @@
const test = require('tape')
const setup = require('./setup')
test.onFinish(setup.deleteTestDataDir)
test('app runs', function (t) {
t.timeoutAfter(10e3)
setup.resetTestDataDir()
const app = setup.createApp()
setup.waitForLoad(app, t)
.then(() => setup.wait())
.then(() => setup.screenshotCreateOrCompare(app, t, 'app-basic'))
.then(() => setup.endTest(app, t),
(err) => setup.endTest(app, t, err || 'error'))
})
require('./test-torrent-list')
require('./test-add-torrent')
require('./test-video')
require('./test-audio')

View File

@@ -1,15 +0,0 @@
const electron = require('electron')
const config = require('./config')
console.log('Mocking electron.dialog.showOpenDialog...')
electron.dialog.showOpenDialog = function (win, opts, cb) {
const ret = /select.*torrent file/i.test(opts.title)
? config.TORRENT_FILES
: config.SEED_FILES
cb(ret)
}
console.log('Mocking electron.remote.dialog.showSaveDialog...')
electron.dialog.showSaveDialog = function (win, opts, cb) {
cb(config.SAVED_TORRENT_FILE)
}

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 302 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 777 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 478 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 480 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 480 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 480 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 481 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 701 KiB

Some files were not shown because too many files have changed in this diff Show More