Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c549fcfc27 | ||
|
|
45e838d4c3 | ||
|
|
64f49e4d4f | ||
|
|
61caa90901 | ||
|
|
3e85289318 | ||
|
|
3a4906079b | ||
|
|
3edf21f457 | ||
|
|
785c44cd2a | ||
|
|
1ad8a5313b | ||
|
|
967e161288 | ||
|
|
fe8c3b190c | ||
|
|
993e7d77ad | ||
|
|
e0be052df4 | ||
|
|
d331bae548 | ||
|
|
d88229694a | ||
|
|
8da5b955d6 | ||
|
|
54882679c1 | ||
|
|
f2007be1b0 | ||
|
|
7a757f9e05 |
2
.gitignore
vendored
@@ -1,4 +1,4 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
build/
|
build/
|
||||||
dist/
|
dist/
|
||||||
npm-debug.log.*
|
npm-debug.log*
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "webtorrent-desktop",
|
"name": "webtorrent-desktop",
|
||||||
"description": "WebTorrent, the streaming torrent client. For Mac, Windows, and Linux.",
|
"description": "WebTorrent, the streaming torrent client. For Mac, Windows, and Linux.",
|
||||||
"version": "0.14.0",
|
"version": "0.15.0",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "WebTorrent, LLC",
|
"name": "WebTorrent, LLC",
|
||||||
"email": "feross@webtorrent.io",
|
"email": "feross@webtorrent.io",
|
||||||
@@ -16,13 +16,15 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"airplayer": "^2.0.0",
|
"airplayer": "^2.0.0",
|
||||||
"application-config": "^1.0.0",
|
"application-config": "^1.0.0",
|
||||||
|
"auto-launch": "^4.0.1",
|
||||||
"bitfield": "^1.0.2",
|
"bitfield": "^1.0.2",
|
||||||
"chromecasts": "^1.8.0",
|
"chromecasts": "^1.8.0",
|
||||||
"create-torrent": "^3.24.5",
|
"create-torrent": "^3.24.5",
|
||||||
"deep-equal": "^1.0.1",
|
"deep-equal": "^1.0.1",
|
||||||
"dlnacasts": "^0.1.0",
|
"dlnacasts": "^0.1.0",
|
||||||
"drag-drop": "^2.12.1",
|
"drag-drop": "^2.12.1",
|
||||||
"electron": "1.3.3",
|
"electron": "1.3.5",
|
||||||
|
"es6-error": "^3.0.1",
|
||||||
"fs-extra": "^0.30.0",
|
"fs-extra": "^0.30.0",
|
||||||
"iso-639-1": "^1.2.1",
|
"iso-639-1": "^1.2.1",
|
||||||
"languagedetect": "^1.1.1",
|
"languagedetect": "^1.1.1",
|
||||||
@@ -65,7 +67,9 @@
|
|||||||
"plist": "^2.0.1",
|
"plist": "^2.0.1",
|
||||||
"rimraf": "^2.5.2",
|
"rimraf": "^2.5.2",
|
||||||
"run-series": "^1.1.4",
|
"run-series": "^1.1.4",
|
||||||
|
"spectron": "^3.3.0",
|
||||||
"standard": "*",
|
"standard": "*",
|
||||||
|
"tape": "^4.6.0",
|
||||||
"walk-sync": "^0.3.1"
|
"walk-sync": "^0.3.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -99,6 +103,7 @@
|
|||||||
"package": "node ./bin/package.js",
|
"package": "node ./bin/package.js",
|
||||||
"prepublish": "npm run build",
|
"prepublish": "npm run build",
|
||||||
"start": "npm run build && electron .",
|
"start": "npm run build && electron .",
|
||||||
|
"integration-test": "npm run build && node ./test",
|
||||||
"test": "standard && depcheck --ignores=babel-cli,nodemon,gh-release --ignore-dirs=build,dist && node ./bin/extra-lint.js",
|
"test": "standard && depcheck --ignores=babel-cli,nodemon,gh-release --ignore-dirs=build,dist && node ./bin/extra-lint.js",
|
||||||
"gh-release": "gh-release",
|
"gh-release": "gh-release",
|
||||||
"update-authors": "./bin/update-authors.sh",
|
"update-authors": "./bin/update-authors.sh",
|
||||||
|
|||||||
@@ -1,12 +1,22 @@
|
|||||||
const appConfig = require('application-config')('WebTorrent')
|
const appConfig = require('application-config')('WebTorrent')
|
||||||
const fs = require('fs')
|
const fs = require('fs')
|
||||||
const path = require('path')
|
const path = require('path')
|
||||||
|
const electron = require('electron')
|
||||||
|
|
||||||
const APP_NAME = 'WebTorrent'
|
const APP_NAME = 'WebTorrent'
|
||||||
const APP_TEAM = 'WebTorrent, LLC'
|
const APP_TEAM = 'WebTorrent, LLC'
|
||||||
const APP_VERSION = require('../package.json').version
|
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 = {
|
module.exports = {
|
||||||
ANNOUNCEMENT_URL: 'https://webtorrent.io/desktop/announcement',
|
ANNOUNCEMENT_URL: 'https://webtorrent.io/desktop/announcement',
|
||||||
@@ -26,26 +36,31 @@ module.exports = {
|
|||||||
|
|
||||||
DEFAULT_TORRENTS: [
|
DEFAULT_TORRENTS: [
|
||||||
{
|
{
|
||||||
|
testID: 'bbb',
|
||||||
name: 'Big Buck Bunny',
|
name: 'Big Buck Bunny',
|
||||||
posterFileName: 'bigBuckBunny.jpg',
|
posterFileName: 'bigBuckBunny.jpg',
|
||||||
torrentFileName: 'bigBuckBunny.torrent'
|
torrentFileName: 'bigBuckBunny.torrent'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
testID: 'cosmos',
|
||||||
name: 'Cosmos Laundromat (Preview)',
|
name: 'Cosmos Laundromat (Preview)',
|
||||||
posterFileName: 'cosmosLaundromat.jpg',
|
posterFileName: 'cosmosLaundromat.jpg',
|
||||||
torrentFileName: 'cosmosLaundromat.torrent'
|
torrentFileName: 'cosmosLaundromat.torrent'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
testID: 'sintel',
|
||||||
name: 'Sintel',
|
name: 'Sintel',
|
||||||
posterFileName: 'sintel.jpg',
|
posterFileName: 'sintel.jpg',
|
||||||
torrentFileName: 'sintel.torrent'
|
torrentFileName: 'sintel.torrent'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
testID: 'tears',
|
||||||
name: 'Tears of Steel',
|
name: 'Tears of Steel',
|
||||||
posterFileName: 'tearsOfSteel.jpg',
|
posterFileName: 'tearsOfSteel.jpg',
|
||||||
torrentFileName: 'tearsOfSteel.torrent'
|
torrentFileName: 'tearsOfSteel.torrent'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
testID: 'wired',
|
||||||
name: 'The WIRED CD - Rip. Sample. Mash. Share.',
|
name: 'The WIRED CD - Rip. Sample. Mash. Share.',
|
||||||
posterFileName: 'wiredCd.jpg',
|
posterFileName: 'wiredCd.jpg',
|
||||||
torrentFileName: 'wiredCd.torrent'
|
torrentFileName: 'wiredCd.torrent'
|
||||||
@@ -62,8 +77,9 @@ module.exports = {
|
|||||||
|
|
||||||
HOME_PAGE_URL: 'https://webtorrent.io',
|
HOME_PAGE_URL: 'https://webtorrent.io',
|
||||||
|
|
||||||
IS_PORTABLE: isPortable(),
|
IS_PORTABLE: IS_PORTABLE,
|
||||||
IS_PRODUCTION: isProduction(),
|
IS_PRODUCTION: IS_PRODUCTION,
|
||||||
|
IS_TEST: IS_TEST,
|
||||||
|
|
||||||
POSTER_PATH: path.join(getConfigPath(), 'Posters'),
|
POSTER_PATH: path.join(getConfigPath(), 'Posters'),
|
||||||
ROOT_PATH: path.join(__dirname, '..'),
|
ROOT_PATH: path.join(__dirname, '..'),
|
||||||
@@ -79,7 +95,7 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getConfigPath () {
|
function getConfigPath () {
|
||||||
if (isPortable()) {
|
if (IS_PORTABLE) {
|
||||||
return PORTABLE_PATH
|
return PORTABLE_PATH
|
||||||
} else {
|
} else {
|
||||||
return path.dirname(appConfig.filePath)
|
return path.dirname(appConfig.filePath)
|
||||||
@@ -89,22 +105,31 @@ function getConfigPath () {
|
|||||||
function getDefaultDownloadPath () {
|
function getDefaultDownloadPath () {
|
||||||
if (!process || !process.type) {
|
if (!process || !process.type) {
|
||||||
return ''
|
return ''
|
||||||
}
|
} else if (IS_PORTABLE) {
|
||||||
|
|
||||||
if (isPortable()) {
|
|
||||||
return path.join(getConfigPath(), 'Downloads')
|
return path.join(getConfigPath(), 'Downloads')
|
||||||
|
} else {
|
||||||
|
return getPath('downloads')
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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'
|
function isTest () {
|
||||||
? electron.remote.app.getPath('downloads')
|
return process.env.NODE_ENV === 'test'
|
||||||
: electron.app.getPath('downloads')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function isPortable () {
|
function isPortable () {
|
||||||
|
if (IS_TEST) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
return process.platform === 'win32' && isProduction() && !!fs.statSync(PORTABLE_PATH)
|
return process.platform === 'win32' && IS_PRODUCTION && !!fs.statSync(PORTABLE_PATH)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const cp = require('child_process')
|
const cp = require('child_process')
|
||||||
|
const path = require('path')
|
||||||
const vlcCommand = require('vlc-command')
|
const vlcCommand = require('vlc-command')
|
||||||
|
|
||||||
const log = require('./log')
|
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
|
// holds a ChildProcess while we're playing a video in an external player, null otherwise
|
||||||
let proc = null
|
let proc = null
|
||||||
|
|
||||||
function checkInstall (path, cb) {
|
function checkInstall (playerPath, cb) {
|
||||||
// check for VLC if external player has not been specified by the user
|
// check for VLC if external player has not been specified by the user
|
||||||
// otherwise assume the player is installed
|
// otherwise assume the player is installed
|
||||||
if (path == null) return vlcCommand((err) => cb(!err))
|
if (playerPath == null) return vlcCommand((err) => cb(!err))
|
||||||
process.nextTick(() => cb(true))
|
process.nextTick(() => cb(true))
|
||||||
}
|
}
|
||||||
|
|
||||||
function spawn (path, url, title) {
|
function spawn (playerPath, url, title) {
|
||||||
if (path != null) return spawnExternal(path, [url])
|
if (playerPath != null) return spawnExternal(playerPath, [url])
|
||||||
|
|
||||||
// Try to find and use VLC if external player is not specified
|
// Try to find and use VLC if external player is not specified
|
||||||
vlcCommand(function (err, vlcPath) {
|
vlcCommand(function (err, vlcPath) {
|
||||||
@@ -44,10 +45,15 @@ function kill () {
|
|||||||
proc = null
|
proc = null
|
||||||
}
|
}
|
||||||
|
|
||||||
function spawnExternal (path, args) {
|
function spawnExternal (playerPath, args) {
|
||||||
log('Running external media player:', path + ' ' + args.join(' '))
|
log('Running external media player:', playerPath + ' ' + args.join(' '))
|
||||||
|
|
||||||
proc = cp.spawn(path, args, {stdio: 'ignore'})
|
if (path.extname(playerPath) === '.app') {
|
||||||
|
// Mac: Use executable in packaged .app bundle
|
||||||
|
playerPath += '/Contents/MacOS/' + path.basename(playerPath, '.app')
|
||||||
|
}
|
||||||
|
|
||||||
|
proc = cp.spawn(playerPath, args, {stdio: 'ignore'})
|
||||||
|
|
||||||
// If it works, close the modal after a second
|
// If it works, close the modal after a second
|
||||||
const closeModalTimeout = setTimeout(() =>
|
const closeModalTimeout = setTimeout(() =>
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
console.time('init')
|
console.time('init')
|
||||||
|
|
||||||
const electron = require('electron')
|
const electron = require('electron')
|
||||||
|
|
||||||
const app = electron.app
|
const app = electron.app
|
||||||
const ipcMain = electron.ipcMain
|
const ipcMain = electron.ipcMain
|
||||||
|
|
||||||
@@ -22,6 +21,11 @@ const windows = require('./windows')
|
|||||||
let shouldQuit = false
|
let shouldQuit = false
|
||||||
let argv = sliceArgv(process.argv)
|
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) {
|
if (config.IS_PRODUCTION) {
|
||||||
// When Electron is running in production mode (packaged app), then run React
|
// When Electron is running in production mode (packaged app), then run React
|
||||||
// in production mode too.
|
// in production mode too.
|
||||||
@@ -68,7 +72,7 @@ function init () {
|
|||||||
app.on('ready', function () {
|
app.on('ready', function () {
|
||||||
isReady = true
|
isReady = true
|
||||||
|
|
||||||
windows.main.init()
|
windows.main.init({hidden: hidden})
|
||||||
windows.webtorrent.init()
|
windows.webtorrent.init()
|
||||||
menu.init()
|
menu.init()
|
||||||
|
|
||||||
@@ -156,9 +160,15 @@ function processArgv (argv) {
|
|||||||
dialog.openTorrentFile()
|
dialog.openTorrentFile()
|
||||||
} else if (arg === '-u') {
|
} else if (arg === '-u') {
|
||||||
dialog.openTorrentAddress()
|
dialog.openTorrentAddress()
|
||||||
|
} else if (arg === '--hidden') {
|
||||||
|
// Ignore hidden argument, already being handled
|
||||||
} else if (arg.startsWith('-psn')) {
|
} else if (arg.startsWith('-psn')) {
|
||||||
// Ignore Mac launchd "process serial number" argument
|
// Ignore Mac launchd "process serial number" argument
|
||||||
// Issue: https://github.com/feross/webtorrent-desktop/issues/214
|
// Issue: https://github.com/feross/webtorrent-desktop/issues/214
|
||||||
|
} else if (arg.startsWith('--')) {
|
||||||
|
// Ignore Spectron flags
|
||||||
|
} else if (arg === 'data:,') {
|
||||||
|
// Ignore weird Spectron argument
|
||||||
} else if (arg !== '.') {
|
} else if (arg !== '.') {
|
||||||
// Ignore '.' argument, which gets misinterpreted as a torrent id, when a
|
// Ignore '.' argument, which gets misinterpreted as a torrent id, when a
|
||||||
// development copy of WebTorrent is started while a production version is
|
// development copy of WebTorrent is started while a production version is
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ const shortcuts = require('./shortcuts')
|
|||||||
const externalPlayer = require('./external-player')
|
const externalPlayer = require('./external-player')
|
||||||
const windows = require('./windows')
|
const windows = require('./windows')
|
||||||
const thumbar = require('./thumbar')
|
const thumbar = require('./thumbar')
|
||||||
|
const startup = require('./startup')
|
||||||
|
|
||||||
// Messages from the main process, to be sent once the WebTorrent process starts
|
// Messages from the main process, to be sent once the WebTorrent process starts
|
||||||
const messageQueueMainToWebTorrent = []
|
const messageQueueMainToWebTorrent = []
|
||||||
@@ -58,7 +59,7 @@ function init () {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
ipc.on('onPlayerOpen', function () {
|
ipc.on('onPlayerOpen', function () {
|
||||||
menu.setPlayerOpen(true)
|
menu.togglePlaybackControls(true)
|
||||||
powerSaveBlocker.enable()
|
powerSaveBlocker.enable()
|
||||||
shortcuts.enable()
|
shortcuts.enable()
|
||||||
thumbar.enable()
|
thumbar.enable()
|
||||||
@@ -70,7 +71,7 @@ function init () {
|
|||||||
})
|
})
|
||||||
|
|
||||||
ipc.on('onPlayerClose', function () {
|
ipc.on('onPlayerClose', function () {
|
||||||
menu.setPlayerOpen(false)
|
menu.togglePlaybackControls(false)
|
||||||
powerSaveBlocker.disable()
|
powerSaveBlocker.disable()
|
||||||
shortcuts.disable()
|
shortcuts.disable()
|
||||||
thumbar.disable()
|
thumbar.disable()
|
||||||
@@ -102,6 +103,14 @@ function init () {
|
|||||||
else handlers.uninstall()
|
else handlers.uninstall()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Startup
|
||||||
|
*/
|
||||||
|
ipc.on('setStartup', (e, flag) => {
|
||||||
|
if (flag) startup.install()
|
||||||
|
else startup.uninstall()
|
||||||
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Windows: Main
|
* Windows: Main
|
||||||
*/
|
*/
|
||||||
@@ -126,7 +135,12 @@ function init () {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
ipc.on('openExternalPlayer', (e, ...args) => externalPlayer.spawn(...args))
|
ipc.on('openExternalPlayer', (e, ...args) => {
|
||||||
|
menu.togglePlaybackControls(false)
|
||||||
|
thumbar.disable()
|
||||||
|
externalPlayer.spawn(...args)
|
||||||
|
})
|
||||||
|
|
||||||
ipc.on('quitExternalPlayer', () => externalPlayer.kill())
|
ipc.on('quitExternalPlayer', () => externalPlayer.kill())
|
||||||
|
|
||||||
// Capture all events
|
// Capture all events
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
init,
|
init,
|
||||||
setPlayerOpen,
|
togglePlaybackControls,
|
||||||
setWindowFocus,
|
setWindowFocus,
|
||||||
setAllowNav,
|
setAllowNav,
|
||||||
onPlayerUpdate,
|
onPlayerUpdate,
|
||||||
@@ -24,7 +24,7 @@ function init () {
|
|||||||
electron.Menu.setApplicationMenu(menu)
|
electron.Menu.setApplicationMenu(menu)
|
||||||
}
|
}
|
||||||
|
|
||||||
function setPlayerOpen (flag) {
|
function togglePlaybackControls (flag) {
|
||||||
getMenuItem('Play/Pause').enabled = flag
|
getMenuItem('Play/Pause').enabled = flag
|
||||||
getMenuItem('Skip Next').enabled = flag
|
getMenuItem('Skip Next').enabled = flag
|
||||||
getMenuItem('Skip Previous').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()
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -26,7 +26,7 @@ const tray = require('../tray')
|
|||||||
const HEADER_HEIGHT = 38
|
const HEADER_HEIGHT = 38
|
||||||
const TORRENT_HEIGHT = 100
|
const TORRENT_HEIGHT = 100
|
||||||
|
|
||||||
function init () {
|
function init (options) {
|
||||||
if (main.win) {
|
if (main.win) {
|
||||||
return main.win.show()
|
return main.win.show()
|
||||||
}
|
}
|
||||||
@@ -40,7 +40,8 @@ function init () {
|
|||||||
titleBarStyle: 'hidden-inset', // Hide title bar (Mac)
|
titleBarStyle: 'hidden-inset', // Hide title bar (Mac)
|
||||||
useContentSize: true, // Specify web page size without OS chrome
|
useContentSize: true, // Specify web page size without OS chrome
|
||||||
width: 500,
|
width: 500,
|
||||||
height: HEADER_HEIGHT + (TORRENT_HEIGHT * 6) // header height + 5 torrents
|
height: HEADER_HEIGHT + (TORRENT_HEIGHT * 6), // header height + 5 torrents
|
||||||
|
show: !options.hidden
|
||||||
})
|
})
|
||||||
|
|
||||||
win.loadURL(config.WINDOW_MAIN)
|
win.loadURL(config.WINDOW_MAIN)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ class Header extends React.Component {
|
|||||||
render () {
|
render () {
|
||||||
const loc = this.props.state.location
|
const loc = this.props.state.location
|
||||||
return (
|
return (
|
||||||
<div className='header'>
|
<div className='header' onMouseMove={dispatcher('mediaMouseMoved')}>
|
||||||
{this.getTitle()}
|
{this.getTitle()}
|
||||||
<div className='nav left float-left'>
|
<div className='nav left float-left'>
|
||||||
<i
|
<i
|
||||||
|
|||||||
@@ -9,12 +9,12 @@ module.exports = class ModalOKCancel extends React.Component {
|
|||||||
return (
|
return (
|
||||||
<div className='float-right'>
|
<div className='float-right'>
|
||||||
<FlatButton
|
<FlatButton
|
||||||
className='control'
|
className='control cancel'
|
||||||
style={cancelStyle}
|
style={cancelStyle}
|
||||||
label={cancelText}
|
label={cancelText}
|
||||||
onClick={onCancel} />
|
onClick={onCancel} />
|
||||||
<RaisedButton
|
<RaisedButton
|
||||||
className='control'
|
className='control ok'
|
||||||
primary
|
primary
|
||||||
label={okText}
|
label={okText}
|
||||||
onClick={onOK} />
|
onClick={onOK} />
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ module.exports = class OpenTorrentAddressModal extends React.Component {
|
|||||||
<p><label>Enter torrent address or magnet link</label></p>
|
<p><label>Enter torrent address or magnet link</label></p>
|
||||||
<div>
|
<div>
|
||||||
<TextField
|
<TextField
|
||||||
|
id='torrent-address-field'
|
||||||
className='control'
|
className='control'
|
||||||
ref={(c) => { this.torrentURL = c }}
|
ref={(c) => { this.torrentURL = c }}
|
||||||
fullWidth
|
fullWidth
|
||||||
@@ -31,7 +32,7 @@ module.exports = class OpenTorrentAddressModal extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleKeyDown (e) {
|
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 () {
|
function handleOK () {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
const React = require('react')
|
const React = require('react')
|
||||||
const electron = require('electron')
|
const electron = require('electron')
|
||||||
const path = require('path')
|
|
||||||
|
|
||||||
const ModalOKCancel = require('./modal-ok-cancel')
|
const ModalOKCancel = require('./modal-ok-cancel')
|
||||||
const {dispatcher} = require('../lib/dispatcher')
|
const {dispatcher} = require('../lib/dispatcher')
|
||||||
@@ -12,15 +11,11 @@ module.exports = class UnsupportedMediaModal extends React.Component {
|
|||||||
const message = (err && err.getMessage)
|
const message = (err && err.getMessage)
|
||||||
? err.getMessage()
|
? err.getMessage()
|
||||||
: err
|
: err
|
||||||
const playerPath = state.saved.prefs.externalPlayerPath
|
|
||||||
const playerName = playerPath
|
|
||||||
? path.basename(playerPath).split('.')[0]
|
|
||||||
: 'VLC'
|
|
||||||
const onAction = state.modal.externalPlayerInstalled
|
const onAction = state.modal.externalPlayerInstalled
|
||||||
? dispatcher('openExternalPlayer')
|
? dispatcher('openExternalPlayer')
|
||||||
: () => this.onInstall()
|
: () => this.onInstall()
|
||||||
const actionText = state.modal.externalPlayerInstalled
|
const actionText = state.modal.externalPlayerInstalled
|
||||||
? 'PLAY IN ' + playerName.toUpperCase()
|
? 'PLAY IN ' + state.getExternalPlayerName().toUpperCase()
|
||||||
: 'INSTALL VLC'
|
: 'INSTALL VLC'
|
||||||
const errorMessage = state.modal.externalPlayerNotFound
|
const errorMessage = state.modal.externalPlayerNotFound
|
||||||
? 'Couldn\'t run external player. Please make sure it\'s installed.'
|
? 'Couldn\'t run external player. Please make sure it\'s installed.'
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ const path = require('path')
|
|||||||
const Cast = require('../lib/cast')
|
const Cast = require('../lib/cast')
|
||||||
const {dispatch} = require('../lib/dispatcher')
|
const {dispatch} = require('../lib/dispatcher')
|
||||||
const telemetry = require('../lib/telemetry')
|
const telemetry = require('../lib/telemetry')
|
||||||
const errors = require('../lib/errors')
|
const {UnplayableFileError, UnplayableTorrentError,
|
||||||
|
PlaybackTimedOutError} = require('../lib/errors')
|
||||||
const sound = require('../lib/sound')
|
const sound = require('../lib/sound')
|
||||||
const TorrentPlayer = require('../lib/torrent-player')
|
const TorrentPlayer = require('../lib/torrent-player')
|
||||||
const TorrentSummary = require('../lib/torrent-summary')
|
const TorrentSummary = require('../lib/torrent-summary')
|
||||||
@@ -42,7 +43,7 @@ module.exports = class PlaybackController {
|
|||||||
|
|
||||||
if (index === undefined || initialized) index = torrentSummary.mostRecentFileIndex
|
if (index === undefined || initialized) index = torrentSummary.mostRecentFileIndex
|
||||||
if (index === undefined) index = torrentSummary.files.findIndex(TorrentPlayer.isPlayable)
|
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
|
initialized = true
|
||||||
|
|
||||||
@@ -87,7 +88,7 @@ module.exports = class PlaybackController {
|
|||||||
// Play next file in list (if any)
|
// Play next file in list (if any)
|
||||||
nextTrack () {
|
nextTrack () {
|
||||||
const state = this.state
|
const state = this.state
|
||||||
if (Playlist.hasNext(state)) {
|
if (Playlist.hasNext(state) && state.playing.location !== 'external') {
|
||||||
this.updatePlayer(
|
this.updatePlayer(
|
||||||
state.playing.infoHash, Playlist.getNextIndex(state), false, (err) => {
|
state.playing.infoHash, Playlist.getNextIndex(state), false, (err) => {
|
||||||
if (err) dispatch('error', err)
|
if (err) dispatch('error', err)
|
||||||
@@ -99,7 +100,7 @@ module.exports = class PlaybackController {
|
|||||||
// Play previous track in list (if any)
|
// Play previous track in list (if any)
|
||||||
previousTrack () {
|
previousTrack () {
|
||||||
const state = this.state
|
const state = this.state
|
||||||
if (Playlist.hasPrevious(state)) {
|
if (Playlist.hasPrevious(state) && state.playing.location !== 'external') {
|
||||||
this.updatePlayer(
|
this.updatePlayer(
|
||||||
state.playing.infoHash, Playlist.getPreviousIndex(state), false, (err) => {
|
state.playing.infoHash, Playlist.getPreviousIndex(state), false, (err) => {
|
||||||
if (err) dispatch('error', err)
|
if (err) dispatch('error', err)
|
||||||
@@ -232,7 +233,7 @@ module.exports = class PlaybackController {
|
|||||||
// TODO: remove torrentSummary.playStatus
|
// TODO: remove torrentSummary.playStatus
|
||||||
torrentSummary.playStatus = 'timeout' /* no seeders available? */
|
torrentSummary.playStatus = 'timeout' /* no seeders available? */
|
||||||
sound.play('ERROR')
|
sound.play('ERROR')
|
||||||
cb(new Error('Playback timed out. Try again.'))
|
cb(new PlaybackTimedOutError())
|
||||||
this.update()
|
this.update()
|
||||||
}, 10000) /* give it a few seconds */
|
}, 10000) /* give it a few seconds */
|
||||||
|
|
||||||
@@ -277,7 +278,7 @@ module.exports = class PlaybackController {
|
|||||||
|
|
||||||
if (!TorrentPlayer.isPlayable(fileSummary)) {
|
if (!TorrentPlayer.isPlayable(fileSummary)) {
|
||||||
torrentSummary.mostRecentFileIndex = undefined
|
torrentSummary.mostRecentFileIndex = undefined
|
||||||
return cb(new Error('Can\'t play that file'))
|
return cb(new UnplayableFileError())
|
||||||
}
|
}
|
||||||
|
|
||||||
torrentSummary.mostRecentFileIndex = index
|
torrentSummary.mostRecentFileIndex = index
|
||||||
|
|||||||
@@ -17,7 +17,9 @@ module.exports = class PrefsController {
|
|||||||
setup: function (cb) {
|
setup: function (cb) {
|
||||||
// initialize preferences
|
// initialize preferences
|
||||||
state.window.title = 'Preferences'
|
state.window.title = 'Preferences'
|
||||||
state.unsaved = Object.assign(state.unsaved || {}, {prefs: state.saved.prefs || {}})
|
state.unsaved = Object.assign(state.unsaved || {}, {
|
||||||
|
prefs: Object.assign({}, state.saved.prefs)
|
||||||
|
})
|
||||||
ipcRenderer.send('setAllowNav', false)
|
ipcRenderer.send('setAllowNav', false)
|
||||||
cb()
|
cb()
|
||||||
},
|
},
|
||||||
@@ -50,6 +52,9 @@ module.exports = class PrefsController {
|
|||||||
if (state.unsaved.prefs.isFileHandler !== state.saved.prefs.isFileHandler) {
|
if (state.unsaved.prefs.isFileHandler !== state.saved.prefs.isFileHandler) {
|
||||||
ipcRenderer.send('setDefaultFileHandler', state.unsaved.prefs.isFileHandler)
|
ipcRenderer.send('setDefaultFileHandler', state.unsaved.prefs.isFileHandler)
|
||||||
}
|
}
|
||||||
|
if (state.unsaved.prefs.startup !== state.saved.prefs.startup) {
|
||||||
|
ipcRenderer.send('setStartup', state.unsaved.prefs.startup)
|
||||||
|
}
|
||||||
state.saved.prefs = Object.assign(state.saved.prefs || {}, state.unsaved.prefs)
|
state.saved.prefs = Object.assign(state.saved.prefs || {}, state.unsaved.prefs)
|
||||||
State.save(state)
|
State.save(state)
|
||||||
dispatch('checkDownloadPath')
|
dispatch('checkDownloadPath')
|
||||||
|
|||||||
@@ -33,11 +33,10 @@ module.exports = class SubtitlesController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
addSubtitles (files, autoSelect) {
|
addSubtitles (files, autoSelect) {
|
||||||
const state = this.state
|
|
||||||
// Subtitles are only supported when playing video files
|
// Subtitles are only supported when playing video files
|
||||||
if (state.playing.type !== 'video') return
|
if (this.state.playing.type !== 'video') return
|
||||||
if (files.length === 0) return
|
if (files.length === 0) return
|
||||||
const subtitles = state.playing.subtitles
|
const subtitles = this.state.playing.subtitles
|
||||||
|
|
||||||
// Read the files concurrently, then add all resulting subtitle tracks
|
// Read the files concurrently, then add all resulting subtitle tracks
|
||||||
const tasks = files.map((file) => (cb) => loadSubtitle(file, cb))
|
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++) {
|
for (let i = 0; i < tracks.length; i++) {
|
||||||
// No dupes allowed
|
// No dupes allowed
|
||||||
const track = tracks[i]
|
const track = tracks[i]
|
||||||
let trackIndex = state.playing.subtitles.tracks
|
let trackIndex = subtitles.tracks.findIndex((t) =>
|
||||||
.findIndex((t) => track.filePath === t.filePath)
|
track.filePath === t.filePath)
|
||||||
|
|
||||||
// Add the track
|
// Add the track
|
||||||
if (trackIndex === -1) {
|
if (trackIndex === -1) {
|
||||||
trackIndex = state.playing.subtitles.tracks.push(track) - 1
|
trackIndex = subtitles.tracks.push(track) - 1
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we're auto-selecting a track, try to find one in the user's language
|
// If we're auto-selecting a track, try to find one in the user's language
|
||||||
if (autoSelect && (i === 0 || isSystemLanguage(track.language))) {
|
if (autoSelect && (i === 0 || isSystemLanguage(track.language))) {
|
||||||
state.playing.subtitles.selectedIndex = trackIndex
|
subtitles.selectedIndex = trackIndex
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -127,6 +127,7 @@ module.exports = class TorrentController {
|
|||||||
|
|
||||||
torrentFileModtimes (torrentKey, fileModtimes) {
|
torrentFileModtimes (torrentKey, fileModtimes) {
|
||||||
const torrentSummary = this.getTorrentSummary(torrentKey)
|
const torrentSummary = this.getTorrentSummary(torrentKey)
|
||||||
|
if (!torrentSummary) throw new Error('Not saving modtimes for deleted torrent ' + torrentKey)
|
||||||
torrentSummary.fileModtimes = fileModtimes
|
torrentSummary.fileModtimes = fileModtimes
|
||||||
dispatch('saveStateThrottled')
|
dispatch('saveStateThrottled')
|
||||||
}
|
}
|
||||||
@@ -176,7 +177,7 @@ function showDoneNotification (torrent) {
|
|||||||
silent: true
|
silent: true
|
||||||
})
|
})
|
||||||
|
|
||||||
notif.onClick = function () {
|
notif.onclick = function () {
|
||||||
ipcRenderer.send('show')
|
ipcRenderer.send('show')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ const path = require('path')
|
|||||||
const electron = require('electron')
|
const electron = require('electron')
|
||||||
|
|
||||||
const {dispatch} = require('../lib/dispatcher')
|
const {dispatch} = require('../lib/dispatcher')
|
||||||
|
const {TorrentKeyNotFoundError} = require('../lib/errors')
|
||||||
const State = require('../lib/state')
|
const State = require('../lib/state')
|
||||||
const sound = require('../lib/sound')
|
const sound = require('../lib/sound')
|
||||||
const TorrentSummary = require('../lib/torrent-summary')
|
const TorrentSummary = require('../lib/torrent-summary')
|
||||||
@@ -75,7 +76,7 @@ module.exports = class TorrentListController {
|
|||||||
// Starts downloading and/or seeding a given torrentSummary.
|
// Starts downloading and/or seeding a given torrentSummary.
|
||||||
startTorrentingSummary (torrentKey) {
|
startTorrentingSummary (torrentKey) {
|
||||||
const s = TorrentSummary.getByKey(this.state, torrentKey)
|
const s = TorrentSummary.getByKey(this.state, torrentKey)
|
||||||
if (!s) throw new Error('Missing key: ' + torrentKey)
|
if (!s) throw new TorrentKeyNotFoundError(torrentKey)
|
||||||
|
|
||||||
// New torrent: give it a path
|
// New torrent: give it a path
|
||||||
if (!s.path) {
|
if (!s.path) {
|
||||||
@@ -127,7 +128,9 @@ module.exports = class TorrentListController {
|
|||||||
torrentSummary.selections[index] = !torrentSummary.selections[index]
|
torrentSummary.selections[index] = !torrentSummary.selections[index]
|
||||||
|
|
||||||
// Let the WebTorrent process know to start or stop fetching that file
|
// Let the WebTorrent process know to start or stop fetching that file
|
||||||
ipcRenderer.send('wt-select-files', infoHash, torrentSummary.selections)
|
if (torrentSummary.status !== 'paused') {
|
||||||
|
ipcRenderer.send('wt-select-files', infoHash, torrentSummary.selections)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
confirmDeleteTorrent (infoHash, deleteData) {
|
confirmDeleteTorrent (infoHash, deleteData) {
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
module.exports = captureVideoFrame
|
module.exports = captureVideoFrame
|
||||||
|
|
||||||
|
const {IllegalArgumentError} = require('./errors')
|
||||||
|
|
||||||
function captureVideoFrame (video, format) {
|
function captureVideoFrame (video, format) {
|
||||||
if (typeof video === 'string') {
|
if (typeof video === 'string') {
|
||||||
video = document.querySelector(video)
|
video = document.querySelector(video)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (video == null || video.nodeName !== 'VIDEO') {
|
if (video == null || video.nodeName !== 'VIDEO') {
|
||||||
throw new Error('First argument must be a <video> element or selector')
|
throw new IllegalArgumentError('First argument must be a <video> element or selector')
|
||||||
}
|
}
|
||||||
|
|
||||||
if (format == null) {
|
if (format == null) {
|
||||||
@@ -14,7 +16,7 @@ function captureVideoFrame (video, format) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (format !== 'png' && format !== 'jpg' && format !== 'webp') {
|
if (format !== 'png' && format !== 'jpg' && format !== 'webp') {
|
||||||
throw new Error('Second argument must be one of "png", "jpg", or "webp"')
|
throw new IllegalArgumentError('Second argument must be one of "png", "jpg", or "webp"')
|
||||||
}
|
}
|
||||||
|
|
||||||
const canvas = document.createElement('canvas')
|
const canvas = document.createElement('canvas')
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const config = require('../../config')
|
const config = require('../../config')
|
||||||
|
const {CastingError} = require('./errors')
|
||||||
|
|
||||||
// Lazy load these for a ~300ms improvement in startup time
|
// Lazy load these for a ~300ms improvement in startup time
|
||||||
let airplayer, chromecasts, dlnacasts
|
let airplayer, chromecasts, dlnacasts
|
||||||
@@ -126,13 +127,7 @@ function chromecastPlayer () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function status () {
|
function status () {
|
||||||
ret.device.status(function (err, status) {
|
ret.device.status(handleStatus)
|
||||||
if (err) return console.log('error getting %s status: %o', state.playing.location, err)
|
|
||||||
state.playing.isPaused = status.playerState === 'PAUSED'
|
|
||||||
state.playing.currentTime = status.currentTime
|
|
||||||
state.playing.volume = status.volume.muted ? 0 : status.volume.level
|
|
||||||
update()
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function seek (time, callback) {
|
function seek (time, callback) {
|
||||||
@@ -306,13 +301,7 @@ function dlnaPlayer (player) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function status () {
|
function status () {
|
||||||
ret.device.status(function (err, status) {
|
ret.device.status(handleStatus)
|
||||||
if (err) return console.log('error getting %s status: %o', state.playing.location, err)
|
|
||||||
state.playing.isPaused = status.playerState === 'PAUSED'
|
|
||||||
state.playing.currentTime = status.currentTime
|
|
||||||
state.playing.volume = status.volume.level
|
|
||||||
update()
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function seek (time, callback) {
|
function seek (time, callback) {
|
||||||
@@ -328,6 +317,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
|
// Start polling cast device state, whenever we're connected
|
||||||
function startStatusInterval () {
|
function startStatusInterval () {
|
||||||
statusInterval = setInterval(function () {
|
statusInterval = setInterval(function () {
|
||||||
@@ -350,14 +351,16 @@ function toggleMenu (location) {
|
|||||||
|
|
||||||
// Never cast to two devices at the same time
|
// Never cast to two devices at the same time
|
||||||
if (state.playing.location !== 'local') {
|
if (state.playing.location !== 'local') {
|
||||||
throw new Error('You can\'t connect to ' + location +
|
throw new CastingError(
|
||||||
' when already connected to another device')
|
`You can't connect to ${location} when already connected to another device`
|
||||||
}
|
) }
|
||||||
|
|
||||||
// Find all cast devices of the given type
|
// Find all cast devices of the given type
|
||||||
const player = getPlayer(location)
|
const player = getPlayer(location)
|
||||||
const devices = player ? player.getDevices() : []
|
const devices = player ? player.getDevices() : []
|
||||||
if (devices.length === 0) throw new Error('No ' + location + ' devices available')
|
if (devices.length === 0) {
|
||||||
|
throw new CastingError(`No ${location} devices available`)
|
||||||
|
}
|
||||||
|
|
||||||
// Show a menu
|
// Show a menu
|
||||||
state.devices.castMenu = {location, devices}
|
state.devices.castMenu = {location, devices}
|
||||||
|
|||||||
@@ -1,15 +1,54 @@
|
|||||||
|
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 {}
|
||||||
|
|
||||||
|
/* Miscellaneous */
|
||||||
|
|
||||||
|
class IllegalArgumentError extends ExtendableError {}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
CastingError,
|
||||||
|
PlaybackError,
|
||||||
|
SoundError,
|
||||||
|
TorrentError,
|
||||||
UnplayableTorrentError,
|
UnplayableTorrentError,
|
||||||
UnplayableFileError
|
UnplayableFileError,
|
||||||
|
PlaybackTimedOutError,
|
||||||
|
InvalidSoundNameError,
|
||||||
|
TorrentKeyNotFoundError,
|
||||||
|
InvalidTorrentError,
|
||||||
|
IllegalArgumentError
|
||||||
}
|
}
|
||||||
|
|
||||||
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 config = require('../../config')
|
||||||
|
const {InvalidSoundNameError} = require('./errors')
|
||||||
const path = require('path')
|
const path = require('path')
|
||||||
|
|
||||||
const VOLUME = 0.15
|
const VOLUME = 0.15
|
||||||
@@ -62,7 +63,7 @@ function play (name) {
|
|||||||
if (!audio) {
|
if (!audio) {
|
||||||
const sound = sounds[name]
|
const sound = sounds[name]
|
||||||
if (!sound) {
|
if (!sound) {
|
||||||
throw new Error('Invalid sound name')
|
throw new InvalidSoundNameError(name)
|
||||||
}
|
}
|
||||||
audio = cache[name] = new window.Audio()
|
audio = cache[name] = new window.Audio()
|
||||||
audio.volume = sound.volume
|
audio.volume = sound.volume
|
||||||
|
|||||||
@@ -68,7 +68,8 @@ function getDefaultState () {
|
|||||||
* Getters, for convenience
|
* Getters, for convenience
|
||||||
*/
|
*/
|
||||||
getPlayingTorrentSummary,
|
getPlayingTorrentSummary,
|
||||||
getPlayingFileSummary
|
getPlayingFileSummary,
|
||||||
|
getExternalPlayerName
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,7 +108,8 @@ function setupSavedState (cb) {
|
|||||||
downloadPath: config.DEFAULT_DOWNLOAD_PATH,
|
downloadPath: config.DEFAULT_DOWNLOAD_PATH,
|
||||||
isFileHandler: false,
|
isFileHandler: false,
|
||||||
openExternalPlayer: false,
|
openExternalPlayer: false,
|
||||||
externalPlayerPath: null
|
externalPlayerPath: null,
|
||||||
|
startup: false
|
||||||
},
|
},
|
||||||
torrents: config.DEFAULT_TORRENTS.map(createTorrentObject),
|
torrents: config.DEFAULT_TORRENTS.map(createTorrentObject),
|
||||||
version: config.APP_VERSION /* make sure we can upgrade gracefully later */
|
version: config.APP_VERSION /* make sure we can upgrade gracefully later */
|
||||||
@@ -151,7 +153,8 @@ function setupSavedState (cb) {
|
|||||||
torrentFileName: parsedTorrent.infoHash + '.torrent',
|
torrentFileName: parsedTorrent.infoHash + '.torrent',
|
||||||
magnetURI: parseTorrent.toMagnetURI(parsedTorrent),
|
magnetURI: parseTorrent.toMagnetURI(parsedTorrent),
|
||||||
files: parsedTorrent.files,
|
files: parsedTorrent.files,
|
||||||
selections: parsedTorrent.files.map((x) => true)
|
selections: parsedTorrent.files.map((x) => true),
|
||||||
|
testID: t.testID
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -167,6 +170,12 @@ function getPlayingFileSummary () {
|
|||||||
return torrentSummary.files[this.playing.fileIndex]
|
return torrentSummary.files[this.playing.fileIndex]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getExternalPlayerName () {
|
||||||
|
const playerPath = this.saved.prefs.externalPlayerPath
|
||||||
|
if (!playerPath) return 'VLC'
|
||||||
|
return path.basename(playerPath).split('.')[0]
|
||||||
|
}
|
||||||
|
|
||||||
function load (cb) {
|
function load (cb) {
|
||||||
const state = getDefaultState()
|
const state = getDefaultState()
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,8 @@ function init (state) {
|
|||||||
telemetry.localTime = now.toTimeString()
|
telemetry.localTime = now.toTimeString()
|
||||||
telemetry.screens = getScreenInfo()
|
telemetry.screens = getScreenInfo()
|
||||||
telemetry.system = getSystemInfo()
|
telemetry.system = getSystemInfo()
|
||||||
telemetry.approxNumTorrents = getApproxNumTorrents(state)
|
telemetry.torrentStats = getTorrentStats(state)
|
||||||
|
telemetry.approxNumTorrents = telemetry.torrentStats.approxCount
|
||||||
|
|
||||||
if (config.IS_PRODUCTION) {
|
if (config.IS_PRODUCTION) {
|
||||||
postToServer()
|
postToServer()
|
||||||
@@ -104,18 +105,63 @@ function getSystemInfo () {
|
|||||||
osPlatform: process.platform,
|
osPlatform: process.platform,
|
||||||
osRelease: os.type() + ' ' + os.release(),
|
osRelease: os.type() + ' ' + os.release(),
|
||||||
architecture: os.arch(),
|
architecture: os.arch(),
|
||||||
totalMemoryMB: os.totalmem() / (1 << 20),
|
totalMemoryMB: roundPow2(os.totalmem() / (1 << 20)),
|
||||||
numCores: os.cpus().length
|
numCores: os.cpus().length
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the number of torrents, rounded to the nearest power of two
|
// Get stats like the # of torrents currently active, # in list, total size
|
||||||
function getApproxNumTorrents (state) {
|
function getTorrentStats (state) {
|
||||||
const exactNum = state.saved.torrents.length
|
const count = state.saved.torrents.length
|
||||||
if (exactNum === 0) return 0
|
let sizeMB = 0
|
||||||
|
let byStatus = {
|
||||||
|
new: { count: 0, sizeMB: 0 },
|
||||||
|
downloading: { count: 0, sizeMB: 0 },
|
||||||
|
seeding: { count: 0, sizeMB: 0 },
|
||||||
|
paused: { count: 0, sizeMB: 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
// First, count torrents & total file size
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
const t = state.saved.torrents[i]
|
||||||
|
const stat = byStatus[t.status]
|
||||||
|
if (!t || !t.files || !stat) continue
|
||||||
|
stat.count++
|
||||||
|
for (let j = 0; j < t.files.length; j++) {
|
||||||
|
const f = t.files[j]
|
||||||
|
if (!f || !f.length) continue
|
||||||
|
const fileSizeMB = f.length / (1 << 20)
|
||||||
|
sizeMB += fileSizeMB
|
||||||
|
stat.sizeMB += fileSizeMB
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then, round all the counts and sums to the nearest power of 2
|
||||||
|
const ret = roundTorrentStats({count, sizeMB})
|
||||||
|
ret.byStatus = {
|
||||||
|
new: roundTorrentStats(byStatus.new),
|
||||||
|
downloading: roundTorrentStats(byStatus.downloading),
|
||||||
|
seeding: roundTorrentStats(byStatus.seeding),
|
||||||
|
paused: roundTorrentStats(byStatus.paused)
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
function roundTorrentStats (stats) {
|
||||||
|
return {
|
||||||
|
approxCount: roundPow2(stats.count),
|
||||||
|
approxSizeMB: roundPow2(stats.sizeMB)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rounds to the nearest power of 2, for privacy and easy bucketing.
|
||||||
|
// Rounds 35 to 32, 70 to 64, 5 to 4, 1 to 1, 0 to 0.
|
||||||
|
// Supports nonnegative numbers only.
|
||||||
|
function roundPow2 (n) {
|
||||||
|
if (n <= 0) return 0
|
||||||
// Otherwise, return 1, 2, 4, 8, etc by rounding in log space
|
// Otherwise, return 1, 2, 4, 8, etc by rounding in log space
|
||||||
const log2 = Math.log(exactNum) / Math.log(2)
|
const log2 = Math.log(n) / Math.log(2)
|
||||||
return 1 << Math.round(log2)
|
return Math.pow(2, Math.round(log2))
|
||||||
}
|
}
|
||||||
|
|
||||||
// An uncaught error happened in the main process or in one of the windows
|
// An uncaught error happened in the main process or in one of the windows
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ State.load(onState)
|
|||||||
function onState (err, _state) {
|
function onState (err, _state) {
|
||||||
if (err) return onError(err)
|
if (err) return onError(err)
|
||||||
state = window.state = _state // Make available for easier debugging
|
state = window.state = _state // Make available for easier debugging
|
||||||
|
window.dispatch = dispatch
|
||||||
|
|
||||||
telemetry.init(state)
|
telemetry.init(state)
|
||||||
|
|
||||||
@@ -122,7 +123,9 @@ function onState (err, _state) {
|
|||||||
document.addEventListener('webkitvisibilitychange', onVisibilityChange)
|
document.addEventListener('webkitvisibilitychange', onVisibilityChange)
|
||||||
|
|
||||||
// Done! Ideally we want to get here < 500ms after the user clicks the app
|
// Done! Ideally we want to get here < 500ms after the user clicks the app
|
||||||
sound.play('STARTUP')
|
if (electron.remote.getCurrentWindow().isVisible()) {
|
||||||
|
sound.play('STARTUP')
|
||||||
|
}
|
||||||
console.timeEnd('init')
|
console.timeEnd('init')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ const React = require('react')
|
|||||||
const Bitfield = require('bitfield')
|
const Bitfield = require('bitfield')
|
||||||
const prettyBytes = require('prettier-bytes')
|
const prettyBytes = require('prettier-bytes')
|
||||||
const zeroFill = require('zero-fill')
|
const zeroFill = require('zero-fill')
|
||||||
const path = require('path')
|
|
||||||
|
|
||||||
const TorrentSummary = require('../lib/torrent-summary')
|
const TorrentSummary = require('../lib/torrent-summary')
|
||||||
const Playlist = require('../lib/playlist')
|
const Playlist = require('../lib/playlist')
|
||||||
@@ -289,11 +288,8 @@ function renderCastScreen (state) {
|
|||||||
castType = 'DLNA'
|
castType = 'DLNA'
|
||||||
isCast = true
|
isCast = true
|
||||||
} else if (state.playing.location === 'external') {
|
} else if (state.playing.location === 'external') {
|
||||||
// TODO: get the player name in a more reliable way
|
|
||||||
const playerPath = state.saved.prefs.externalPlayerPath
|
|
||||||
const playerName = playerPath ? path.basename(playerPath).split('.')[0] : 'VLC'
|
|
||||||
castIcon = 'tv'
|
castIcon = 'tv'
|
||||||
castType = playerName
|
castType = state.getExternalPlayerName()
|
||||||
isCast = false
|
isCast = false
|
||||||
} else if (state.playing.location === 'error') {
|
} else if (state.playing.location === 'error') {
|
||||||
castIcon = 'error_outline'
|
castIcon = 'error_outline'
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
const colors = require('material-ui/styles/colors')
|
|
||||||
const path = require('path')
|
const path = require('path')
|
||||||
const React = require('react')
|
const React = require('react')
|
||||||
|
|
||||||
|
const colors = require('material-ui/styles/colors')
|
||||||
const Checkbox = require('material-ui/Checkbox').default
|
const Checkbox = require('material-ui/Checkbox').default
|
||||||
|
const RaisedButton = require('material-ui/RaisedButton').default
|
||||||
const Heading = require('../components/heading')
|
const Heading = require('../components/heading')
|
||||||
const PathSelector = require('../components/path-selector')
|
const PathSelector = require('../components/path-selector')
|
||||||
const RaisedButton = require('material-ui/RaisedButton').default
|
|
||||||
|
|
||||||
const {dispatch} = require('../lib/dispatcher')
|
const {dispatch} = require('../lib/dispatcher')
|
||||||
|
const config = require('../../config')
|
||||||
|
|
||||||
class PreferencesPage extends React.Component {
|
class PreferencesPage extends React.Component {
|
||||||
constructor (props) {
|
constructor (props) {
|
||||||
@@ -21,6 +22,9 @@ class PreferencesPage extends React.Component {
|
|||||||
|
|
||||||
this.handleExternalPlayerPathChange =
|
this.handleExternalPlayerPathChange =
|
||||||
this.handleExternalPlayerPathChange.bind(this)
|
this.handleExternalPlayerPathChange.bind(this)
|
||||||
|
|
||||||
|
this.handleStartupChange =
|
||||||
|
this.handleStartupChange.bind(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
downloadPathSelector () {
|
downloadPathSelector () {
|
||||||
@@ -59,9 +63,8 @@ class PreferencesPage extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
externalPlayerPathSelector () {
|
externalPlayerPathSelector () {
|
||||||
const playerName = path.basename(
|
const playerPath = this.props.state.unsaved.prefs.externalPlayerPath
|
||||||
this.props.state.unsaved.prefs.externalPlayerPath || 'VLC'
|
const playerName = this.props.state.getExternalPlayerName()
|
||||||
)
|
|
||||||
|
|
||||||
const description = this.props.state.unsaved.prefs.openExternalPlayer
|
const description = this.props.state.unsaved.prefs.openExternalPlayer
|
||||||
? `Torrent media files will always play in ${playerName}.`
|
? `Torrent media files will always play in ${playerName}.`
|
||||||
@@ -79,16 +82,12 @@ class PreferencesPage extends React.Component {
|
|||||||
displayValue={playerName}
|
displayValue={playerName}
|
||||||
onChange={this.handleExternalPlayerPathChange}
|
onChange={this.handleExternalPlayerPathChange}
|
||||||
title='External player'
|
title='External player'
|
||||||
value={this.props.state.unsaved.prefs.externalPlayerPath} />
|
value={playerPath ? path.dirname(playerPath) : null} />
|
||||||
</Preference>
|
</Preference>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
handleExternalPlayerPathChange (filePath) {
|
handleExternalPlayerPathChange (filePath) {
|
||||||
if (path.extname(filePath) === '.app') {
|
|
||||||
// Mac: Use executable in packaged .app bundle
|
|
||||||
filePath += '/Contents/MacOS/' + path.basename(filePath, '.app')
|
|
||||||
}
|
|
||||||
dispatch('updatePreferences', 'externalPlayerPath', filePath)
|
dispatch('updatePreferences', 'externalPlayerPath', filePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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 () {
|
handleSetDefaultApp () {
|
||||||
dispatch('updatePreferences', 'isFileHandler', true)
|
dispatch('updatePreferences', 'isFileHandler', true)
|
||||||
}
|
}
|
||||||
@@ -134,6 +156,7 @@ class PreferencesPage extends React.Component {
|
|||||||
<PreferencesSection title='Default torrent app'>
|
<PreferencesSection title='Default torrent app'>
|
||||||
{this.setDefaultAppButton()}
|
{this.setDefaultAppButton()}
|
||||||
</PreferencesSection>
|
</PreferencesSection>
|
||||||
|
{this.setStartupSection()}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ const prettyBytes = require('prettier-bytes')
|
|||||||
const TorrentSummary = require('../lib/torrent-summary')
|
const TorrentSummary = require('../lib/torrent-summary')
|
||||||
const TorrentPlayer = require('../lib/torrent-player')
|
const TorrentPlayer = require('../lib/torrent-player')
|
||||||
const {dispatcher} = require('../lib/dispatcher')
|
const {dispatcher} = require('../lib/dispatcher')
|
||||||
|
const {InvalidTorrentError} = require('../lib/errors')
|
||||||
|
|
||||||
module.exports = class TorrentList extends React.Component {
|
module.exports = class TorrentList extends React.Component {
|
||||||
render () {
|
render () {
|
||||||
@@ -58,9 +59,10 @@ module.exports = class TorrentList extends React.Component {
|
|||||||
if (torrentSummary.playStatus) classes.push(torrentSummary.playStatus)
|
if (torrentSummary.playStatus) classes.push(torrentSummary.playStatus)
|
||||||
if (isSelected) classes.push('selected')
|
if (isSelected) classes.push('selected')
|
||||||
if (!infoHash) classes.push('disabled')
|
if (!infoHash) classes.push('disabled')
|
||||||
if (!torrentSummary.torrentKey) throw new Error('Missing torrentKey')
|
if (!torrentSummary.torrentKey) throw new InvalidTorrentError('Missing torrentKey')
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
id={torrentSummary.testID && ('torrent-' + torrentSummary.testID)}
|
||||||
key={torrentSummary.torrentKey}
|
key={torrentSummary.torrentKey}
|
||||||
style={style}
|
style={style}
|
||||||
className={classes.join(' ')}
|
className={classes.join(' ')}
|
||||||
@@ -345,7 +347,7 @@ module.exports = class TorrentList extends React.Component {
|
|||||||
</td>
|
</td>
|
||||||
<td className='col-select'
|
<td className='col-select'
|
||||||
onClick={dispatcher('toggleTorrentFile', infoHash, index)}>
|
onClick={dispatcher('toggleTorrentFile', infoHash, index)}>
|
||||||
<i className='icon'>{isSelected ? 'close' : 'add'}</i>
|
<i className='icon deselect-file'>{isSelected ? 'close' : 'add'}</i>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ const zeroFill = require('zero-fill')
|
|||||||
|
|
||||||
const crashReporter = require('../crash-reporter')
|
const crashReporter = require('../crash-reporter')
|
||||||
const config = require('../config')
|
const config = require('../config')
|
||||||
|
const {TorrentKeyNotFoundError} = require('./lib/errors')
|
||||||
const torrentPoster = require('./lib/torrent-poster')
|
const torrentPoster = require('./lib/torrent-poster')
|
||||||
|
|
||||||
// Report when the process crashes
|
// Report when the process crashes
|
||||||
@@ -350,6 +351,9 @@ function selectFiles (torrentOrInfoHash, selections) {
|
|||||||
} else {
|
} else {
|
||||||
torrent = torrentOrInfoHash
|
torrent = torrentOrInfoHash
|
||||||
}
|
}
|
||||||
|
if (!torrent) {
|
||||||
|
throw new Error('selectFiles: missing torrent ' + torrentOrInfoHash)
|
||||||
|
}
|
||||||
|
|
||||||
// Selections not specified?
|
// Selections not specified?
|
||||||
// Load all files. We still need to replace the default whole-torrent
|
// Load all files. We still need to replace the default whole-torrent
|
||||||
@@ -384,7 +388,7 @@ function selectFiles (torrentOrInfoHash, selections) {
|
|||||||
// Throws an Error if we're not currently torrenting anything w/ that key
|
// Throws an Error if we're not currently torrenting anything w/ that key
|
||||||
function getTorrent (torrentKey) {
|
function getTorrent (torrentKey) {
|
||||||
const ret = client.torrents.find((x) => x.key === torrentKey)
|
const ret = client.torrents.find((x) => x.key === torrentKey)
|
||||||
if (!ret) throw new Error('missing torrent key ' + torrentKey)
|
if (!ret) throw new TorrentKeyNotFoundError(torrentKey)
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>WebTorrent Desktop</title>
|
<title>Main Window</title>
|
||||||
<link rel="stylesheet" href="main.css">
|
<link rel="stylesheet" href="main.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>WebTorrent Desktop</title>
|
<title>WebTorrent Hidden Window</title>
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
background-color: #282828;
|
background-color: #282828;
|
||||||
|
|||||||
33
test/index.js
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
const test = require('tape')
|
||||||
|
const fs = require('fs-extra')
|
||||||
|
const setup = require('./setup')
|
||||||
|
|
||||||
|
console.log('Creating download dir: ' + setup.TEST_DOWNLOAD_DIR)
|
||||||
|
fs.mkdirpSync(setup.TEST_DOWNLOAD_DIR)
|
||||||
|
|
||||||
|
test.onFinish(function () {
|
||||||
|
console.log('Removing test dir: ' + setup.TEST_DATA_DIR)
|
||||||
|
fs.removeSync(setup.TEST_DATA_DIR) // includes download dir
|
||||||
|
})
|
||||||
|
|
||||||
|
test('app runs', function (t) {
|
||||||
|
t.timeoutAfter(10e3)
|
||||||
|
const app = setup.createApp()
|
||||||
|
setup.waitForLoad(app, t)
|
||||||
|
.then(() => setup.wait())
|
||||||
|
.then(() => setup.screenshotCreateOrCompare(app, t, 'torrent-list-basic'))
|
||||||
|
.then(() => setup.endTest(app, t),
|
||||||
|
(err) => setup.endTest(app, t, err || 'error'))
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log('Testing the torrent list (home page)...')
|
||||||
|
setup.wipeTestDataDir()
|
||||||
|
require('./test-torrent-list')
|
||||||
|
|
||||||
|
// TODO:
|
||||||
|
// require('./test-add-torrent')
|
||||||
|
// require('./test-create-torrent')
|
||||||
|
// require('./test-prefs')
|
||||||
|
// require('./test-video')
|
||||||
|
// require('./test-audio')
|
||||||
|
// require('./test-cast')
|
||||||
BIN
test/screenshots/darwin/prefs-basic.png
Normal file
|
After Width: | Height: | Size: 91 KiB |
BIN
test/screenshots/darwin/torrent-list-basic.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
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 |
111
test/setup.js
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
const path = require('path')
|
||||||
|
const Application = require('spectron').Application
|
||||||
|
const fs = require('fs-extra')
|
||||||
|
|
||||||
|
const TEST_DATA_DIR = path.join(__dirname, 'tempTestData')
|
||||||
|
const TEST_DOWNLOAD_DIR = path.join(TEST_DATA_DIR, 'Downloads')
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
TEST_DATA_DIR,
|
||||||
|
TEST_DOWNLOAD_DIR,
|
||||||
|
createApp,
|
||||||
|
endTest,
|
||||||
|
screenshotCreateOrCompare,
|
||||||
|
compareDownloadFolder,
|
||||||
|
waitForLoad,
|
||||||
|
wait,
|
||||||
|
wipeTestDataDir
|
||||||
|
}
|
||||||
|
|
||||||
|
// Runs WebTorrent Desktop.
|
||||||
|
// Returns a promise that resolves to a Spectron Application once the app has loaded.
|
||||||
|
// Takes a Tape test. Makes some basic assertions to verify that the app loaded correctly.
|
||||||
|
function createApp (t) {
|
||||||
|
return new Application({
|
||||||
|
path: path.join(__dirname, '..', 'node_modules', '.bin',
|
||||||
|
'electron' + (process.platform === 'win32' ? '.cmd' : '')),
|
||||||
|
args: [path.join(__dirname, '..')],
|
||||||
|
env: {NODE_ENV: 'test'}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Starts the app, waits for it to load, returns a promise
|
||||||
|
function waitForLoad (app, t, opts) {
|
||||||
|
if (!opts) opts = {}
|
||||||
|
return app.start().then(function () {
|
||||||
|
return app.client.waitUntilWindowLoaded()
|
||||||
|
}).then(function () {
|
||||||
|
// Offline mode? Disable internet in the webtorrent window
|
||||||
|
// TODO. For now, just run integration tests with internet turned off.
|
||||||
|
// Spectron is poorly documented, and contrary to the docs, webContents.session is missing
|
||||||
|
// That is the correct API (in theory) to put the app in offline mode
|
||||||
|
}).then(function () {
|
||||||
|
// Switch to the main window. Index 0 is apparently the hidden webtorrent window...
|
||||||
|
return app.client.windowByIndex(1)
|
||||||
|
}).then(function () {
|
||||||
|
return app.client.waitUntilWindowLoaded()
|
||||||
|
}).then(function () {
|
||||||
|
return app.webContents.getTitle()
|
||||||
|
}).then(function (title) {
|
||||||
|
// Note the window title is WebTorrent (BETA), this is the HTML <title>
|
||||||
|
t.equal(title, 'Main Window', 'html title')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns a promise that resolves after 'ms' milliseconds. Default: 500
|
||||||
|
function wait (ms) {
|
||||||
|
if (ms === undefined) ms = 500 // Default: wait long enough for the UI to update
|
||||||
|
return new Promise(function (resolve, reject) {
|
||||||
|
setTimeout(resolve, ms)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Quit the app, end the test, either in success (!err) or failure (err)
|
||||||
|
function endTest (app, t, err) {
|
||||||
|
return app.stop().then(function () {
|
||||||
|
t.end(err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Takes a screenshot of the app
|
||||||
|
// If we already have a reference under test/screenshots, assert that they're the same
|
||||||
|
// Otherwise, create the reference screenshot: test/screenshots/<platform>/<name>.png
|
||||||
|
function screenshotCreateOrCompare (app, t, name) {
|
||||||
|
const ssDir = path.join(__dirname, 'screenshots', process.platform)
|
||||||
|
const ssPath = path.join(ssDir, name + '.png')
|
||||||
|
fs.ensureFileSync(ssPath)
|
||||||
|
const ssBuf = fs.readFileSync(ssPath)
|
||||||
|
return app.browserWindow.capturePage().then(function (buffer) {
|
||||||
|
if (ssBuf.length === 0) {
|
||||||
|
console.log('Saving screenshot ' + ssPath)
|
||||||
|
fs.writeFileSync(ssPath, buffer)
|
||||||
|
} else {
|
||||||
|
const match = Buffer.compare(buffer, ssBuf) === 0
|
||||||
|
t.ok(match, 'screenshot comparison ' + name)
|
||||||
|
if (!match) {
|
||||||
|
const ssFailedPath = path.join(ssDir, name + '-failed.png')
|
||||||
|
console.log('Saving screenshot, failed comparison: ' + ssFailedPath)
|
||||||
|
fs.writeFileSync(ssFailedPath, buffer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resets the test directory, containing config.json, torrents, downloads, etc
|
||||||
|
function wipeTestDataDir () {
|
||||||
|
fs.removeSync(TEST_DATA_DIR)
|
||||||
|
fs.mkdirpSync(TEST_DOWNLOAD_DIR) // Downloads/ is inside of TEST_DATA_DIR
|
||||||
|
}
|
||||||
|
|
||||||
|
function compareDownloadFolder (t, dirname, filenames) {
|
||||||
|
const dirpath = path.join(TEST_DOWNLOAD_DIR, dirname)
|
||||||
|
try {
|
||||||
|
const actualFilenames = fs.readdirSync(dirpath)
|
||||||
|
const expectedSorted = filenames.slice().sort()
|
||||||
|
const actualSorted = actualFilenames.slice().sort()
|
||||||
|
t.deepEqual(actualSorted, expectedSorted, 'download folder contents: ' + dirname)
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
t.equal(filenames, null, 'download folder missing: ' + dirname)
|
||||||
|
}
|
||||||
|
}
|
||||||
104
test/test-torrent-list.js
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
const test = require('tape')
|
||||||
|
const fs = require('fs-extra')
|
||||||
|
const setup = require('./setup')
|
||||||
|
|
||||||
|
test.skip('torrent-list: show download path missing', function (t) {
|
||||||
|
setup.wipeTestDataDir()
|
||||||
|
fs.removeSync(setup.TEST_DOWNLOAD_DIR)
|
||||||
|
|
||||||
|
t.timeoutAfter(10e3)
|
||||||
|
const app = setup.createApp()
|
||||||
|
setup.waitForLoad(app, t)
|
||||||
|
.then(() => app.client.getTitle())
|
||||||
|
.then((text) => console.log('Title ' + text))
|
||||||
|
.then(() => app.client.waitUntilTextExists('.torrent-list', 'Download path missing'))
|
||||||
|
.then((err) => t.notOk(err))
|
||||||
|
.then(() => setup.screenshotCreateOrCompare(app, t, 'torrent-list-download-path-missing'))
|
||||||
|
.then(() => app.client.click('a'))
|
||||||
|
.then(() => setup.wait())
|
||||||
|
.then(() => app.browserWindow.getTitle())
|
||||||
|
.then((windowTitle) => t.equal(windowTitle, 'Preferences', 'window title'))
|
||||||
|
.then(() => setup.screenshotCreateOrCompare(app, t, 'prefs-basic'))
|
||||||
|
.then(() => setup.endTest(app, t),
|
||||||
|
(err) => setup.endTest(app, t, err || 'error'))
|
||||||
|
})
|
||||||
|
|
||||||
|
test.skip('torrent-list: start, stop, and delete torrents', function (t) {
|
||||||
|
setup.wipeTestDataDir()
|
||||||
|
|
||||||
|
const app = setup.createApp()
|
||||||
|
setup.waitForLoad(app, t, {offline: true})
|
||||||
|
.then(() => app.client.waitUntilTextExists('.torrent-list', 'Big Buck Bunny'))
|
||||||
|
// Mouse over the first torrent
|
||||||
|
.then(() => app.client.moveToObject('.torrent'))
|
||||||
|
.then(() => setup.wait())
|
||||||
|
.then(() => setup.screenshotCreateOrCompare(app, t, 'torrent-list-hover'))
|
||||||
|
// Click download on the first torrent, start downloading
|
||||||
|
.then(() => app.client.click('.icon.download'))
|
||||||
|
.then(() => app.client.waitUntilTextExists('.torrent-list', '276 MB'))
|
||||||
|
.then(() => setup.screenshotCreateOrCompare(app, t, 'torrent-list-start-download'))
|
||||||
|
// Click download on the first torrent again, stop downloading
|
||||||
|
.then(() => app.client.click('.icon.download'))
|
||||||
|
.then(() => setup.wait())
|
||||||
|
.then(() => setup.screenshotCreateOrCompare(app, t, 'torrent-list-hover-download'))
|
||||||
|
// Click delete on the first torrent
|
||||||
|
.then(() => app.client.click('.icon.delete'))
|
||||||
|
.then(() => setup.wait())
|
||||||
|
.then(() => setup.screenshotCreateOrCompare(app, t, 'torrent-list-delete-prompt'))
|
||||||
|
// Click cancel on the resulting confirmation dialog. Should be same as before.
|
||||||
|
.then(() => app.client.click('.control.cancel'))
|
||||||
|
.then(() => setup.wait())
|
||||||
|
.then(() => setup.screenshotCreateOrCompare(app, t, 'torrent-list-hover'))
|
||||||
|
// Click delete on the first torrent again
|
||||||
|
.then(() => app.client.click('.icon.delete'))
|
||||||
|
.then(() => setup.wait())
|
||||||
|
.then(() => setup.screenshotCreateOrCompare(app, t, 'torrent-list-delete-prompt'))
|
||||||
|
// This time, click OK to confirm.
|
||||||
|
.then(() => app.client.click('.control.ok'))
|
||||||
|
.then(() => setup.wait())
|
||||||
|
.then(() => setup.screenshotCreateOrCompare(app, t, 'torrent-list-deleted'))
|
||||||
|
.then(() => setup.endTest(app, t),
|
||||||
|
(err) => setup.endTest(app, t, err || 'error'))
|
||||||
|
})
|
||||||
|
|
||||||
|
test('torrent-list: expand torrent, unselect file', function (t) {
|
||||||
|
setup.wipeTestDataDir()
|
||||||
|
|
||||||
|
const app = setup.createApp()
|
||||||
|
setup.waitForLoad(app, t, {offline: true})
|
||||||
|
.then(() => app.client.waitUntilTextExists('.torrent-list', 'Big Buck Bunny'))
|
||||||
|
// Mouse over the torrent
|
||||||
|
.then(() => app.client.moveToObject('#torrent-cosmos'))
|
||||||
|
.then(() => setup.wait())
|
||||||
|
.then(() => setup.screenshotCreateOrCompare(app, t, 'torrent-list-cosmos-hover'))
|
||||||
|
// Click on the torrent, expand
|
||||||
|
.then(() => app.client.click('#torrent-cosmos'))
|
||||||
|
.then(() => setup.wait())
|
||||||
|
.then(() => setup.screenshotCreateOrCompare(app, t, 'torrent-list-cosmos-expand'))
|
||||||
|
// Deselect the first file
|
||||||
|
.then(() => app.client.click('#torrent-cosmos .icon.deselect-file'))
|
||||||
|
.then(() => setup.wait())
|
||||||
|
.then(() => setup.screenshotCreateOrCompare(app, t, 'torrent-list-cosmos-expand-deselect'))
|
||||||
|
// Start the torrent
|
||||||
|
.then(() => app.client.click('#torrent-cosmos .icon.download'))
|
||||||
|
.then(() => app.client.waitUntilTextExists('.torrent-list', '0%'))
|
||||||
|
.then(() => setup.screenshotCreateOrCompare(app, t, 'torrent-list-cosmos-expand-start'))
|
||||||
|
// Make sure that it creates all files EXCEPT the deslected one
|
||||||
|
.then(() => setup.compareDownloadFolder(t, 'CosmosLaundromatFirstCycle', [
|
||||||
|
// TODO: the .gif should NOT be here, since we just deselected it.
|
||||||
|
// This is a bug. See https://github.com/feross/webtorrent-desktop/issues/719
|
||||||
|
'Cosmos Laundromat - First Cycle (1080p).gif',
|
||||||
|
'Cosmos Laundromat - First Cycle (1080p).mp4',
|
||||||
|
'Cosmos Laundromat - First Cycle (1080p).ogv',
|
||||||
|
'CosmosLaundromat-FirstCycle1080p.en.srt',
|
||||||
|
'CosmosLaundromat-FirstCycle1080p.es.srt',
|
||||||
|
'CosmosLaundromat-FirstCycle1080p.fr.srt',
|
||||||
|
'CosmosLaundromat-FirstCycle1080p.it.srt',
|
||||||
|
'CosmosLaundromatFirstCycle_meta.sqlite',
|
||||||
|
'CosmosLaundromatFirstCycle_meta.xml'
|
||||||
|
]))
|
||||||
|
// Make sure that all the files are gone
|
||||||
|
.then(() => setup.compareDownloadFolder(t, 'CosmosLaundromatFirstCycle', null))
|
||||||
|
.then(() => setup.endTest(app, t),
|
||||||
|
(err) => setup.endTest(app, t, err || 'error'))
|
||||||
|
})
|
||||||