Compare commits
50 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9cdc73edce | ||
|
|
3d254fa075 | ||
|
|
ed1e43015e | ||
|
|
e6cbbd73f0 | ||
|
|
67dff7b38c | ||
|
|
ced67176a3 | ||
|
|
00ac8afe64 | ||
|
|
a6964c4495 | ||
|
|
6541291e0d | ||
|
|
711d274398 | ||
|
|
6c5861b9fc | ||
|
|
f7ab27f9fd | ||
|
|
e4e789cc5b | ||
|
|
09b525fe58 | ||
|
|
9dabfc1367 | ||
|
|
bdb733352a | ||
|
|
75a4655a0f | ||
|
|
051c1516a0 | ||
|
|
62c5b78358 | ||
|
|
290913d07a | ||
|
|
77534d650a | ||
|
|
a4c715e3f6 | ||
|
|
7415d3cee5 | ||
|
|
e1ba9c89fe | ||
|
|
0fcbe7369a | ||
|
|
c8087b5b63 | ||
|
|
065faca8eb | ||
|
|
bcd6a38a05 | ||
|
|
fa67f9b82b | ||
|
|
39acd0bd47 | ||
|
|
c549fcfc27 | ||
|
|
45e838d4c3 | ||
|
|
64f49e4d4f | ||
|
|
61caa90901 | ||
|
|
3e85289318 | ||
|
|
a629f287f0 | ||
|
|
3a4906079b | ||
|
|
3edf21f457 | ||
|
|
785c44cd2a | ||
|
|
1ad8a5313b | ||
|
|
967e161288 | ||
|
|
fe8c3b190c | ||
|
|
993e7d77ad | ||
|
|
e0be052df4 | ||
|
|
d331bae548 | ||
|
|
d88229694a | ||
|
|
8da5b955d6 | ||
|
|
54882679c1 | ||
|
|
f2007be1b0 | ||
|
|
7a757f9e05 |
2
.gitignore
vendored
@@ -1,4 +1,4 @@
|
||||
node_modules/
|
||||
build/
|
||||
dist/
|
||||
npm-debug.log.*
|
||||
npm-debug.log*
|
||||
|
||||
@@ -32,5 +32,6 @@
|
||||
- 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.
|
||||
|
||||
22
CHANGELOG.md
@@ -1,5 +1,27 @@
|
||||
# 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
|
||||
|
||||
22
README.md
@@ -56,6 +56,28 @@ Restart the app automatically every time code changes. Useful during development
|
||||
$ 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
|
||||
|
||||
Builds app binaries for Mac, Linux, and Windows.
|
||||
|
||||
109
bin/package.js
@@ -112,7 +112,7 @@ var darwin = {
|
||||
// Build for Mac
|
||||
platform: 'darwin',
|
||||
|
||||
// Build 64 bit binaries only.
|
||||
// Build x64 binaries only.
|
||||
arch: 'x64',
|
||||
|
||||
// The bundle identifier to use in the application's plist (Mac only).
|
||||
@@ -133,8 +133,8 @@ var win32 = {
|
||||
// Build for Windows.
|
||||
platform: 'win32',
|
||||
|
||||
// Build 32 bit binaries only.
|
||||
arch: 'ia32',
|
||||
// Build ia32 and x64 binaries.
|
||||
arch: 'all',
|
||||
|
||||
// Object hash of application metadata to embed into the executable (Windows only)
|
||||
'version-string': {
|
||||
@@ -167,7 +167,7 @@ var linux = {
|
||||
// Build for Linux.
|
||||
platform: 'linux',
|
||||
|
||||
// Build 32 and 64 bit binaries.
|
||||
// Build ia32 and x64 binaries.
|
||||
arch: 'all'
|
||||
|
||||
// Note: Application icon for Linux is specified via the BrowserWindow `icon` option.
|
||||
@@ -388,19 +388,25 @@ function buildWin32 (cb) {
|
||||
}
|
||||
|
||||
var tasks = []
|
||||
if (argv.package === 'exe' || argv.package === 'all') {
|
||||
tasks.push((cb) => packageInstaller(cb))
|
||||
}
|
||||
if (argv.package === 'portable' || argv.package === 'all') {
|
||||
tasks.push((cb) => packagePortable(cb))
|
||||
}
|
||||
buildPath.forEach(function (filesPath) {
|
||||
var destArch = filesPath.split('-').pop()
|
||||
|
||||
if (argv.package === 'exe' || argv.package === 'all') {
|
||||
tasks.push((cb) => packageInstaller(filesPath, destArch, cb))
|
||||
}
|
||||
if (argv.package === 'portable' || argv.package === 'all') {
|
||||
tasks.push((cb) => packagePortable(filesPath, destArch, cb))
|
||||
}
|
||||
})
|
||||
series(tasks, cb)
|
||||
|
||||
function packageInstaller (cb) {
|
||||
console.log('Windows: Creating installer...')
|
||||
function packageInstaller (filesPath, destArch, cb) {
|
||||
console.log(`Windows: Creating ${destArch} installer...`)
|
||||
|
||||
var archStr = destArch === 'ia32' ? '-ia32' : ''
|
||||
|
||||
installer.createWindowsInstaller({
|
||||
appDirectory: buildPath[0],
|
||||
appDirectory: filesPath,
|
||||
authors: config.APP_TEAM,
|
||||
description: config.APP_NAME,
|
||||
exe: config.APP_NAME + '.exe',
|
||||
@@ -410,8 +416,21 @@ function buildWin32 (cb) {
|
||||
noMsi: true,
|
||||
outputDirectory: DIST_PATH,
|
||||
productName: config.APP_NAME,
|
||||
remoteReleases: config.GITHUB_URL,
|
||||
setupExe: config.APP_NAME + 'Setup-v' + config.APP_VERSION + '.exe',
|
||||
/**
|
||||
* Only create delta updates for the Windows x64 build because 90% of our
|
||||
* 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',
|
||||
signWithParams: signWithParams,
|
||||
title: config.APP_NAME,
|
||||
@@ -419,23 +438,65 @@ function buildWin32 (cb) {
|
||||
version: pkg.version
|
||||
})
|
||||
.then(function () {
|
||||
console.log('Windows: Created installer.')
|
||||
console.log(`Windows: Created ${destArch} 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)
|
||||
})
|
||||
.catch(cb)
|
||||
}
|
||||
|
||||
function packagePortable (cb) {
|
||||
console.log('Windows: Creating portable app...')
|
||||
function packagePortable (filesPath, destArch, cb) {
|
||||
console.log(`Windows: Creating ${destArch} portable app...`)
|
||||
|
||||
var portablePath = path.join(buildPath[0], 'Portable Settings')
|
||||
var portablePath = path.join(filesPath, 'Portable Settings')
|
||||
mkdirp.sync(portablePath)
|
||||
|
||||
var inPath = path.join(DIST_PATH, path.basename(buildPath[0]))
|
||||
var outPath = path.join(DIST_PATH, BUILD_NAME + '-win.zip')
|
||||
var archStr = destArch === 'ia32' ? '-ia32' : ''
|
||||
|
||||
var inPath = path.join(DIST_PATH, path.basename(filesPath))
|
||||
var outPath = path.join(DIST_PATH, BUILD_NAME + '-win' + archStr + '.zip')
|
||||
zip.zipSync(inPath, outPath)
|
||||
|
||||
console.log('Windows: Created portable app.')
|
||||
console.log(`Windows: Created ${destArch} portable app.`)
|
||||
cb(null)
|
||||
}
|
||||
})
|
||||
@@ -500,8 +561,10 @@ function buildLinux (cb) {
|
||||
// Create .zip file for Linux
|
||||
console.log(`Linux: Creating ${destArch} zip...`)
|
||||
|
||||
var archStr = destArch === 'ia32' ? '-ia32' : ''
|
||||
|
||||
var inPath = path.join(DIST_PATH, path.basename(filesPath))
|
||||
var outPath = path.join(DIST_PATH, BUILD_NAME + '-linux-' + destArch + '.zip')
|
||||
var outPath = path.join(DIST_PATH, BUILD_NAME + '-linux' + archStr + '.zip')
|
||||
zip.zipSync(inPath, outPath)
|
||||
|
||||
console.log(`Linux: Created ${destArch} zip.`)
|
||||
|
||||
11
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "webtorrent-desktop",
|
||||
"description": "WebTorrent, the streaming torrent client. For Mac, Windows, and Linux.",
|
||||
"version": "0.14.0",
|
||||
"version": "0.16.0",
|
||||
"author": {
|
||||
"name": "WebTorrent, LLC",
|
||||
"email": "feross@webtorrent.io",
|
||||
@@ -16,13 +16,16 @@
|
||||
"dependencies": {
|
||||
"airplayer": "^2.0.0",
|
||||
"application-config": "^1.0.0",
|
||||
"auto-launch": "^4.0.1",
|
||||
"bitfield": "^1.0.2",
|
||||
"capture-frame": "^1.0.0",
|
||||
"chromecasts": "^1.8.0",
|
||||
"create-torrent": "^3.24.5",
|
||||
"deep-equal": "^1.0.1",
|
||||
"dlnacasts": "^0.1.0",
|
||||
"drag-drop": "^2.12.1",
|
||||
"electron": "1.3.3",
|
||||
"electron": "1.4.0",
|
||||
"es6-error": "^3.0.1",
|
||||
"fs-extra": "^0.30.0",
|
||||
"iso-639-1": "^1.2.1",
|
||||
"languagedetect": "^1.1.1",
|
||||
@@ -63,9 +66,12 @@
|
||||
"nodemon": "^1.10.2",
|
||||
"open": "0.0.5",
|
||||
"plist": "^2.0.1",
|
||||
"pngjs": "^3.0.0",
|
||||
"rimraf": "^2.5.2",
|
||||
"run-series": "^1.1.4",
|
||||
"spectron": "^3.3.0",
|
||||
"standard": "*",
|
||||
"tape": "^4.6.0",
|
||||
"walk-sync": "^0.3.1"
|
||||
},
|
||||
"engines": {
|
||||
@@ -99,6 +105,7 @@
|
||||
"package": "node ./bin/package.js",
|
||||
"prepublish": "npm run build",
|
||||
"start": "npm run build && electron .",
|
||||
"integration-test": "npm run build && node ./test",
|
||||
"test": "standard && depcheck --ignores=babel-cli,nodemon,gh-release --ignore-dirs=build,dist && node ./bin/extra-lint.js",
|
||||
"gh-release": "gh-release",
|
||||
"update-authors": "./bin/update-authors.sh",
|
||||
|
||||
@@ -1,12 +1,22 @@
|
||||
const appConfig = require('application-config')('WebTorrent')
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const electron = require('electron')
|
||||
|
||||
const APP_NAME = 'WebTorrent'
|
||||
const APP_TEAM = 'WebTorrent, LLC'
|
||||
const APP_VERSION = require('../package.json').version
|
||||
|
||||
const PORTABLE_PATH = path.join(path.dirname(process.execPath), 'Portable Settings')
|
||||
const IS_TEST = isTest()
|
||||
const PORTABLE_PATH = IS_TEST
|
||||
? path.join(__dirname, '../test/tempTestData')
|
||||
: path.join(path.dirname(process.execPath), 'Portable Settings')
|
||||
const IS_PORTABLE = isPortable()
|
||||
const IS_PRODUCTION = isProduction()
|
||||
|
||||
console.log('Production: %s portable: %s test: %s',
|
||||
IS_PRODUCTION, IS_PORTABLE, IS_TEST)
|
||||
if (IS_PORTABLE) console.log('Portable path: %s', PORTABLE_PATH)
|
||||
|
||||
module.exports = {
|
||||
ANNOUNCEMENT_URL: 'https://webtorrent.io/desktop/announcement',
|
||||
@@ -26,26 +36,31 @@ module.exports = {
|
||||
|
||||
DEFAULT_TORRENTS: [
|
||||
{
|
||||
testID: 'bbb',
|
||||
name: 'Big Buck Bunny',
|
||||
posterFileName: 'bigBuckBunny.jpg',
|
||||
torrentFileName: 'bigBuckBunny.torrent'
|
||||
},
|
||||
{
|
||||
testID: 'cosmos',
|
||||
name: 'Cosmos Laundromat (Preview)',
|
||||
posterFileName: 'cosmosLaundromat.jpg',
|
||||
torrentFileName: 'cosmosLaundromat.torrent'
|
||||
},
|
||||
{
|
||||
testID: 'sintel',
|
||||
name: 'Sintel',
|
||||
posterFileName: 'sintel.jpg',
|
||||
torrentFileName: 'sintel.torrent'
|
||||
},
|
||||
{
|
||||
testID: 'tears',
|
||||
name: 'Tears of Steel',
|
||||
posterFileName: 'tearsOfSteel.jpg',
|
||||
torrentFileName: 'tearsOfSteel.torrent'
|
||||
},
|
||||
{
|
||||
testID: 'wired',
|
||||
name: 'The WIRED CD - Rip. Sample. Mash. Share.',
|
||||
posterFileName: 'wiredCd.jpg',
|
||||
torrentFileName: 'wiredCd.torrent'
|
||||
@@ -62,8 +77,11 @@ module.exports = {
|
||||
|
||||
HOME_PAGE_URL: 'https://webtorrent.io',
|
||||
|
||||
IS_PORTABLE: isPortable(),
|
||||
IS_PRODUCTION: isProduction(),
|
||||
IS_PORTABLE: IS_PORTABLE,
|
||||
IS_PRODUCTION: IS_PRODUCTION,
|
||||
IS_TEST: IS_TEST,
|
||||
|
||||
OS_SYSARCH: is64BitOperatingSystem() ? 'x64' : 'ia32',
|
||||
|
||||
POSTER_PATH: path.join(getConfigPath(), 'Posters'),
|
||||
ROOT_PATH: path.join(__dirname, '..'),
|
||||
@@ -79,7 +97,7 @@ module.exports = {
|
||||
}
|
||||
|
||||
function getConfigPath () {
|
||||
if (isPortable()) {
|
||||
if (IS_PORTABLE) {
|
||||
return PORTABLE_PATH
|
||||
} else {
|
||||
return path.dirname(appConfig.filePath)
|
||||
@@ -89,22 +107,31 @@ function getConfigPath () {
|
||||
function getDefaultDownloadPath () {
|
||||
if (!process || !process.type) {
|
||||
return ''
|
||||
}
|
||||
|
||||
if (isPortable()) {
|
||||
} else if (IS_PORTABLE) {
|
||||
return path.join(getConfigPath(), 'Downloads')
|
||||
} else {
|
||||
return getPath('downloads')
|
||||
}
|
||||
}
|
||||
|
||||
const electron = require('electron')
|
||||
function getPath (key) {
|
||||
if (process.type === 'renderer') {
|
||||
return electron.remote.app.getPath(key)
|
||||
} else {
|
||||
return electron.app.getPath(key)
|
||||
}
|
||||
}
|
||||
|
||||
return process.type === 'renderer'
|
||||
? electron.remote.app.getPath('downloads')
|
||||
: electron.app.getPath('downloads')
|
||||
function isTest () {
|
||||
return process.env.NODE_ENV === 'test'
|
||||
}
|
||||
|
||||
function isPortable () {
|
||||
if (IS_TEST) {
|
||||
return true
|
||||
}
|
||||
try {
|
||||
return process.platform === 'win32' && isProduction() && !!fs.statSync(PORTABLE_PATH)
|
||||
return process.platform === 'win32' && IS_PRODUCTION && !!fs.statSync(PORTABLE_PATH)
|
||||
} catch (err) {
|
||||
return false
|
||||
}
|
||||
@@ -124,3 +151,32 @@ function isProduction () {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -21,12 +21,7 @@ function openSeedFile () {
|
||||
title: 'Select a file for the torrent.',
|
||||
properties: [ 'openFile' ]
|
||||
}
|
||||
setTitle(opts.title)
|
||||
electron.dialog.showOpenDialog(windows.main.win, opts, function (selectedPaths) {
|
||||
resetTitle()
|
||||
if (!Array.isArray(selectedPaths)) return
|
||||
windows.main.dispatch('showCreateTorrent', selectedPaths)
|
||||
})
|
||||
showOpenSeed(opts)
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -46,12 +41,7 @@ function openSeedDirectory () {
|
||||
title: 'Select a folder for the torrent.',
|
||||
properties: [ 'openDirectory' ]
|
||||
}
|
||||
setTitle(opts.title)
|
||||
electron.dialog.showOpenDialog(windows.main.win, opts, function (selectedPaths) {
|
||||
resetTitle()
|
||||
if (!Array.isArray(selectedPaths)) return
|
||||
windows.main.dispatch('showCreateTorrent', selectedPaths)
|
||||
})
|
||||
showOpenSeed(opts)
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -119,3 +109,16 @@ function setTitle (title) {
|
||||
function 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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ module.exports = {
|
||||
}
|
||||
|
||||
const cp = require('child_process')
|
||||
const path = require('path')
|
||||
const vlcCommand = require('vlc-command')
|
||||
|
||||
const log = require('./log')
|
||||
@@ -13,15 +14,15 @@ const windows = require('./windows')
|
||||
// holds a ChildProcess while we're playing a video in an external player, null otherwise
|
||||
let proc = null
|
||||
|
||||
function checkInstall (path, cb) {
|
||||
function checkInstall (playerPath, cb) {
|
||||
// check for VLC if external player has not been specified by the user
|
||||
// otherwise assume the player is installed
|
||||
if (path == null) return vlcCommand((err) => cb(!err))
|
||||
if (playerPath == null) return vlcCommand((err) => cb(!err))
|
||||
process.nextTick(() => cb(true))
|
||||
}
|
||||
|
||||
function spawn (path, url, title) {
|
||||
if (path != null) return spawnExternal(path, [url])
|
||||
function spawn (playerPath, url, title) {
|
||||
if (playerPath != null) return spawnExternal(playerPath, [url])
|
||||
|
||||
// Try to find and use VLC if external player is not specified
|
||||
vlcCommand(function (err, vlcPath) {
|
||||
@@ -44,10 +45,15 @@ function kill () {
|
||||
proc = null
|
||||
}
|
||||
|
||||
function spawnExternal (path, args) {
|
||||
log('Running external media player:', path + ' ' + args.join(' '))
|
||||
function spawnExternal (playerPath, args) {
|
||||
log('Running external media player:', playerPath + ' ' + args.join(' '))
|
||||
|
||||
proc = cp.spawn(path, args, {stdio: 'ignore'})
|
||||
if (path.extname(playerPath) === '.app') {
|
||||
// Mac: Use executable in packaged .app bundle
|
||||
playerPath += '/Contents/MacOS/' + path.basename(playerPath, '.app')
|
||||
}
|
||||
|
||||
proc = cp.spawn(playerPath, args, {stdio: 'ignore'})
|
||||
|
||||
// If it works, close the modal after a second
|
||||
const closeModalTimeout = setTimeout(() =>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
console.time('init')
|
||||
|
||||
const electron = require('electron')
|
||||
|
||||
const app = electron.app
|
||||
const ipcMain = electron.ipcMain
|
||||
|
||||
@@ -22,6 +21,11 @@ const windows = require('./windows')
|
||||
let shouldQuit = false
|
||||
let argv = sliceArgv(process.argv)
|
||||
|
||||
// Start the app without showing the main window when auto launching on login
|
||||
// (On Windows and Linux, we get a flag. On MacOS, we get special API.)
|
||||
const hidden = argv.includes('--hidden') ||
|
||||
(process.platform === 'darwin' && app.getLoginItemSettings().wasOpenedAsHidden)
|
||||
|
||||
if (config.IS_PRODUCTION) {
|
||||
// When Electron is running in production mode (packaged app), then run React
|
||||
// in production mode too.
|
||||
@@ -68,7 +72,7 @@ function init () {
|
||||
app.on('ready', function () {
|
||||
isReady = true
|
||||
|
||||
windows.main.init()
|
||||
windows.main.init({hidden: hidden})
|
||||
windows.webtorrent.init()
|
||||
menu.init()
|
||||
|
||||
@@ -143,8 +147,14 @@ 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) {
|
||||
return argv.slice(config.IS_PRODUCTION ? 1 : 2)
|
||||
return argv.slice(config.IS_PRODUCTION ? 1
|
||||
: config.IS_TEST ? 4
|
||||
: 2)
|
||||
}
|
||||
|
||||
function processArgv (argv) {
|
||||
@@ -156,9 +166,15 @@ function processArgv (argv) {
|
||||
dialog.openTorrentFile()
|
||||
} else if (arg === '-u') {
|
||||
dialog.openTorrentAddress()
|
||||
} else if (arg === '--hidden') {
|
||||
// Ignore hidden argument, already being handled
|
||||
} else if (arg.startsWith('-psn')) {
|
||||
// Ignore Mac launchd "process serial number" argument
|
||||
// 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 !== '.') {
|
||||
// Ignore '.' argument, which gets misinterpreted as a torrent id, when a
|
||||
// development copy of WebTorrent is started while a production version is
|
||||
|
||||
@@ -17,6 +17,7 @@ const shortcuts = require('./shortcuts')
|
||||
const externalPlayer = require('./external-player')
|
||||
const windows = require('./windows')
|
||||
const thumbar = require('./thumbar')
|
||||
const startup = require('./startup')
|
||||
|
||||
// Messages from the main process, to be sent once the WebTorrent process starts
|
||||
const messageQueueMainToWebTorrent = []
|
||||
@@ -58,7 +59,7 @@ function init () {
|
||||
*/
|
||||
|
||||
ipc.on('onPlayerOpen', function () {
|
||||
menu.setPlayerOpen(true)
|
||||
menu.togglePlaybackControls(true)
|
||||
powerSaveBlocker.enable()
|
||||
shortcuts.enable()
|
||||
thumbar.enable()
|
||||
@@ -70,7 +71,7 @@ function init () {
|
||||
})
|
||||
|
||||
ipc.on('onPlayerClose', function () {
|
||||
menu.setPlayerOpen(false)
|
||||
menu.togglePlaybackControls(false)
|
||||
powerSaveBlocker.disable()
|
||||
shortcuts.disable()
|
||||
thumbar.disable()
|
||||
@@ -97,11 +98,21 @@ function init () {
|
||||
/**
|
||||
* File handlers
|
||||
*/
|
||||
|
||||
ipc.on('setDefaultFileHandler', (e, flag) => {
|
||||
if (flag) handlers.install()
|
||||
else handlers.uninstall()
|
||||
})
|
||||
|
||||
/**
|
||||
* Auto start on login
|
||||
*/
|
||||
|
||||
ipc.on('setStartup', (e, flag) => {
|
||||
if (flag) startup.install()
|
||||
else startup.uninstall()
|
||||
})
|
||||
|
||||
/**
|
||||
* Windows: Main
|
||||
*/
|
||||
@@ -126,10 +137,18 @@ function init () {
|
||||
})
|
||||
})
|
||||
|
||||
ipc.on('openExternalPlayer', (e, ...args) => externalPlayer.spawn(...args))
|
||||
ipc.on('openExternalPlayer', (e, ...args) => {
|
||||
menu.togglePlaybackControls(false)
|
||||
thumbar.disable()
|
||||
externalPlayer.spawn(...args)
|
||||
})
|
||||
|
||||
ipc.on('quitExternalPlayer', () => externalPlayer.kill())
|
||||
|
||||
// Capture all events
|
||||
/**
|
||||
* Message passing
|
||||
*/
|
||||
|
||||
const oldEmit = ipc.emit
|
||||
ipc.emit = function (name, e, ...args) {
|
||||
// Relay messages between the main window and the WebTorrent hidden window
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
module.exports = {
|
||||
init,
|
||||
setPlayerOpen,
|
||||
togglePlaybackControls,
|
||||
setWindowFocus,
|
||||
setAllowNav,
|
||||
onPlayerUpdate,
|
||||
@@ -24,7 +24,7 @@ function init () {
|
||||
electron.Menu.setApplicationMenu(menu)
|
||||
}
|
||||
|
||||
function setPlayerOpen (flag) {
|
||||
function togglePlaybackControls (flag) {
|
||||
getMenuItem('Play/Pause').enabled = flag
|
||||
getMenuItem('Skip Next').enabled = flag
|
||||
getMenuItem('Skip Previous').enabled = flag
|
||||
|
||||
36
src/main/startup.js
Normal file
@@ -0,0 +1,36 @@
|
||||
module.exports = {
|
||||
install,
|
||||
uninstall
|
||||
}
|
||||
|
||||
const config = require('../config')
|
||||
const AutoLaunch = require('auto-launch')
|
||||
const { app } = require('electron')
|
||||
|
||||
// On Mac, work around a bug in auto-launch where it opens a Terminal window
|
||||
// See https://github.com/Teamwork/node-auto-launch/issues/28#issuecomment-222194437
|
||||
const appPath = process.platform === 'darwin'
|
||||
? app.getPath('exe').replace(/\.app\/Content.*/, '.app')
|
||||
: undefined // Use the default
|
||||
|
||||
const appLauncher = new AutoLaunch({
|
||||
name: config.APP_NAME,
|
||||
path: appPath,
|
||||
isHidden: true
|
||||
})
|
||||
|
||||
function install () {
|
||||
return appLauncher
|
||||
.isEnabled()
|
||||
.then(enabled => {
|
||||
if (!enabled) return appLauncher.enable()
|
||||
})
|
||||
}
|
||||
|
||||
function uninstall () {
|
||||
return appLauncher
|
||||
.isEnabled()
|
||||
.then(enabled => {
|
||||
if (enabled) return appLauncher.disable()
|
||||
})
|
||||
}
|
||||
@@ -11,7 +11,8 @@ const windows = require('./windows')
|
||||
|
||||
const AUTO_UPDATE_URL = config.AUTO_UPDATE_URL +
|
||||
'?version=' + config.APP_VERSION +
|
||||
'&platform=' + process.platform
|
||||
'&platform=' + process.platform +
|
||||
'&sysarch=' + config.OS_SYSARCH
|
||||
|
||||
function init () {
|
||||
if (process.platform === 'linux') {
|
||||
|
||||
@@ -26,7 +26,7 @@ const tray = require('../tray')
|
||||
const HEADER_HEIGHT = 38
|
||||
const TORRENT_HEIGHT = 100
|
||||
|
||||
function init () {
|
||||
function init (options) {
|
||||
if (main.win) {
|
||||
return main.win.show()
|
||||
}
|
||||
@@ -40,7 +40,8 @@ function init () {
|
||||
titleBarStyle: 'hidden-inset', // Hide title bar (Mac)
|
||||
useContentSize: true, // Specify web page size without OS chrome
|
||||
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)
|
||||
|
||||
@@ -6,7 +6,7 @@ class Header extends React.Component {
|
||||
render () {
|
||||
const loc = this.props.state.location
|
||||
return (
|
||||
<div className='header'>
|
||||
<div className='header' onMouseMove={dispatcher('mediaMouseMoved')}>
|
||||
{this.getTitle()}
|
||||
<div className='nav left float-left'>
|
||||
<i
|
||||
|
||||
@@ -9,12 +9,12 @@ module.exports = class ModalOKCancel extends React.Component {
|
||||
return (
|
||||
<div className='float-right'>
|
||||
<FlatButton
|
||||
className='control'
|
||||
className='control cancel'
|
||||
style={cancelStyle}
|
||||
label={cancelText}
|
||||
onClick={onCancel} />
|
||||
<RaisedButton
|
||||
className='control'
|
||||
className='control ok'
|
||||
primary
|
||||
label={okText}
|
||||
onClick={onOK} />
|
||||
|
||||
@@ -11,6 +11,7 @@ module.exports = class OpenTorrentAddressModal extends React.Component {
|
||||
<p><label>Enter torrent address or magnet link</label></p>
|
||||
<div>
|
||||
<TextField
|
||||
id='torrent-address-field'
|
||||
className='control'
|
||||
ref={(c) => { this.torrentURL = c }}
|
||||
fullWidth
|
||||
@@ -31,7 +32,7 @@ module.exports = class OpenTorrentAddressModal extends React.Component {
|
||||
}
|
||||
|
||||
function handleKeyDown (e) {
|
||||
if (e.which === 13) this.handleOK() /* hit Enter to submit */
|
||||
if (e.which === 13) handleOK.call(this) /* hit Enter to submit */
|
||||
}
|
||||
|
||||
function handleOK () {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const React = require('react')
|
||||
|
||||
const FlatButton = require('material-ui/FlatButton').default
|
||||
const RaisedButton = require('material-ui/RaisedButton').default
|
||||
|
||||
class ShowMore extends React.Component {
|
||||
static get propTypes () {
|
||||
@@ -39,9 +39,10 @@ class ShowMore extends React.Component {
|
||||
? this.props.hideLabel
|
||||
: this.props.showLabel
|
||||
return (
|
||||
<div style={this.props.style}>
|
||||
<div className='show-more' style={this.props.style}>
|
||||
{this.state.expanded ? this.props.children : null}
|
||||
<FlatButton
|
||||
<RaisedButton
|
||||
className='control'
|
||||
onClick={this.handleClick}
|
||||
label={label} />
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
const React = require('react')
|
||||
const electron = require('electron')
|
||||
const path = require('path')
|
||||
|
||||
const ModalOKCancel = require('./modal-ok-cancel')
|
||||
const {dispatcher} = require('../lib/dispatcher')
|
||||
@@ -12,15 +11,11 @@ module.exports = class UnsupportedMediaModal extends React.Component {
|
||||
const message = (err && err.getMessage)
|
||||
? err.getMessage()
|
||||
: err
|
||||
const playerPath = state.saved.prefs.externalPlayerPath
|
||||
const playerName = playerPath
|
||||
? path.basename(playerPath).split('.')[0]
|
||||
: 'VLC'
|
||||
const onAction = state.modal.externalPlayerInstalled
|
||||
? dispatcher('openExternalPlayer')
|
||||
: () => this.onInstall()
|
||||
const actionText = state.modal.externalPlayerInstalled
|
||||
? 'PLAY IN ' + playerName.toUpperCase()
|
||||
? 'PLAY IN ' + state.getExternalPlayerName().toUpperCase()
|
||||
: 'INSTALL VLC'
|
||||
const errorMessage = state.modal.externalPlayerNotFound
|
||||
? 'Couldn\'t run external player. Please make sure it\'s installed.'
|
||||
|
||||
@@ -4,7 +4,8 @@ const path = require('path')
|
||||
const Cast = require('../lib/cast')
|
||||
const {dispatch} = require('../lib/dispatcher')
|
||||
const telemetry = require('../lib/telemetry')
|
||||
const errors = require('../lib/errors')
|
||||
const {UnplayableFileError, UnplayableTorrentError,
|
||||
PlaybackTimedOutError} = require('../lib/errors')
|
||||
const sound = require('../lib/sound')
|
||||
const TorrentPlayer = require('../lib/torrent-player')
|
||||
const TorrentSummary = require('../lib/torrent-summary')
|
||||
@@ -42,7 +43,7 @@ module.exports = class PlaybackController {
|
||||
|
||||
if (index === undefined || initialized) index = torrentSummary.mostRecentFileIndex
|
||||
if (index === undefined) index = torrentSummary.files.findIndex(TorrentPlayer.isPlayable)
|
||||
if (index === undefined) return cb(new errors.UnplayableError())
|
||||
if (index === undefined) return cb(new UnplayableTorrentError())
|
||||
|
||||
initialized = true
|
||||
|
||||
@@ -87,7 +88,7 @@ module.exports = class PlaybackController {
|
||||
// Play next file in list (if any)
|
||||
nextTrack () {
|
||||
const state = this.state
|
||||
if (Playlist.hasNext(state)) {
|
||||
if (Playlist.hasNext(state) && state.playing.location !== 'external') {
|
||||
this.updatePlayer(
|
||||
state.playing.infoHash, Playlist.getNextIndex(state), false, (err) => {
|
||||
if (err) dispatch('error', err)
|
||||
@@ -99,7 +100,7 @@ module.exports = class PlaybackController {
|
||||
// Play previous track in list (if any)
|
||||
previousTrack () {
|
||||
const state = this.state
|
||||
if (Playlist.hasPrevious(state)) {
|
||||
if (Playlist.hasPrevious(state) && state.playing.location !== 'external') {
|
||||
this.updatePlayer(
|
||||
state.playing.infoHash, Playlist.getPreviousIndex(state), false, (err) => {
|
||||
if (err) dispatch('error', err)
|
||||
@@ -232,7 +233,7 @@ module.exports = class PlaybackController {
|
||||
// TODO: remove torrentSummary.playStatus
|
||||
torrentSummary.playStatus = 'timeout' /* no seeders available? */
|
||||
sound.play('ERROR')
|
||||
cb(new Error('Playback timed out. Try again.'))
|
||||
cb(new PlaybackTimedOutError())
|
||||
this.update()
|
||||
}, 10000) /* give it a few seconds */
|
||||
|
||||
@@ -277,7 +278,7 @@ module.exports = class PlaybackController {
|
||||
|
||||
if (!TorrentPlayer.isPlayable(fileSummary)) {
|
||||
torrentSummary.mostRecentFileIndex = undefined
|
||||
return cb(new Error('Can\'t play that file'))
|
||||
return cb(new UnplayableFileError())
|
||||
}
|
||||
|
||||
torrentSummary.mostRecentFileIndex = index
|
||||
|
||||
@@ -17,7 +17,9 @@ module.exports = class PrefsController {
|
||||
setup: function (cb) {
|
||||
// initialize preferences
|
||||
state.window.title = 'Preferences'
|
||||
state.unsaved = Object.assign(state.unsaved || {}, {prefs: state.saved.prefs || {}})
|
||||
state.unsaved = Object.assign(state.unsaved || {}, {
|
||||
prefs: Object.assign({}, state.saved.prefs)
|
||||
})
|
||||
ipcRenderer.send('setAllowNav', false)
|
||||
cb()
|
||||
},
|
||||
@@ -50,6 +52,9 @@ module.exports = class PrefsController {
|
||||
if (state.unsaved.prefs.isFileHandler !== state.saved.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.save(state)
|
||||
dispatch('checkDownloadPath')
|
||||
|
||||
@@ -33,11 +33,10 @@ module.exports = class SubtitlesController {
|
||||
}
|
||||
|
||||
addSubtitles (files, autoSelect) {
|
||||
const state = this.state
|
||||
// Subtitles are only supported when playing video files
|
||||
if (state.playing.type !== 'video') return
|
||||
if (this.state.playing.type !== 'video') return
|
||||
if (files.length === 0) return
|
||||
const subtitles = state.playing.subtitles
|
||||
const subtitles = this.state.playing.subtitles
|
||||
|
||||
// Read the files concurrently, then add all resulting subtitle tracks
|
||||
const tasks = files.map((file) => (cb) => loadSubtitle(file, cb))
|
||||
@@ -47,17 +46,17 @@ module.exports = class SubtitlesController {
|
||||
for (let i = 0; i < tracks.length; i++) {
|
||||
// No dupes allowed
|
||||
const track = tracks[i]
|
||||
let trackIndex = state.playing.subtitles.tracks
|
||||
.findIndex((t) => track.filePath === t.filePath)
|
||||
let trackIndex = subtitles.tracks.findIndex((t) =>
|
||||
track.filePath === t.filePath)
|
||||
|
||||
// Add the track
|
||||
if (trackIndex === -1) {
|
||||
trackIndex = state.playing.subtitles.tracks.push(track) - 1
|
||||
trackIndex = subtitles.tracks.push(track) - 1
|
||||
}
|
||||
|
||||
// If we're auto-selecting a track, try to find one in the user's language
|
||||
if (autoSelect && (i === 0 || isSystemLanguage(track.language))) {
|
||||
state.playing.subtitles.selectedIndex = trackIndex
|
||||
subtitles.selectedIndex = trackIndex
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -127,6 +127,7 @@ module.exports = class TorrentController {
|
||||
|
||||
torrentFileModtimes (torrentKey, fileModtimes) {
|
||||
const torrentSummary = this.getTorrentSummary(torrentKey)
|
||||
if (!torrentSummary) throw new Error('Not saving modtimes for deleted torrent ' + torrentKey)
|
||||
torrentSummary.fileModtimes = fileModtimes
|
||||
dispatch('saveStateThrottled')
|
||||
}
|
||||
@@ -176,7 +177,7 @@ function showDoneNotification (torrent) {
|
||||
silent: true
|
||||
})
|
||||
|
||||
notif.onClick = function () {
|
||||
notif.onclick = function () {
|
||||
ipcRenderer.send('show')
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ const path = require('path')
|
||||
const electron = require('electron')
|
||||
|
||||
const {dispatch} = require('../lib/dispatcher')
|
||||
const {TorrentKeyNotFoundError} = require('../lib/errors')
|
||||
const State = require('../lib/state')
|
||||
const sound = require('../lib/sound')
|
||||
const TorrentSummary = require('../lib/torrent-summary')
|
||||
@@ -75,7 +76,7 @@ module.exports = class TorrentListController {
|
||||
// Starts downloading and/or seeding a given torrentSummary.
|
||||
startTorrentingSummary (torrentKey) {
|
||||
const s = TorrentSummary.getByKey(this.state, torrentKey)
|
||||
if (!s) throw new Error('Missing key: ' + torrentKey)
|
||||
if (!s) throw new TorrentKeyNotFoundError(torrentKey)
|
||||
|
||||
// New torrent: give it a path
|
||||
if (!s.path) {
|
||||
@@ -127,7 +128,9 @@ module.exports = class TorrentListController {
|
||||
torrentSummary.selections[index] = !torrentSummary.selections[index]
|
||||
|
||||
// Let the WebTorrent process know to start or stop fetching that file
|
||||
ipcRenderer.send('wt-select-files', infoHash, torrentSummary.selections)
|
||||
if (torrentSummary.status !== 'paused') {
|
||||
ipcRenderer.send('wt-select-files', infoHash, torrentSummary.selections)
|
||||
}
|
||||
}
|
||||
|
||||
confirmDeleteTorrent (infoHash, deleteData) {
|
||||
@@ -212,11 +215,41 @@ module.exports = class TorrentListController {
|
||||
|
||||
menu.append(new electron.remote.MenuItem({
|
||||
label: 'Save Torrent File As...',
|
||||
click: () => saveTorrentFileAs(torrentSummary)
|
||||
click: () => dispatch('saveTorrentFileAs', torrentSummary.torrentKey)
|
||||
}))
|
||||
|
||||
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
|
||||
@@ -277,27 +310,3 @@ function moveItemToTrash (torrentSummary) {
|
||||
function showItemInFolder (torrentSummary) {
|
||||
ipcRenderer.send('showItemInFolder', TorrentSummary.getFileOrFolder(torrentSummary))
|
||||
}
|
||||
|
||||
function saveTorrentFileAs (torrentSummary) {
|
||||
const downloadPath = this.state.saved.prefs.downloadPath
|
||||
const newFileName = path.parse(torrentSummary.name).name + '.torrent'
|
||||
const opts = {
|
||||
title: 'Save Torrent File',
|
||||
defaultPath: path.join(downloadPath, newFileName),
|
||||
filters: [
|
||||
{ name: 'Torrent Files', extensions: ['torrent'] },
|
||||
{ name: 'All Files', extensions: ['*'] }
|
||||
]
|
||||
}
|
||||
const win = electron.remote.getCurrentWindow()
|
||||
electron.remote.dialog.showSaveDialog(win, opts, function (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)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
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"')
|
||||
}
|
||||
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.width = video.videoWidth
|
||||
canvas.height = video.videoHeight
|
||||
|
||||
canvas.getContext('2d').drawImage(video, 0, 0)
|
||||
|
||||
const dataUri = canvas.toDataURL('image/' + format)
|
||||
const data = dataUri.split(',')[1]
|
||||
|
||||
return new Buffer(data, 'base64')
|
||||
}
|
||||
@@ -14,6 +14,7 @@ module.exports = {
|
||||
}
|
||||
|
||||
const config = require('../../config')
|
||||
const {CastingError} = require('./errors')
|
||||
|
||||
// Lazy load these for a ~300ms improvement in startup time
|
||||
let airplayer, chromecasts, dlnacasts
|
||||
@@ -32,6 +33,15 @@ function init (appState, callback) {
|
||||
state = appState
|
||||
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
|
||||
airplayer = require('airplayer')()
|
||||
chromecasts = require('chromecasts')()
|
||||
@@ -57,6 +67,32 @@ 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
|
||||
function chromecastPlayer () {
|
||||
const ret = {
|
||||
@@ -126,13 +162,7 @@ function chromecastPlayer () {
|
||||
}
|
||||
|
||||
function status () {
|
||||
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()
|
||||
})
|
||||
ret.device.status(handleStatus)
|
||||
}
|
||||
|
||||
function seek (time, callback) {
|
||||
@@ -306,13 +336,7 @@ function dlnaPlayer (player) {
|
||||
}
|
||||
|
||||
function status () {
|
||||
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()
|
||||
})
|
||||
ret.device.status(handleStatus)
|
||||
}
|
||||
|
||||
function seek (time, callback) {
|
||||
@@ -328,6 +352,18 @@ 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
|
||||
function startStatusInterval () {
|
||||
statusInterval = setInterval(function () {
|
||||
@@ -350,14 +386,16 @@ function toggleMenu (location) {
|
||||
|
||||
// Never cast to two devices at the same time
|
||||
if (state.playing.location !== 'local') {
|
||||
throw new Error('You can\'t connect to ' + location +
|
||||
' when already connected to another device')
|
||||
}
|
||||
throw new CastingError(
|
||||
`You can't connect to ${location} when already connected to another device`
|
||||
) }
|
||||
|
||||
// Find all cast devices of the given type
|
||||
const player = getPlayer(location)
|
||||
const devices = player ? player.getDevices() : []
|
||||
if (devices.length === 0) throw new Error('No ' + location + ' devices available')
|
||||
if (devices.length === 0) {
|
||||
throw new CastingError(`No ${location} devices available`)
|
||||
}
|
||||
|
||||
// Show a menu
|
||||
state.devices.castMenu = {location, devices}
|
||||
|
||||
@@ -1,15 +1,49 @@
|
||||
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 = {
|
||||
CastingError,
|
||||
PlaybackError,
|
||||
SoundError,
|
||||
TorrentError,
|
||||
UnplayableTorrentError,
|
||||
UnplayableFileError
|
||||
UnplayableFileError,
|
||||
PlaybackTimedOutError,
|
||||
InvalidSoundNameError,
|
||||
TorrentKeyNotFoundError,
|
||||
InvalidTorrentError
|
||||
}
|
||||
|
||||
function UnplayableTorrentError () {
|
||||
this.message = 'Can\'t play any files in torrent'
|
||||
}
|
||||
|
||||
function UnplayableFileError () {
|
||||
this.message = 'Can\'t play that file'
|
||||
}
|
||||
|
||||
UnplayableTorrentError.prototype = Error
|
||||
UnplayableFileError.prototype = Error
|
||||
|
||||
@@ -4,6 +4,7 @@ module.exports = {
|
||||
}
|
||||
|
||||
const config = require('../../config')
|
||||
const {InvalidSoundNameError} = require('./errors')
|
||||
const path = require('path')
|
||||
|
||||
const VOLUME = 0.15
|
||||
@@ -62,7 +63,7 @@ function play (name) {
|
||||
if (!audio) {
|
||||
const sound = sounds[name]
|
||||
if (!sound) {
|
||||
throw new Error('Invalid sound name')
|
||||
throw new InvalidSoundNameError(name)
|
||||
}
|
||||
audio = cache[name] = new window.Audio()
|
||||
audio.volume = sound.volume
|
||||
|
||||
@@ -68,7 +68,8 @@ function getDefaultState () {
|
||||
* Getters, for convenience
|
||||
*/
|
||||
getPlayingTorrentSummary,
|
||||
getPlayingFileSummary
|
||||
getPlayingFileSummary,
|
||||
getExternalPlayerName
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,7 +108,8 @@ function setupSavedState (cb) {
|
||||
downloadPath: config.DEFAULT_DOWNLOAD_PATH,
|
||||
isFileHandler: false,
|
||||
openExternalPlayer: false,
|
||||
externalPlayerPath: null
|
||||
externalPlayerPath: null,
|
||||
startup: false
|
||||
},
|
||||
torrents: config.DEFAULT_TORRENTS.map(createTorrentObject),
|
||||
version: config.APP_VERSION /* make sure we can upgrade gracefully later */
|
||||
@@ -151,7 +153,8 @@ function setupSavedState (cb) {
|
||||
torrentFileName: parsedTorrent.infoHash + '.torrent',
|
||||
magnetURI: parseTorrent.toMagnetURI(parsedTorrent),
|
||||
files: parsedTorrent.files,
|
||||
selections: parsedTorrent.files.map((x) => true)
|
||||
selections: parsedTorrent.files.map((x) => true),
|
||||
testID: t.testID
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -167,6 +170,12 @@ function getPlayingFileSummary () {
|
||||
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) {
|
||||
const state = getDefaultState()
|
||||
|
||||
|
||||
@@ -29,7 +29,8 @@ function init (state) {
|
||||
telemetry.localTime = now.toTimeString()
|
||||
telemetry.screens = getScreenInfo()
|
||||
telemetry.system = getSystemInfo()
|
||||
telemetry.approxNumTorrents = getApproxNumTorrents(state)
|
||||
telemetry.torrentStats = getTorrentStats(state)
|
||||
telemetry.approxNumTorrents = telemetry.torrentStats.approxCount
|
||||
|
||||
if (config.IS_PRODUCTION) {
|
||||
postToServer()
|
||||
@@ -104,18 +105,64 @@ function getSystemInfo () {
|
||||
osPlatform: process.platform,
|
||||
osRelease: os.type() + ' ' + os.release(),
|
||||
architecture: os.arch(),
|
||||
totalMemoryMB: os.totalmem() / (1 << 20),
|
||||
systemArchitecture: config.OS_SYSARCH,
|
||||
totalMemoryMB: roundPow2(os.totalmem() / (1 << 20)),
|
||||
numCores: os.cpus().length
|
||||
}
|
||||
}
|
||||
|
||||
// Get the number of torrents, rounded to the nearest power of two
|
||||
function getApproxNumTorrents (state) {
|
||||
const exactNum = state.saved.torrents.length
|
||||
if (exactNum === 0) return 0
|
||||
// Get stats like the # of torrents currently active, # in list, total size
|
||||
function getTorrentStats (state) {
|
||||
const count = state.saved.torrents.length
|
||||
let sizeMB = 0
|
||||
let byStatus = {
|
||||
new: { count: 0, sizeMB: 0 },
|
||||
downloading: { count: 0, sizeMB: 0 },
|
||||
seeding: { count: 0, sizeMB: 0 },
|
||||
paused: { count: 0, sizeMB: 0 }
|
||||
}
|
||||
|
||||
// First, count torrents & total file size
|
||||
for (let i = 0; i < count; i++) {
|
||||
const t = state.saved.torrents[i]
|
||||
const stat = byStatus[t.status]
|
||||
if (!t || !t.files || !stat) continue
|
||||
stat.count++
|
||||
for (let j = 0; j < t.files.length; j++) {
|
||||
const f = t.files[j]
|
||||
if (!f || !f.length) continue
|
||||
const fileSizeMB = f.length / (1 << 20)
|
||||
sizeMB += fileSizeMB
|
||||
stat.sizeMB += fileSizeMB
|
||||
}
|
||||
}
|
||||
|
||||
// Then, round all the counts and sums to the nearest power of 2
|
||||
const ret = roundTorrentStats({count, sizeMB})
|
||||
ret.byStatus = {
|
||||
new: roundTorrentStats(byStatus.new),
|
||||
downloading: roundTorrentStats(byStatus.downloading),
|
||||
seeding: roundTorrentStats(byStatus.seeding),
|
||||
paused: roundTorrentStats(byStatus.paused)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
function roundTorrentStats (stats) {
|
||||
return {
|
||||
approxCount: roundPow2(stats.count),
|
||||
approxSizeMB: roundPow2(stats.sizeMB)
|
||||
}
|
||||
}
|
||||
|
||||
// Rounds to the nearest power of 2, for privacy and easy bucketing.
|
||||
// Rounds 35 to 32, 70 to 64, 5 to 4, 1 to 1, 0 to 0.
|
||||
// Supports nonnegative numbers only.
|
||||
function roundPow2 (n) {
|
||||
if (n <= 0) return 0
|
||||
// Otherwise, return 1, 2, 4, 8, etc by rounding in log space
|
||||
const log2 = Math.log(exactNum) / Math.log(2)
|
||||
return 1 << Math.round(log2)
|
||||
const log2 = Math.log(n) / Math.log(2)
|
||||
return Math.pow(2, Math.round(log2))
|
||||
}
|
||||
|
||||
// An uncaught error happened in the main process or in one of the windows
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
module.exports = torrentPoster
|
||||
|
||||
const captureVideoFrame = require('./capture-video-frame')
|
||||
const captureFrame = require('capture-frame')
|
||||
const path = require('path')
|
||||
|
||||
function torrentPoster (torrent, cb) {
|
||||
@@ -61,7 +61,7 @@ function torrentPosterFromVideo (file, torrent, cb) {
|
||||
function onSeeked () {
|
||||
video.removeEventListener('seeked', onSeeked)
|
||||
|
||||
const buf = captureVideoFrame(video)
|
||||
const buf = captureFrame(video)
|
||||
|
||||
// unload video element
|
||||
video.pause()
|
||||
|
||||
@@ -58,6 +58,7 @@ State.load(onState)
|
||||
function onState (err, _state) {
|
||||
if (err) return onError(err)
|
||||
state = window.state = _state // Make available for easier debugging
|
||||
window.dispatch = dispatch
|
||||
|
||||
telemetry.init(state)
|
||||
|
||||
@@ -122,7 +123,9 @@ function onState (err, _state) {
|
||||
document.addEventListener('webkitvisibilitychange', onVisibilityChange)
|
||||
|
||||
// Done! Ideally we want to get here < 500ms after the user clicks the app
|
||||
sound.play('STARTUP')
|
||||
if (electron.remote.getCurrentWindow().isVisible()) {
|
||||
sound.play('STARTUP')
|
||||
}
|
||||
console.timeEnd('init')
|
||||
}
|
||||
|
||||
@@ -191,6 +194,8 @@ const dispatchHandlers = {
|
||||
controllers.torrentList.openTorrentContextMenu(infoHash),
|
||||
'startTorrentingSummary': (torrentKey) =>
|
||||
controllers.torrentList.startTorrentingSummary(torrentKey),
|
||||
'saveTorrentFileAs': (torrentKey) =>
|
||||
controllers.torrentList.saveTorrentFileAs(torrentKey),
|
||||
|
||||
// Playback
|
||||
'playFile': (infoHash, index) => controllers.playback.playFile(infoHash, index),
|
||||
|
||||
@@ -99,14 +99,14 @@ class CreateTorrentPage extends React.Component {
|
||||
</ShowMore>
|
||||
<div className='float-right'>
|
||||
<FlatButton
|
||||
className='control'
|
||||
className='control cancel'
|
||||
label='Cancel'
|
||||
style={{
|
||||
marginRight: 10
|
||||
}}
|
||||
onClick={dispatcher('cancel')} />
|
||||
<RaisedButton
|
||||
className='control'
|
||||
className='control create-torrent'
|
||||
label='Create Torrent'
|
||||
primary
|
||||
onClick={this.handleSubmit} />
|
||||
|
||||
@@ -2,11 +2,11 @@ const React = require('react')
|
||||
const Bitfield = require('bitfield')
|
||||
const prettyBytes = require('prettier-bytes')
|
||||
const zeroFill = require('zero-fill')
|
||||
const path = require('path')
|
||||
|
||||
const TorrentSummary = require('../lib/torrent-summary')
|
||||
const Playlist = require('../lib/playlist')
|
||||
const {dispatch, dispatcher} = require('../lib/dispatcher')
|
||||
const config = require('../../config')
|
||||
|
||||
// Shows a streaming video player. Standard features + Chromecast + Airplay
|
||||
module.exports = class Player extends React.Component {
|
||||
@@ -289,11 +289,8 @@ function renderCastScreen (state) {
|
||||
castType = 'DLNA'
|
||||
isCast = true
|
||||
} else if (state.playing.location === 'external') {
|
||||
// TODO: get the player name in a more reliable way
|
||||
const playerPath = state.saved.prefs.externalPlayerPath
|
||||
const playerName = playerPath ? path.basename(playerPath).split('.')[0] : 'VLC'
|
||||
castIcon = 'tv'
|
||||
castType = playerName
|
||||
castType = state.getExternalPlayerName()
|
||||
isCast = false
|
||||
} else if (state.playing.location === 'error') {
|
||||
castIcon = 'error_outline'
|
||||
@@ -589,6 +586,8 @@ function renderPlayerControls (state) {
|
||||
// Renders the loading bar. Shows which parts of the torrent are loaded, which
|
||||
// can be 'spongey' / non-contiguous
|
||||
function renderLoadingBar (state) {
|
||||
if (config.IS_TEST) return // Don't integration test the loading bar. Screenshots won't match.
|
||||
|
||||
const torrentSummary = state.getPlayingTorrentSummary()
|
||||
if (!torrentSummary.progress) {
|
||||
return []
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
const colors = require('material-ui/styles/colors')
|
||||
const path = require('path')
|
||||
const React = require('react')
|
||||
|
||||
const colors = require('material-ui/styles/colors')
|
||||
const Checkbox = require('material-ui/Checkbox').default
|
||||
const RaisedButton = require('material-ui/RaisedButton').default
|
||||
const Heading = require('../components/heading')
|
||||
const PathSelector = require('../components/path-selector')
|
||||
const RaisedButton = require('material-ui/RaisedButton').default
|
||||
|
||||
const {dispatch} = require('../lib/dispatcher')
|
||||
const config = require('../../config')
|
||||
|
||||
class PreferencesPage extends React.Component {
|
||||
constructor (props) {
|
||||
@@ -21,6 +22,9 @@ class PreferencesPage extends React.Component {
|
||||
|
||||
this.handleExternalPlayerPathChange =
|
||||
this.handleExternalPlayerPathChange.bind(this)
|
||||
|
||||
this.handleStartupChange =
|
||||
this.handleStartupChange.bind(this)
|
||||
}
|
||||
|
||||
downloadPathSelector () {
|
||||
@@ -59,9 +63,8 @@ class PreferencesPage extends React.Component {
|
||||
}
|
||||
|
||||
externalPlayerPathSelector () {
|
||||
const playerName = path.basename(
|
||||
this.props.state.unsaved.prefs.externalPlayerPath || 'VLC'
|
||||
)
|
||||
const playerPath = this.props.state.unsaved.prefs.externalPlayerPath
|
||||
const playerName = this.props.state.getExternalPlayerName()
|
||||
|
||||
const description = this.props.state.unsaved.prefs.openExternalPlayer
|
||||
? `Torrent media files will always play in ${playerName}.`
|
||||
@@ -79,16 +82,12 @@ class PreferencesPage extends React.Component {
|
||||
displayValue={playerName}
|
||||
onChange={this.handleExternalPlayerPathChange}
|
||||
title='External player'
|
||||
value={this.props.state.unsaved.prefs.externalPlayerPath} />
|
||||
value={playerPath ? path.dirname(playerPath) : null} />
|
||||
</Preference>
|
||||
)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -112,6 +111,29 @@ class PreferencesPage extends React.Component {
|
||||
)
|
||||
}
|
||||
|
||||
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 () {
|
||||
dispatch('updatePreferences', 'isFileHandler', true)
|
||||
}
|
||||
@@ -134,6 +156,7 @@ class PreferencesPage extends React.Component {
|
||||
<PreferencesSection title='Default torrent app'>
|
||||
{this.setDefaultAppButton()}
|
||||
</PreferencesSection>
|
||||
{this.setStartupSection()}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ const prettyBytes = require('prettier-bytes')
|
||||
const TorrentSummary = require('../lib/torrent-summary')
|
||||
const TorrentPlayer = require('../lib/torrent-player')
|
||||
const {dispatcher} = require('../lib/dispatcher')
|
||||
const {InvalidTorrentError} = require('../lib/errors')
|
||||
|
||||
module.exports = class TorrentList extends React.Component {
|
||||
render () {
|
||||
@@ -58,9 +59,10 @@ module.exports = class TorrentList extends React.Component {
|
||||
if (torrentSummary.playStatus) classes.push(torrentSummary.playStatus)
|
||||
if (isSelected) classes.push('selected')
|
||||
if (!infoHash) classes.push('disabled')
|
||||
if (!torrentSummary.torrentKey) throw new Error('Missing torrentKey')
|
||||
if (!torrentSummary.torrentKey) throw new InvalidTorrentError('Missing torrentKey')
|
||||
return (
|
||||
<div
|
||||
id={torrentSummary.testID && ('torrent-' + torrentSummary.testID)}
|
||||
key={torrentSummary.torrentKey}
|
||||
style={style}
|
||||
className={classes.join(' ')}
|
||||
@@ -345,7 +347,7 @@ module.exports = class TorrentList extends React.Component {
|
||||
</td>
|
||||
<td className='col-select'
|
||||
onClick={dispatcher('toggleTorrentFile', infoHash, index)}>
|
||||
<i className='icon'>{isSelected ? 'close' : 'add'}</i>
|
||||
<i className='icon deselect-file'>{isSelected ? 'close' : 'add'}</i>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
|
||||
@@ -15,6 +15,7 @@ const zeroFill = require('zero-fill')
|
||||
|
||||
const crashReporter = require('../crash-reporter')
|
||||
const config = require('../config')
|
||||
const {TorrentKeyNotFoundError} = require('./lib/errors')
|
||||
const torrentPoster = require('./lib/torrent-poster')
|
||||
|
||||
// Report when the process crashes
|
||||
@@ -53,11 +54,18 @@ const VERSION_STR = VERSION.match(/([0-9]+)/g)
|
||||
*/
|
||||
const 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
|
||||
// client, as explained here: https://webtorrent.io/faq
|
||||
const client = window.client = new WebTorrent({
|
||||
peerId: Buffer.from(VERSION_PREFIX + crypto.randomBytes(6).toString('hex'))
|
||||
})
|
||||
let client = window.client = new WebTorrent({ peerId: PEER_ID })
|
||||
|
||||
// WebTorrent-to-HTTP streaming sever
|
||||
let server = null
|
||||
@@ -68,8 +76,7 @@ let prevProgress = null
|
||||
init()
|
||||
|
||||
function init () {
|
||||
client.on('warning', (err) => ipc.send('wt-warning', null, err.message))
|
||||
client.on('error', (err) => ipc.send('wt-error', null, err.message))
|
||||
listenToClientEvents()
|
||||
|
||||
ipc.on('wt-start-torrenting', (e, torrentKey, torrentID, path, fileModtimes, selections) =>
|
||||
startTorrenting(torrentKey, torrentID, path, fileModtimes, selections))
|
||||
@@ -100,6 +107,11 @@ function init () {
|
||||
console.timeEnd('init')
|
||||
}
|
||||
|
||||
function listenToClientEvents () {
|
||||
client.on('warning', (err) => ipc.send('wt-warning', null, err.message))
|
||||
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) {
|
||||
@@ -156,7 +168,7 @@ function addTorrentEvents (torrent) {
|
||||
function torrentReady () {
|
||||
const info = getTorrentInfo(torrent)
|
||||
ipc.send('wt-ready', torrent.key, info)
|
||||
ipc.send('wt-ready-' + torrent.infoHash, torrent.key, info) // TODO: hack
|
||||
ipc.send('wt-ready-' + torrent.infoHash, torrent.key, info)
|
||||
|
||||
updateTorrentProgress()
|
||||
}
|
||||
@@ -350,6 +362,9 @@ function selectFiles (torrentOrInfoHash, selections) {
|
||||
} else {
|
||||
torrent = torrentOrInfoHash
|
||||
}
|
||||
if (!torrent) {
|
||||
throw new Error('selectFiles: missing torrent ' + torrentOrInfoHash)
|
||||
}
|
||||
|
||||
// Selections not specified?
|
||||
// Load all files. We still need to replace the default whole-torrent
|
||||
@@ -384,10 +399,24 @@ function selectFiles (torrentOrInfoHash, selections) {
|
||||
// Throws an Error if we're not currently torrenting anything w/ that key
|
||||
function getTorrent (torrentKey) {
|
||||
const ret = client.torrents.find((x) => x.key === torrentKey)
|
||||
if (!ret) throw new Error('missing torrent key ' + torrentKey)
|
||||
if (!ret) throw new TorrentKeyNotFoundError(torrentKey)
|
||||
return ret
|
||||
}
|
||||
|
||||
function onError (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()
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
<p>
|
||||
Version <script>document.write(require('../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><script>document.write(require('../config').APP_COPYRIGHT)</script></p>
|
||||
</body>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>WebTorrent Desktop</title>
|
||||
<title>Main Window</title>
|
||||
<link rel="stylesheet" href="main.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>WebTorrent Desktop</title>
|
||||
<title>WebTorrent Hidden Window</title>
|
||||
<style>
|
||||
body {
|
||||
background-color: #282828;
|
||||
|
||||
14
test/config.js
Normal file
@@ -0,0 +1,14 @@
|
||||
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
|
||||
}
|
||||
20
test/index.js
Normal file
@@ -0,0 +1,20 @@
|
||||
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')
|
||||
15
test/mocks.js
Normal file
@@ -0,0 +1,15 @@
|
||||
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)
|
||||
}
|
||||
BIN
test/resources/1.torrent
Normal file
BIN
test/resources/expected-single-file.torrent
Normal file
BIN
test/resources/m3.jpg
Normal file
|
After Width: | Height: | Size: 2.2 MiB |
BIN
test/resources/monitor-test.mp4
Normal file
BIN
test/screenshots/darwin/add-torrent-0-percent.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
test/screenshots/darwin/add-torrent-100-percent.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
test/screenshots/darwin/app-basic.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
test/screenshots/darwin/create-torrent-100-percent.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
test/screenshots/darwin/create-torrent-advanced.png
Normal file
|
After Width: | Height: | Size: 146 KiB |
BIN
test/screenshots/darwin/create-torrent-simple.png
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
test/screenshots/darwin/play-torrent-bbb.png
Normal file
|
After Width: | Height: | Size: 302 KiB |
BIN
test/screenshots/darwin/play-torrent-return.png
Normal file
|
After Width: | Height: | Size: 777 KiB |
BIN
test/screenshots/darwin/play-torrent-wired-2.png
Normal file
|
After Width: | Height: | Size: 478 KiB |
BIN
test/screenshots/darwin/play-torrent-wired-3.png
Normal file
|
After Width: | Height: | Size: 480 KiB |
BIN
test/screenshots/darwin/play-torrent-wired-4.png
Normal file
|
After Width: | Height: | Size: 480 KiB |
BIN
test/screenshots/darwin/play-torrent-wired-5.png
Normal file
|
After Width: | Height: | Size: 480 KiB |
BIN
test/screenshots/darwin/play-torrent-wired-fullscreen.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
test/screenshots/darwin/play-torrent-wired-list.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
test/screenshots/darwin/play-torrent-wired.png
Normal file
|
After Width: | Height: | Size: 481 KiB |
BIN
test/screenshots/darwin/prefs-basic.png
Normal file
|
After Width: | Height: | Size: 91 KiB |
BIN
test/screenshots/darwin/torrent-list-cosmos-delete-data.png
Normal file
|
After Width: | Height: | Size: 701 KiB |
BIN
test/screenshots/darwin/torrent-list-cosmos-deleted.png
Normal file
|
After Width: | Height: | Size: 932 KiB |
BIN
test/screenshots/darwin/torrent-list-cosmos-expand-deselect.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
test/screenshots/darwin/torrent-list-cosmos-expand-start.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
test/screenshots/darwin/torrent-list-cosmos-expand.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
test/screenshots/darwin/torrent-list-cosmos-hover.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
test/screenshots/darwin/torrent-list-delete-prompt.png
Normal file
|
After Width: | Height: | Size: 617 KiB |
BIN
test/screenshots/darwin/torrent-list-delete.png
Normal file
|
After Width: | Height: | Size: 617 KiB |
BIN
test/screenshots/darwin/torrent-list-deleted.png
Normal file
|
After Width: | Height: | Size: 777 KiB |
BIN
test/screenshots/darwin/torrent-list-download-path-missing.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
test/screenshots/darwin/torrent-list-hover-download.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
test/screenshots/darwin/torrent-list-hover.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
test/screenshots/darwin/torrent-list-start-download.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
test/screenshots/darwin/torrent-list-stop-download.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
170
test/setup.js
Normal file
@@ -0,0 +1,170 @@
|
||||
const path = require('path')
|
||||
const Application = require('spectron').Application
|
||||
const fs = require('fs-extra')
|
||||
const parseTorrent = require('parse-torrent')
|
||||
const PNG = require('pngjs').PNG
|
||||
const config = require('./config')
|
||||
|
||||
module.exports = {
|
||||
createApp,
|
||||
endTest,
|
||||
screenshotCreateOrCompare,
|
||||
compareDownloadFolder,
|
||||
compareFiles,
|
||||
compareTorrentFiles,
|
||||
waitForLoad,
|
||||
wait,
|
||||
resetTestDataDir,
|
||||
deleteTestDataDir
|
||||
}
|
||||
|
||||
// Runs WebTorrent Desktop.
|
||||
// Returns a promise that resolves to a Spectron Application once the app has loaded.
|
||||
// Takes a Tape test. Makes some basic assertions to verify that the app loaded correctly.
|
||||
function createApp (t) {
|
||||
return new Application({
|
||||
path: path.join(__dirname, '..', 'node_modules', '.bin',
|
||||
'electron' + (process.platform === 'win32' ? '.cmd' : '')),
|
||||
args: ['-r', path.join(__dirname, 'mocks.js'), path.join(__dirname, '..')],
|
||||
env: {NODE_ENV: 'test'}
|
||||
})
|
||||
}
|
||||
|
||||
// Starts the app, waits for it to load, returns a promise
|
||||
function waitForLoad (app, t, opts) {
|
||||
if (!opts) opts = {}
|
||||
return app.start().then(function () {
|
||||
return app.client.waitUntilWindowLoaded()
|
||||
}).then(function () {
|
||||
// Offline mode
|
||||
if (opts.offline) app.webContents.executeJavaScript('testOfflineMode()')
|
||||
}).then(function () {
|
||||
// Switch to the main window. Index 0 is apparently the hidden webtorrent window...
|
||||
return app.client.windowByIndex(1)
|
||||
}).then(function () {
|
||||
return app.client.waitUntilWindowLoaded()
|
||||
}).then(function () {
|
||||
return app.webContents.getTitle()
|
||||
}).then(function (title) {
|
||||
// Note the window title is WebTorrent (BETA), this is the HTML <title>
|
||||
t.equal(title, 'Main Window', 'html title')
|
||||
})
|
||||
}
|
||||
|
||||
// Returns a promise that resolves after 'ms' milliseconds. Default: 1 second
|
||||
function wait (ms) {
|
||||
if (ms === undefined) ms = 1000 // Default: wait long enough for the UI to update
|
||||
return new Promise(function (resolve, reject) {
|
||||
setTimeout(resolve, ms)
|
||||
})
|
||||
}
|
||||
|
||||
// Quit the app, end the test, either in success (!err) or failure (err)
|
||||
function endTest (app, t, err) {
|
||||
return app.stop().then(function () {
|
||||
t.end(err)
|
||||
})
|
||||
}
|
||||
|
||||
// Takes a screenshot of the app
|
||||
// If we already have a reference under test/screenshots, assert that they're the same
|
||||
// Otherwise, create the reference screenshot: test/screenshots/<platform>/<name>.png
|
||||
function screenshotCreateOrCompare (app, t, name) {
|
||||
const ssDir = path.join(__dirname, 'screenshots', process.platform)
|
||||
const ssPath = path.join(ssDir, name + '.png')
|
||||
fs.ensureFileSync(ssPath)
|
||||
const ssBuf = fs.readFileSync(ssPath)
|
||||
return app.browserWindow.capturePage().then(function (buffer) {
|
||||
if (ssBuf.length === 0) {
|
||||
console.log('Saving screenshot ' + ssPath)
|
||||
fs.writeFileSync(ssPath, buffer)
|
||||
} else {
|
||||
const match = compareIgnoringTransparency(buffer, ssBuf)
|
||||
t.ok(match, 'screenshot comparison ' + name)
|
||||
if (!match) {
|
||||
const ssFailedPath = path.join(ssDir, name + '-failed.png')
|
||||
console.log('Saving screenshot, failed comparison: ' + ssFailedPath)
|
||||
fs.writeFileSync(ssFailedPath, buffer)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Compares two PNGs, ignoring any transparent regions in bufExpected.
|
||||
// Returns true if they match.
|
||||
function compareIgnoringTransparency (bufActual, bufExpected) {
|
||||
// Common case: exact byte-for-byte match
|
||||
if (Buffer.compare(bufActual, bufExpected) === 0) return true
|
||||
|
||||
// Otherwise, compare pixel by pixel
|
||||
const pngA = PNG.sync.read(bufActual)
|
||||
const pngE = PNG.sync.read(bufExpected)
|
||||
if (pngA.width !== pngE.width || pngA.height !== pngE.height) return false
|
||||
const w = pngA.width
|
||||
const h = pngE.height
|
||||
const da = pngA.data
|
||||
const de = pngE.data
|
||||
for (let y = 0; y < h; y++) {
|
||||
for (let x = 0; x < w; x++) {
|
||||
const i = (y * w + x) * 4
|
||||
if (de[i + 3] === 0) continue // Skip transparent pixels
|
||||
if (da[i] !== de[i] || da[i + 1] !== de[i + 1] || da[i + 2] !== de[i + 2]) return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Resets the test directory, containing config.json, torrents, downloads, etc
|
||||
function resetTestDataDir () {
|
||||
fs.removeSync(config.TEST_DIR)
|
||||
// Create TEST_DIR as well as /Downloads and /Desktop
|
||||
fs.mkdirpSync(config.TEST_DIR_DOWNLOAD)
|
||||
fs.mkdirpSync(config.TEST_DIR_DESKTOP)
|
||||
}
|
||||
|
||||
function deleteTestDataDir () {
|
||||
fs.removeSync(config.TEST_DIR)
|
||||
}
|
||||
|
||||
// Checks a given folder under Downloads.
|
||||
// Makes sure that the filenames match exactly.
|
||||
// If `filenames` is null, asserts that the folder doesn't exist.
|
||||
function compareDownloadFolder (t, dirname, filenames) {
|
||||
const dirpath = path.join(config.TEST_DIR_DOWNLOAD, dirname)
|
||||
try {
|
||||
const actualFilenames = fs.readdirSync(dirpath)
|
||||
const expectedSorted = filenames.slice().sort()
|
||||
const actualSorted = actualFilenames.slice().sort()
|
||||
t.deepEqual(actualSorted, expectedSorted, 'download folder contents: ' + dirname)
|
||||
} catch (e) {
|
||||
if (e.code === 'ENOENT') {
|
||||
t.equal(filenames, null, 'download folder missing: ' + dirname)
|
||||
} else {
|
||||
console.error(e)
|
||||
t.fail('unexpected error getting download folder: ' + dirname)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Makes sure two files have identical contents
|
||||
function compareFiles (t, pathActual, pathExpected) {
|
||||
const bufActual = fs.readFileSync(pathActual)
|
||||
const bufExpected = fs.readFileSync(pathExpected)
|
||||
const match = Buffer.compare(bufActual, bufExpected) === 0
|
||||
t.ok(match, 'correct contents: ' + pathActual)
|
||||
}
|
||||
|
||||
// Makes sure two torrents have the same infohash and flags
|
||||
function compareTorrentFiles (t, pathActual, pathExpected) {
|
||||
const bufActual = fs.readFileSync(pathActual)
|
||||
const bufExpected = fs.readFileSync(pathExpected)
|
||||
const fieldsActual = extractImportantFields(parseTorrent(bufActual))
|
||||
const fieldsExpected = extractImportantFields(parseTorrent(bufExpected))
|
||||
t.deepEqual(fieldsActual, fieldsExpected, 'torrent contents: ' + pathActual)
|
||||
}
|
||||
|
||||
function extractImportantFields (parsedTorrent) {
|
||||
const { infoHash, name, announce, urlList, comment } = parsedTorrent
|
||||
const priv = parsedTorrent.private // private is a reserved word in JS
|
||||
return { infoHash, name, announce, urlList, comment, 'private': priv }
|
||||
}
|
||||
69
test/test-add-torrent.js
Normal file
@@ -0,0 +1,69 @@
|
||||
const test = require('tape')
|
||||
const fs = require('fs-extra')
|
||||
const path = require('path')
|
||||
const setup = require('./setup')
|
||||
const config = require('./config')
|
||||
|
||||
test('add-torrent', function (t) {
|
||||
setup.resetTestDataDir()
|
||||
|
||||
t.timeoutAfter(30e3)
|
||||
const app = setup.createApp()
|
||||
setup.waitForLoad(app, t)
|
||||
.then(() => app.client.waitUntilTextExists('.torrent-list', 'Big Buck Bunny'))
|
||||
// Add an existing torrent. The corresponding file is not present. Should be at 0%
|
||||
.then(() => app.electron.ipcRenderer.send('openTorrentFile'))
|
||||
// The call to dialog.openFiles() is mocked. See mocks.js
|
||||
.then(() => app.client.waitUntilTextExists('m3.jpg'))
|
||||
.then(() => setup.screenshotCreateOrCompare(app, t, 'add-torrent-0-percent'))
|
||||
// Delete the torrent.
|
||||
.then(() => app.client.moveToObject('.torrent'))
|
||||
.then(() => setup.wait())
|
||||
.then(() => app.client.click('.icon.delete'))
|
||||
.then(() => app.client.waitUntilTextExists('REMOVE'))
|
||||
.then(() => app.client.click('.control.ok'))
|
||||
// Add the same existing torrent, this time with the file present. Should be at 100%
|
||||
.then(() => fs.copySync(
|
||||
path.join(__dirname, 'resources', 'm3.jpg'),
|
||||
path.join(config.TEST_DIR_DOWNLOAD, 'm3.jpg')))
|
||||
.then(() => app.electron.ipcRenderer.send('openTorrentFile'))
|
||||
.then(() => app.client.waitUntilTextExists('m3.jpg'))
|
||||
.then(() => setup.wait())
|
||||
.then(() => setup.screenshotCreateOrCompare(app, t, 'add-torrent-100-percent'))
|
||||
.then(() => setup.endTest(app, t),
|
||||
(err) => setup.endTest(app, t, err || 'error'))
|
||||
})
|
||||
|
||||
test('create-torrent', function (t) {
|
||||
setup.resetTestDataDir()
|
||||
|
||||
// Set up the files to seed
|
||||
fs.copySync(path.join(__dirname, 'resources', 'm3.jpg'), config.SEED_FILES[0])
|
||||
|
||||
t.timeoutAfter(30e3)
|
||||
const app = setup.createApp()
|
||||
setup.waitForLoad(app, t)
|
||||
.then(() => app.client.waitUntilTextExists('.torrent-list', 'Big Buck Bunny'))
|
||||
// Click the + button, open a non-torrent file to seed
|
||||
.then(() => app.client.click('.icon.add'))
|
||||
.then(() => app.client.waitUntilTextExists('Create'))
|
||||
.then(() => setup.screenshotCreateOrCompare(app, t, 'create-torrent-simple'))
|
||||
// Click to show advanced settings
|
||||
.then(() => app.client.click('.show-more .control'))
|
||||
.then(() => app.client.waitUntilTextExists('Comment'))
|
||||
.then(() => setup.screenshotCreateOrCompare(app, t, 'create-torrent-advanced'))
|
||||
// Click OK to create the torrent
|
||||
.then(() => app.client.click('.control.create-torrent'))
|
||||
.then(() => app.client.waitUntilTextExists('tmp.jpg'))
|
||||
.then(() => setup.screenshotCreateOrCompare(app, t, 'create-torrent-100-percent'))
|
||||
// Click "Save Torrent File As..." on the new torrent
|
||||
.then(() => app.webContents.executeJavaScript(
|
||||
'dispatch("saveTorrentFileAs", 6)'))
|
||||
.then(() => setup.wait())
|
||||
// Mock saves to <temp folder>/Desktop/saved.torrent
|
||||
.then(() => setup.compareTorrentFiles(t,
|
||||
config.SAVED_TORRENT_FILE,
|
||||
path.join(__dirname, 'resources', 'expected-single-file.torrent')))
|
||||
.then(() => setup.endTest(app, t),
|
||||
(err) => setup.endTest(app, t, err || 'error'))
|
||||
})
|
||||
59
test/test-audio.js
Normal file
@@ -0,0 +1,59 @@
|
||||
const test = require('tape')
|
||||
const setup = require('./setup')
|
||||
|
||||
test('audio-streaming', function (t) {
|
||||
setup.resetTestDataDir()
|
||||
|
||||
t.timeoutAfter(60e3)
|
||||
const app = setup.createApp()
|
||||
setup.waitForLoad(app, t, {online: true})
|
||||
.then(() => app.client.waitUntilTextExists('.torrent-list', 'Big Buck Bunny'))
|
||||
// Play Wired CD. Wait for it to start streaming.
|
||||
.then(() => app.client.moveToObject('#torrent-wired'))
|
||||
.then(() => setup.wait())
|
||||
.then(() => app.client.click('#torrent-wired .icon.play'))
|
||||
.then(() => app.client.waitUntilTextExists('The Wired CD'))
|
||||
// Pause. Skip to two seconds in. Wait another two seconds for it to load.
|
||||
.then(() => app.webContents.executeJavaScript('dispatch("playPause")'))
|
||||
.then(() => app.webContents.executeJavaScript('dispatch("skipTo", 2)'))
|
||||
.then(() => setup.wait())
|
||||
.then(() => app.client.waitUntilTextExists('Artist'))
|
||||
.then(() => setup.screenshotCreateOrCompare(app, t, 'play-torrent-wired'))
|
||||
// Click next
|
||||
.then(() => app.client.click('.skip-next'))
|
||||
.then(() => app.client.waitUntilTextExists('The Wired CD'))
|
||||
.then(() => app.client.moveToObject('.letterbox'))
|
||||
.then(() => app.webContents.executeJavaScript('dispatch("playPause")'))
|
||||
.then(() => app.webContents.executeJavaScript('dispatch("skipTo", 2)'))
|
||||
.then(() => setup.wait())
|
||||
.then(() => setup.screenshotCreateOrCompare(app, t, 'play-torrent-wired-2'))
|
||||
// Play from end of song, let it advance on its own
|
||||
.then(() => app.webContents.executeJavaScript('dispatch("skipTo", 206)'))
|
||||
.then(() => app.webContents.executeJavaScript('dispatch("playPause")'))
|
||||
.then(() => setup.wait(5e3)) // Let it play a few seconds, past the end of the song
|
||||
.then(() => app.webContents.executeJavaScript('dispatch("playPause")'))
|
||||
.then(() => app.webContents.executeJavaScript('dispatch("skipTo", 2)'))
|
||||
.then(() => setup.wait())
|
||||
.then(() => setup.screenshotCreateOrCompare(app, t, 'play-torrent-wired-3'))
|
||||
// Fullscreen
|
||||
.then(() => app.client.click('.fullscreen'))
|
||||
.then(() => setup.wait())
|
||||
.then(() => setup.screenshotCreateOrCompare(app, t, 'play-torrent-wired-fullscreen'))
|
||||
// Back to normal audio view. Give the player controls have had time to disappear.
|
||||
.then(() => app.webContents.executeJavaScript('dispatch("escapeBack")'))
|
||||
.then(() => setup.wait())
|
||||
.then(() => setup.screenshotCreateOrCompare(app, t, 'play-torrent-wired-4'))
|
||||
// Back. Return to torrent list
|
||||
.then(() => app.client.click('.back'))
|
||||
.then(() => app.client.waitUntilTextExists('Big Buck Bunny'))
|
||||
.then(() => setup.screenshotCreateOrCompare(app, t, 'play-torrent-wired-list'))
|
||||
// Forward. Should play again where we left off (should not stay paused)
|
||||
.then(() => app.client.click('.forward'))
|
||||
.then(() => setup.wait())
|
||||
.then(() => app.webContents.executeJavaScript('dispatch("playPause")'))
|
||||
.then(() => app.webContents.executeJavaScript('dispatch("skipTo", 2)'))
|
||||
.then(() => setup.wait())
|
||||
.then(() => setup.screenshotCreateOrCompare(app, t, 'play-torrent-wired-5'))
|
||||
.then(() => setup.endTest(app, t),
|
||||
(err) => setup.endTest(app, t, err || 'error'))
|
||||
})
|
||||
115
test/test-torrent-list.js
Normal file
@@ -0,0 +1,115 @@
|
||||
const test = require('tape')
|
||||
const fs = require('fs-extra')
|
||||
const setup = require('./setup')
|
||||
const config = require('./config')
|
||||
|
||||
test('torrent-list: show download path missing', function (t) {
|
||||
setup.resetTestDataDir()
|
||||
fs.removeSync(config.TEST_DIR_DOWNLOAD)
|
||||
|
||||
t.timeoutAfter(10e3)
|
||||
const app = setup.createApp()
|
||||
setup.waitForLoad(app, t)
|
||||
.then(() => app.client.getTitle())
|
||||
.then((text) => console.log('Title ' + text))
|
||||
.then(() => app.client.waitUntilTextExists('.torrent-list', 'Download path missing'))
|
||||
.then((err) => t.notOk(err))
|
||||
.then(() => setup.screenshotCreateOrCompare(app, t, 'torrent-list-download-path-missing'))
|
||||
.then(() => app.client.click('a'))
|
||||
.then(() => setup.wait())
|
||||
.then(() => app.browserWindow.getTitle())
|
||||
.then((windowTitle) => t.equal(windowTitle, 'Preferences', 'window title'))
|
||||
.then(() => setup.screenshotCreateOrCompare(app, t, 'prefs-basic'))
|
||||
.then(() => setup.endTest(app, t),
|
||||
(err) => setup.endTest(app, t, err || 'error'))
|
||||
})
|
||||
|
||||
test('torrent-list: start, stop, and delete torrents', function (t) {
|
||||
setup.resetTestDataDir()
|
||||
|
||||
const app = setup.createApp()
|
||||
setup.waitForLoad(app, t, {offline: true})
|
||||
.then(() => app.client.waitUntilTextExists('.torrent-list', 'Big Buck Bunny'))
|
||||
// Mouse over the first torrent
|
||||
.then(() => app.client.moveToObject('.torrent'))
|
||||
.then(() => setup.wait())
|
||||
.then(() => setup.screenshotCreateOrCompare(app, t, 'torrent-list-hover'))
|
||||
// Click download on the first torrent, start downloading
|
||||
.then(() => app.client.click('.icon.download'))
|
||||
.then(() => app.client.waitUntilTextExists('.torrent-list', '276MB'))
|
||||
.then(() => setup.screenshotCreateOrCompare(app, t, 'torrent-list-start-download'))
|
||||
// Click download on the first torrent again, stop downloading
|
||||
.then(() => app.client.click('.icon.download'))
|
||||
.then(() => setup.wait())
|
||||
.then(() => setup.screenshotCreateOrCompare(app, t, 'torrent-list-hover-download'))
|
||||
// Click delete on the first torrent
|
||||
.then(() => app.client.click('.icon.delete'))
|
||||
.then(() => setup.wait())
|
||||
.then(() => setup.screenshotCreateOrCompare(app, t, 'torrent-list-delete-prompt'))
|
||||
// Click cancel on the resulting confirmation dialog. Should be same as before.
|
||||
.then(() => app.client.click('.control.cancel'))
|
||||
.then(() => setup.wait())
|
||||
.then(() => setup.screenshotCreateOrCompare(app, t, 'torrent-list-hover'))
|
||||
// Click delete on the first torrent again
|
||||
.then(() => app.client.click('.icon.delete'))
|
||||
.then(() => setup.wait())
|
||||
.then(() => setup.screenshotCreateOrCompare(app, t, 'torrent-list-delete-prompt'))
|
||||
// This time, click OK to confirm.
|
||||
.then(() => app.client.click('.control.ok'))
|
||||
.then(() => setup.wait())
|
||||
.then(() => setup.screenshotCreateOrCompare(app, t, 'torrent-list-deleted'))
|
||||
.then(() => setup.endTest(app, t),
|
||||
(err) => setup.endTest(app, t, err || 'error'))
|
||||
})
|
||||
|
||||
test('torrent-list: expand torrent, unselect file', function (t) {
|
||||
setup.resetTestDataDir()
|
||||
|
||||
const app = setup.createApp()
|
||||
setup.waitForLoad(app, t, {offline: true})
|
||||
.then(() => app.client.waitUntilTextExists('.torrent-list', 'Big Buck Bunny'))
|
||||
// Mouse over the torrent
|
||||
.then(() => app.client.moveToObject('#torrent-cosmos'))
|
||||
.then(() => setup.wait())
|
||||
.then(() => setup.screenshotCreateOrCompare(app, t, 'torrent-list-cosmos-hover'))
|
||||
// Click on the torrent, expand
|
||||
.then(() => app.client.click('#torrent-cosmos'))
|
||||
.then(() => setup.wait())
|
||||
.then(() => setup.screenshotCreateOrCompare(app, t, 'torrent-list-cosmos-expand'))
|
||||
// Deselect the first file
|
||||
.then(() => app.client.click('#torrent-cosmos .icon.deselect-file'))
|
||||
.then(() => setup.wait())
|
||||
.then(() => setup.screenshotCreateOrCompare(app, t, 'torrent-list-cosmos-expand-deselect'))
|
||||
// Start the torrent
|
||||
.then(() => app.client.click('#torrent-cosmos .icon.download'))
|
||||
.then(() => app.client.waitUntilTextExists('.torrent-list', 'peers'))
|
||||
.then(() => setup.screenshotCreateOrCompare(app, t, 'torrent-list-cosmos-expand-start'))
|
||||
// Make sure that it creates all files EXCEPT the deslected one
|
||||
.then(() => setup.compareDownloadFolder(t, 'CosmosLaundromatFirstCycle', [
|
||||
// TODO: the .gif should NOT be here, since we just deselected it.
|
||||
// This is a bug. See https://github.com/feross/webtorrent-desktop/issues/719
|
||||
'Cosmos Laundromat - First Cycle (1080p).gif',
|
||||
'Cosmos Laundromat - First Cycle (1080p).mp4',
|
||||
'Cosmos Laundromat - First Cycle (1080p).ogv',
|
||||
'CosmosLaundromat-FirstCycle1080p.en.srt',
|
||||
'CosmosLaundromat-FirstCycle1080p.es.srt',
|
||||
'CosmosLaundromat-FirstCycle1080p.fr.srt',
|
||||
'CosmosLaundromat-FirstCycle1080p.it.srt',
|
||||
'CosmosLaundromatFirstCycle_meta.sqlite',
|
||||
'CosmosLaundromatFirstCycle_meta.xml'
|
||||
]))
|
||||
// Delete torrent plus data
|
||||
// Spectron doesn't have proper support for menu clicks yet...
|
||||
.then(() => app.webContents.executeJavaScript(
|
||||
'dispatch("confirmDeleteTorrent", "6a02592d2bbc069628cd5ed8a54f88ee06ac0ba5", true)'))
|
||||
.then(() => setup.wait())
|
||||
.then(() => setup.screenshotCreateOrCompare(app, t, 'torrent-list-cosmos-delete-data'))
|
||||
// Click confirm
|
||||
.then(() => app.client.click('.control.ok'))
|
||||
.then(() => setup.wait())
|
||||
.then(() => setup.screenshotCreateOrCompare(app, t, 'torrent-list-cosmos-deleted'))
|
||||
// Make sure that all the files are gone
|
||||
.then(() => setup.compareDownloadFolder(t, 'CosmosLaundromatFirstCycle', null))
|
||||
.then(() => setup.endTest(app, t),
|
||||
(err) => setup.endTest(app, t, err || 'error'))
|
||||
})
|
||||
36
test/test-video.js
Normal file
@@ -0,0 +1,36 @@
|
||||
const test = require('tape')
|
||||
const setup = require('./setup')
|
||||
|
||||
test('video-streaming', function (t) {
|
||||
setup.resetTestDataDir()
|
||||
|
||||
t.timeoutAfter(30e3)
|
||||
const app = setup.createApp()
|
||||
setup.waitForLoad(app, t, {online: true})
|
||||
.then(() => app.client.waitUntilTextExists('.torrent-list', 'Big Buck Bunny'))
|
||||
// Play Big Buck Bunny. Wait for it to start streaming.
|
||||
.then(() => app.client.moveToObject('.torrent'))
|
||||
.then(() => setup.wait())
|
||||
.then(() => app.client.click('.icon.play'))
|
||||
.then(() => setup.wait(10e3))
|
||||
// Pause. Skip to two seconds in. Wait another two seconds for it to load.
|
||||
.then(() => app.webContents.executeJavaScript('dispatch("playPause")'))
|
||||
.then(() => app.webContents.executeJavaScript('dispatch("skipTo", 2)'))
|
||||
.then(() => setup.wait(5e3))
|
||||
// Take a screenshot to verify video playback
|
||||
.then(() => setup.screenshotCreateOrCompare(app, t, 'play-torrent-bbb'))
|
||||
// Hit escape
|
||||
.then(() => app.webContents.executeJavaScript('dispatch("escapeBack")'))
|
||||
.then(() => setup.wait())
|
||||
// Delete Big Buck Bunny
|
||||
.then(() => app.client.moveToObject('.torrent'))
|
||||
.then(() => setup.wait())
|
||||
.then(() => app.client.click('.icon.delete'))
|
||||
.then(() => setup.wait())
|
||||
.then(() => app.client.click('.control.ok'))
|
||||
.then(() => setup.wait())
|
||||
// Take another screenshot to verify that the window resized correctly
|
||||
.then(() => setup.screenshotCreateOrCompare(app, t, 'play-torrent-return'))
|
||||
.then(() => setup.endTest(app, t),
|
||||
(err) => setup.endTest(app, t, err || 'error'))
|
||||
})
|
||||