Compare commits

..

12 Commits

Author SHA1 Message Date
Alberto Miranda
e2c3c182d0 Plugins divided into main and renderer processes. 2017-04-23 13:46:52 -03:00
Alberto Miranda
687038560c Passing state and params to plugins.
Dispatcher now sends params to plugins; notifying about plugin load
errors; allowing local plugin paths not to end with a slash.
2017-04-19 09:00:11 -03:00
Alberto Miranda
09d00c6383 Passing dispatcher to plugins.
This will enable plugins to dispatch events and interact with the
renderer process; removed unused extension keys.
2017-04-16 16:31:39 -03:00
Alberto Miranda
957c5b64e9 Merge branch 'master' into plugins 2017-04-16 13:29:51 -03:00
Alberto Miranda
568fede67e fixed periodic plugins update 2017-02-06 08:06:02 -03:00
Alberto Miranda
944eb8b8b0 styling fixes; tests passing; added missing ms dependency. 2017-02-06 00:54:16 -03:00
Alberto Miranda
c3cdda354e added node-notifier; showing plugin notifications. 2017-02-06 00:38:27 -03:00
Alberto Miranda
25ed12ba3c removed extra logging; cleanup 2017-02-06 00:00:26 -03:00
Alberto Miranda
830312842b hot reloading config; hot loading plugins when config changes. 2017-02-05 13:15:20 -03:00
Alberto Miranda
086d8bf00a fixed initialization on first run 2017-02-01 14:35:28 -03:00
Alberto Miranda
50c100130a removed unused packages; hashing installed plugins; saving plugins config; detecting newly installed plugins. 2017-02-01 00:34:28 -03:00
Alberto Miranda
edcea2661a working on plugins features: lots of todos, but current version loads remote and local plugins. 2017-01-30 00:08:22 -03:00
99 changed files with 1209 additions and 13102 deletions

19
.github/config.yml vendored
View File

@@ -1,19 +0,0 @@
# ProBot Request Info (https://probot.github.io/apps/request-info/)
requestInfoReplyComment: >
👋 We would appreciate it if you could provide us with more information about this
issue. <br><br> ![](https://feross.net/s/cats/14.gif)
requestInfoLabelToAdd: 'need more info'
# ProBot Welcome (https://probot.github.io/apps/welcome/)
newPRWelcomeComment: >
🙌 Thanks for opening this pull request! You're awesome.
<br><br> ![](https://feross.net/s/cats/6.gif)
firstPRMergeComment: >
🎉 Congrats on getting your first pull request landed! <br><br>
![](https://feross.net/s/cats/29.gif)
# ProBot WIP (https://probot.github.io/apps/wip/)
# (Requires no configuration)

4
.github/lock.yml vendored
View File

@@ -1,4 +0,0 @@
# ProBot Lock (https://probot.github.io/apps/lock/)
daysUntilLock: 90
lockComment: false

View File

@@ -1,9 +0,0 @@
# ProBot No Response (https://probot.github.io/apps/no-response/)
daysUntilClose: 7
responseRequiredLabel: 'need more info'
closeComment: >
This issue has been automatically closed because there was no response to a
request for more information from the issue opener. Please leave a comment or
open a new issue if you have additional information related to this issue.
<br><br> ![](https://feross.net/s/cats/7.gif)

14
.github/stale.yml vendored
View File

@@ -1,14 +0,0 @@
# ProBot Stale (https://probot.github.io/apps/stale/)
daysUntilStale: 90
daysUntilClose: 7
exemptLabels:
- accepted
- bug
- enhancement
- meta
staleLabel: stale
markComment: >
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs.
closeComment: false

View File

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

View File

@@ -37,15 +37,5 @@
- Alexey Romanov (romanalexey@gmail.com) - Alexey Romanov (romanalexey@gmail.com)
- Karan Thakkar (karanjthakkar@gmail.com) - Karan Thakkar (karanjthakkar@gmail.com)
- Nuno Campos (nuno.campos@me.com) - Nuno Campos (nuno.campos@me.com)
- Ebrahim Byagowi (ebrahim@gnu.org)
- Josip Janzic (josip@jjanzic.com)
- Emil Bay (github@tixz.dk)
- Borewit (borewit@users.noreply.github.com)
- greenkeeper[bot] (greenkeeper[bot]@users.noreply.github.com)
- Auyer (rafa_auyer@icloud.com)
- SimplyAhmazing (ahmad19526@gmail.com)
- Cezar Carneiro (cezargcarneiro@gmail.com)
- Terry Hau (terryhau@gmail.com)
- Vítor Galvão (info@vitorgalvao.com)
#### Generated by bin/update-authors.sh. #### Generated by bin/update-authors.sh.

View File

@@ -1,41 +1,6 @@
# WebTorrent Desktop Version History # WebTorrent Desktop Version History
## v0.20.0 - 2018-04-26 ## v0.18.0
### Added
- Added support for additional audio extensions: 'aiff', 'ape', 'mp2', 'oga', 'opus', 'wma' (#1240)
### Changed
- Displaying filename while music metadata is being downloaded (#1361)
- Improved the poster selection for audio/music based torrents (#1334)
- Launch VLC player without the `--video-on-top` flag (#1286)
### Fixed
- Fix silently failing to open magnets links on Linux (#1367)
## v0.19.0 - 2018-01-26
### Added
- Added watch folder feature: Automatically add new torrent files added to a folder on disk (#1154)
- Added highest playback priority feature: pauses other active torrents when playback starts (#840)
- Add 'Start Speaking' and 'Stop Speaking' menu item (Mac) (#439)
- Add pinch-to-zoom gesture to enter/exit fullscreen (#1148)
### Changed
- [SECURITY] Mitigate Electron protocol handler issue (Windows)
- Moved project from Feross's GitHub account to the WebTorrent GitHub organization
- Updated to electron@1.6.16
- Updated to material-ui@0.17
- Treat .FLAC as playable audio (#1127)
### Fixed
- Fix time and duration so it doesn't bounce in the UI (#1233)
- Fix 'About WebTorrent' menu location on Windows (#1120)
## v0.18.0 - 2017-02-03
### Added ### Added
- Add a new "Transfers" menu for pausing or resuming all torrents (#1027) - Add a new "Transfers" menu for pausing or resuming all torrents (#1027)
@@ -102,7 +67,7 @@
## v0.16.0 - 2016-09-18 ## v0.16.0 - 2016-09-18
### Added ### Added
- **Windows 64-bit support!** (#931) - **Windows 64-bit support!** ([#931](https://github.com/webtorrent/webtorrent-desktop/pull/931))
- Existing 32-bit users will update to 64-bit automatically in next release - 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 - 64-bit reduces likelihood of out-of-memory errors by increasing the address space

View File

@@ -13,33 +13,20 @@
<p align="center"> <p align="center">
<a href="https://gitter.im/webtorrent/webtorrent"><img src="https://img.shields.io/badge/gitter-join%20chat%20%E2%86%92-brightgreen.svg" alt="gitter"></a> <a href="https://gitter.im/webtorrent/webtorrent"><img src="https://img.shields.io/badge/gitter-join%20chat%20%E2%86%92-brightgreen.svg" alt="gitter"></a>
<a href="https://github.com/webtorrent/webtorrent-desktop/releases"><img src="https://img.shields.io/github/release/webtorrent/webtorrent-desktop.svg" alt="github release"></a>
<a href="https://travis-ci.org/webtorrent/webtorrent-desktop"><img src="https://img.shields.io/travis/webtorrent/webtorrent-desktop/master.svg" alt="travis"></a> <a href="https://travis-ci.org/webtorrent/webtorrent-desktop"><img src="https://img.shields.io/travis/webtorrent/webtorrent-desktop/master.svg" alt="travis"></a>
<a href="https://github.com/webtorrent/webtorrent-desktop/releases"><img src="https://img.shields.io/github/release/webtorrent/webtorrent-desktop.svg" alt="github release version"></a>
<a href="https://github.com/webtorrent/webtorrent-desktop/releases"><img src="https://img.shields.io/github/downloads/webtorrent/webtorrent-desktop/total.svg" alt="github release downloads"></a>
<a href="https://standardjs.com"><img src="https://img.shields.io/badge/code_style-standard-brightgreen.svg" alt="Standard - JavaScript Style Guide"></a> <a href="https://standardjs.com"><img src="https://img.shields.io/badge/code_style-standard-brightgreen.svg" alt="Standard - JavaScript Style Guide"></a>
</p> </p>
## Install ## Install
### Recommended Install
Download the latest version of WebTorrent Desktop from Download the latest version of WebTorrent Desktop from
[the official website](https://webtorrent.io/desktop/): [the official website](https://webtorrent.io/desktop/) or the
[GitHub releases](https://github.com/webtorrent/webtorrent-desktop/releases) page.
### [✨ Download WebTorrent Desktop ✨](https://webtorrent.io/desktop/) **WebTorrent Desktop** is under very active development. You can try out the
current (unstable) development version by cloning the Git repo. See the
### Advanced Install instructions below in the ["How to Contribute"](#how-to-contribute) section.
- Download specific installer files from the [GitHub releases](https://github.com/webtorrent/webtorrent-desktop/releases) page.
- Use [Homebrew-Cask](https://github.com/caskroom/homebrew-cask) to install from the command line:
```
$ brew cask install webtorrent
```
- Try the (unstable) development version by cloning the Git repository. See the
["How to Contribute"](#how-to-contribute) instructions.
## Screenshots ## Screenshots

View File

@@ -1,43 +0,0 @@
# Security Policies and Procedures
This document outlines security procedures and general policies for the WebTorrent
project.
* [Reporting a Bug](#reporting-a-bug)
* [Disclosure Policy](#disclosure-policy)
* [Comments on this Policy](#comments-on-this-policy)
## Reporting a Bug
The WebTorrent team and community take all security bugs in WebTorrent seriously.
Thank you for improving the security of WebTorrent. We appreciate your efforts and
responsible disclosure and will make every effort to acknowledge your
contributions.
Report security bugs by emailing the lead maintainer at feross@feross.org.
The lead maintainer will acknowledge your email within 48 hours, and will send a
more detailed response within 48 hours indicating the next steps in handling
your report. After the initial reply to your report, the security team will
endeavor to keep you informed of the progress towards a fix and full
announcement, and may ask for additional information or guidance.
Report security bugs in third-party modules to the person or team maintaining
the module. You can also report a vulnerability through the
[Node Security Project](https://nodesecurity.io/report).
## Disclosure Policy
When the security team receives a security bug report, they will assign it to a
primary handler. This person will coordinate the fix and release process,
involving the following steps:
* Confirm the problem and determine the affected versions.
* Audit code to find any potential similar problems.
* Prepare fixes for all releases still under maintenance. These fixes will be
released as fast as possible to npm.
## Comments on this Policy
If you have suggestions on how this process could be improved please submit a
pull request.

View File

@@ -7,7 +7,7 @@ const path = require('path')
let hasErrors = false let hasErrors = false
// Find all Javascript source files // Find all Javascript source files
const files = walkSync('src', { globs: ['**/*.js'] }) const files = walkSync('src', {globs: ['**/*.js']})
console.log('Running extra-lint on ' + files.length + ' files...') console.log('Running extra-lint on ' + files.length + ' files...')
// Read each file, line by line // Read each file, line by line

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env node #!/usr/bin/env node
const { CONFIG_PATH } = require('../src/config') const config = require('../src/config')
const opn = require('opn') const open = require('open')
opn(CONFIG_PATH) open(config.CONFIG_PATH)

12121
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{ {
"name": "webtorrent-desktop", "name": "webtorrent-desktop",
"description": "WebTorrent, the streaming torrent client. For Mac, Windows, and Linux.", "description": "WebTorrent, the streaming torrent client. For Mac, Windows, and Linux.",
"version": "0.20.0", "version": "0.18.0",
"author": { "author": {
"name": "WebTorrent, LLC", "name": "WebTorrent, LLC",
"email": "feross@webtorrent.io", "email": "feross@webtorrent.io",
@@ -16,25 +16,27 @@
"arch": "^2.0.0", "arch": "^2.0.0",
"auto-launch": "^4.0.1", "auto-launch": "^4.0.1",
"bitfield": "^1.0.2", "bitfield": "^1.0.2",
"capture-frame": "^2.0.0", "capture-frame": "^1.0.0",
"chokidar": "^2.0.4", "chromecasts": "^1.8.0",
"chromecasts": "^1.9.1", "cp-file": "^4.0.1",
"cp-file": "^6.0.0",
"create-torrent": "^3.24.5", "create-torrent": "^3.24.5",
"debounce": "^1.0.0", "debounce": "^1.0.0",
"deep-equal": "^1.0.1", "deep-equal": "^1.0.1",
"dlnacasts": "^0.1.0", "dlnacasts": "^0.1.0",
"drag-drop": "^4.1.0", "drag-drop": "^2.12.1",
"es6-error": "^4.0.0", "es6-error": "^4.0.0",
"fn-getter": "^1.0.0", "fn-getter": "^1.0.0",
"gaze": "^1.1.2",
"iso-639-1": "^1.2.1", "iso-639-1": "^1.2.1",
"languagedetect": "^1.1.1", "languagedetect": "^1.1.1",
"location-history": "^1.0.0", "location-history": "^1.0.0",
"material-ui": "^0.17.0", "material-ui": "^0.17.0",
"mkdirp": "^0.5.1", "mkdirp": "^0.5.1",
"music-metadata": "^3.1.0", "ms": "^0.7.2",
"musicmetadata": "^2.0.2",
"network-address": "^1.1.0", "network-address": "^1.1.0",
"parse-torrent": "^6.0.1", "node-notifier": "^5.0.2",
"parse-torrent": "^5.7.3",
"prettier-bytes": "^1.0.1", "prettier-bytes": "^1.0.1",
"react": "^15.4.2", "react": "^15.4.2",
"react-dom": "^15.4.2", "react-dom": "^15.4.2",
@@ -42,8 +44,9 @@
"rimraf": "^2.5.2", "rimraf": "^2.5.2",
"run-parallel": "^1.1.6", "run-parallel": "^1.1.6",
"semver": "^5.1.0", "semver": "^5.1.0",
"shell-env": "^0.3.0",
"simple-concat": "^1.0.0", "simple-concat": "^1.0.0",
"simple-get": "^3.0.3", "simple-get": "^2.0.0",
"srt-to-vtt": "^1.1.1", "srt-to-vtt": "^1.1.1",
"vlc-command": "^1.0.1", "vlc-command": "^1.0.1",
"webtorrent": "0.x", "webtorrent": "0.x",
@@ -51,20 +54,19 @@
"zero-fill": "^2.2.3" "zero-fill": "^2.2.3"
}, },
"devDependencies": { "devDependencies": {
"babel-eslint": "^9.0.0", "buble": "^0.15.2",
"buble": "^0.19.3",
"cross-zip": "^2.0.1", "cross-zip": "^2.0.1",
"depcheck": "^0.6.4", "depcheck": "^0.6.4",
"electron": "1.6.16", "electron": "1.6.0",
"electron-osx-sign": "^0.4.10", "electron-osx-sign": "0.4.3",
"electron-packager": "~8.5.1", "electron-packager": "~8.5.1",
"electron-winstaller": "^2.6.4", "electron-winstaller": "~2.5.2",
"gh-release": "^3.2.1", "gh-release": "^2.0.3",
"minimist": "^1.2.0", "minimist": "^1.2.0",
"nobin-debian-installer": "0.0.10", "nobin-debian-installer": "0.0.10",
"nodemon": "^1.10.2", "nodemon": "^1.10.2",
"opn": "^5.4.0", "open": "0.0.5",
"plist": "^3.0.1", "plist": "^2.0.1",
"pngjs": "^3.0.0", "pngjs": "^3.0.0",
"run-series": "^1.1.4", "run-series": "^1.1.4",
"spectron": "^3.3.0", "spectron": "^3.3.0",
@@ -105,12 +107,9 @@
"package": "node ./bin/package.js", "package": "node ./bin/package.js",
"prepublish": "npm run build", "prepublish": "npm run build",
"start": "npm run build && electron .", "start": "npm run build && electron .",
"test": "standard && depcheck --ignores=standard,babel-eslint --ignore-dirs=build,dist && node ./bin/extra-lint.js", "test": "standard && depcheck --ignores=buble,nodemon,gh-release --ignore-dirs=build,dist && node ./bin/extra-lint.js",
"test-integration": "npm run build && node ./test", "test-integration": "npm run build && node ./test",
"update-authors": "./bin/update-authors.sh", "update-authors": "./bin/update-authors.sh",
"watch": "nodemon --exec \"npm run start\" --ext js,css --ignore build/ --ignore dist/" "watch": "nodemon --exec \"npm run start\" --ext js,css --ignore build/ --ignore dist/"
},
"standard": {
"parser": "babel-eslint"
} }
} }

View File

@@ -1,7 +1,9 @@
const appConfig = require('application-config')('WebTorrent') const appConfig = require('application-config')('WebTorrent')
const fs = require('fs')
const path = require('path') const path = require('path')
const electron = require('electron') const electron = require('electron')
const arch = require('arch') const arch = require('arch')
const gaze = require('gaze')
const APP_NAME = 'WebTorrent' const APP_NAME = 'WebTorrent'
const APP_TEAM = 'WebTorrent, LLC' const APP_TEAM = 'WebTorrent, LLC'
@@ -17,13 +19,13 @@ const IS_PORTABLE = isPortable()
const UI_HEADER_HEIGHT = 38 const UI_HEADER_HEIGHT = 38
const UI_TORRENT_HEIGHT = 100 const UI_TORRENT_HEIGHT = 100
module.exports = { const exports = module.exports = {
ANNOUNCEMENT_URL: 'https://webtorrent.io/desktop/announcement', ANNOUNCEMENT_URL: 'https://webtorrent.io/desktop/announcement',
AUTO_UPDATE_URL: 'https://webtorrent.io/desktop/update', AUTO_UPDATE_URL: 'https://webtorrent.io/desktop/update',
CRASH_REPORT_URL: 'https://webtorrent.io/desktop/crash-report', CRASH_REPORT_URL: 'https://webtorrent.io/desktop/crash-report',
TELEMETRY_URL: 'https://webtorrent.io/desktop/telemetry', TELEMETRY_URL: 'https://webtorrent.io/desktop/telemetry',
APP_COPYRIGHT: 'Copyright © 2014-2018 ' + APP_TEAM, APP_COPYRIGHT: 'Copyright © 2014-2017 ' + APP_TEAM,
APP_FILE_ICON: path.join(__dirname, '..', 'static', 'WebTorrentFile'), APP_FILE_ICON: path.join(__dirname, '..', 'static', 'WebTorrentFile'),
APP_ICON: path.join(__dirname, '..', 'static', 'WebTorrent'), APP_ICON: path.join(__dirname, '..', 'static', 'WebTorrent'),
APP_NAME: APP_NAME, APP_NAME: APP_NAME,
@@ -102,6 +104,62 @@ module.exports = {
UI_TORRENT_HEIGHT: UI_TORRENT_HEIGHT UI_TORRENT_HEIGHT: UI_TORRENT_HEIGHT
} }
const configFile = appConfig.filePath
let config = getConfig()
const watchers = []
function updateConfig () {
config = JSON.parse(fs.readFileSync(configFile))
}
function watch () {
gaze(configFile, function (err) {
if (err) {
throw err
}
this.on('changed', () => {
try {
updateConfig()
console.log('WebTorrent configuration reloaded!')
watchers.forEach(fn => fn())
} catch (err) {
// TODO: display notification
console.log(`An error occurred loading your configuration (${configFile}): ${err.message}`)
}
})
this.on('error', () => {
// Ignore file watching errors
})
})
}
// start watching for config changes
watch()
exports.subscribe = function (fn) {
watchers.push(fn)
return () => {
watchers.splice(watchers.indexOf(fn), 1)
}
}
function getPlugins () {
return config.plugins || {}
}
exports.getPlugins = getPlugins
exports.getConfigPath = getConfigPath
exports.getConfig = getConfig
function getConfig () {
const config = {}
try {
return require(configFile)
} catch (e) {
return config
}
}
function getConfigPath () { function getConfigPath () {
if (IS_PORTABLE) { if (IS_PORTABLE) {
return PORTABLE_PATH return PORTABLE_PATH
@@ -145,8 +203,6 @@ function isPortable () {
return false return false
} }
const fs = require('fs')
try { try {
// This line throws if the "Portable Settings" folder does not exist, and does // This line throws if the "Portable Settings" folder does not exist, and does
// nothing otherwise. // nothing otherwise.

View File

@@ -7,8 +7,9 @@ const electron = require('electron')
const config = require('../config') const config = require('../config')
const log = require('./log') const log = require('./log')
const ANNOUNCEMENT_URL = const ANNOUNCEMENT_URL = config.ANNOUNCEMENT_URL +
`${config.ANNOUNCEMENT_URL}?version=${config.APP_VERSION}&platform=${process.platform}` '?version=' + config.APP_VERSION +
'&platform=' + process.platform
/** /**
* In certain situations, the WebTorrent team may need to show an announcement to * In certain situations, the WebTorrent team may need to show an announcement to

View File

@@ -4,7 +4,7 @@ module.exports = {
setBadge setBadge
} }
const { app, Menu } = require('electron') const {app, Menu} = require('electron')
const dialog = require('./dialog') const dialog = require('./dialog')
const log = require('./log') const log = require('./log')

View File

@@ -25,10 +25,11 @@ function spawn (playerPath, url, title) {
if (playerPath != null) return spawnExternal(playerPath, [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((err, vlcPath) => { vlcCommand(function (err, vlcPath) {
if (err) return windows.main.dispatch('externalPlayerNotFound') if (err) return windows.main.dispatch('externalPlayerNotFound')
const args = [ const args = [
'--play-and-exit', '--play-and-exit',
'--video-on-top',
'--quiet', '--quiet',
`--meta-title=${JSON.stringify(title)}`, `--meta-title=${JSON.stringify(title)}`,
url url
@@ -39,26 +40,26 @@ function spawn (playerPath, url, title) {
function kill () { function kill () {
if (!proc) return if (!proc) return
log(`Killing external player, pid ${proc.pid}`) log('Killing external player, pid ' + proc.pid)
proc.kill('SIGKILL') // kill -9 proc.kill('SIGKILL') // kill -9
proc = null proc = null
} }
function spawnExternal (playerPath, args) { function spawnExternal (playerPath, args) {
log('Running external media player:', `${playerPath} ${args.join(' ')}`) log('Running external media player:', playerPath + ' ' + args.join(' '))
if (process.platform === 'darwin' && path.extname(playerPath) === '.app') { if (process.platform === 'darwin' && path.extname(playerPath) === '.app') {
// Mac: Use executable in packaged .app bundle // Mac: Use executable in packaged .app bundle
playerPath += `/Contents/MacOS/${path.basename(playerPath, '.app')}` playerPath += '/Contents/MacOS/' + path.basename(playerPath, '.app')
} }
proc = cp.spawn(playerPath, args, { stdio: 'ignore' }) 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(() =>
windows.main.dispatch('exitModal'), 1000) windows.main.dispatch('exitModal'), 1000)
proc.on('close', code => { proc.on('close', function (code) {
clearTimeout(closeModalTimeout) clearTimeout(closeModalTimeout)
if (!proc) return // Killed if (!proc) return // Killed
log('External player exited with code ', code) log('External player exited with code ', code)
@@ -70,7 +71,7 @@ function spawnExternal (playerPath, args) {
proc = null proc = null
}) })
proc.on('error', err => { proc.on('error', function (e) {
log('External player error', err) log('External player error', e)
}) })
} }

View File

@@ -1,50 +0,0 @@
const chokidar = require('chokidar')
const log = require('./log')
class FolderWatcher {
constructor ({ window, state }) {
this.window = window
this.state = state
this.torrentsFolderPath = null
this.watching = false
}
isEnabled () {
return this.state.saved.prefs.autoAddTorrents
}
start () {
// Stop watching previous folder before
// start watching a new one.
if (this.watching) this.stop()
const torrentsFolderPath = this.state.saved.prefs.torrentsFolderPath
this.torrentsFolderPath = torrentsFolderPath
if (!torrentsFolderPath) return
const glob = `${torrentsFolderPath}/**/*.torrent`
log('Folder Watcher: watching: ', glob)
const options = {
ignoreInitial: true,
awaitWriteFinish: true
}
this.watcher = chokidar.watch(glob, options)
this.watcher
.on('add', (path) => {
log('Folder Watcher: added torrent: ', path)
this.window.dispatch('addTorrent', path)
})
this.watching = true
}
stop () {
log('Folder Watcher: stop.')
if (!this.watching) return
this.watcher.close()
this.watching = false
}
}
module.exports = FolderWatcher

View File

@@ -7,24 +7,26 @@ const config = require('../config')
const path = require('path') const path = require('path')
function install () { function install () {
switch (process.platform) { if (process.platform === 'darwin') {
case 'darwin': installDarwin() installDarwin()
break }
case 'win32': installWin32() if (process.platform === 'win32') {
break installWin32()
case 'linux': installLinux() }
break if (process.platform === 'linux') {
installLinux()
} }
} }
function uninstall () { function uninstall () {
switch (process.platform) { if (process.platform === 'darwin') {
case 'darwin': uninstallDarwin() uninstallDarwin()
break }
case 'win32': uninstallWin32() if (process.platform === 'win32') {
break uninstallWin32()
case 'linux': uninstallLinux() }
break if (process.platform === 'linux') {
uninstallLinux()
} }
} }
@@ -42,7 +44,7 @@ function installDarwin () {
function uninstallDarwin () {} function uninstallDarwin () {}
const EXEC_COMMAND = [ process.execPath, '--' ] const EXEC_COMMAND = [ process.execPath ]
if (!config.IS_PRODUCTION) { if (!config.IS_PRODUCTION) {
EXEC_COMMAND.push(config.ROOT_PATH) EXEC_COMMAND.push(config.ROOT_PATH)
@@ -106,37 +108,37 @@ function installWin32 () {
setProtocol() setProtocol()
function setProtocol (err) { function setProtocol (err) {
if (err) return log.error(err.message) if (err) log.error(err.message)
protocolKey.set('', Registry.REG_SZ, name, setURLProtocol) protocolKey.set('', Registry.REG_SZ, name, setURLProtocol)
} }
function setURLProtocol (err) { function setURLProtocol (err) {
if (err) return log.error(err.message) if (err) log.error(err.message)
protocolKey.set('URL Protocol', Registry.REG_SZ, '', setIcon) protocolKey.set('URL Protocol', Registry.REG_SZ, '', setIcon)
} }
function setIcon (err) { function setIcon (err) {
if (err) return log.error(err.message) if (err) log.error(err.message)
const iconKey = new Registry({ const iconKey = new Registry({
hive: Registry.HKCU, hive: Registry.HKCU,
key: `\\Software\\Classes\\${protocol}\\DefaultIcon` key: '\\Software\\Classes\\' + protocol + '\\DefaultIcon'
}) })
iconKey.set('', Registry.REG_SZ, icon, setCommand) iconKey.set('', Registry.REG_SZ, icon, setCommand)
} }
function setCommand (err) { function setCommand (err) {
if (err) return log.error(err.message) if (err) log.error(err.message)
const commandKey = new Registry({ const commandKey = new Registry({
hive: Registry.HKCU, hive: Registry.HKCU,
key: `\\Software\\Classes\\${protocol}\\shell\\open\\command` key: '\\Software\\Classes\\' + protocol + '\\shell\\open\\command'
}) })
commandKey.set('', Registry.REG_SZ, `${commandToArgs(command)} "%1"`, done) commandKey.set('', Registry.REG_SZ, `${commandToArgs(command)} "%1"`, done)
} }
function done (err) { function done (err) {
if (err) return log.error(err.message) if (err) log.error(err.message)
} }
} }
@@ -161,43 +163,43 @@ function installWin32 () {
function setExt () { function setExt () {
const extKey = new Registry({ const extKey = new Registry({
hive: Registry.HKCU, // HKEY_CURRENT_USER hive: Registry.HKCU, // HKEY_CURRENT_USER
key: `\\Software\\Classes\\${ext}` key: '\\Software\\Classes\\' + ext
}) })
extKey.set('', Registry.REG_SZ, id, setId) extKey.set('', Registry.REG_SZ, id, setId)
} }
function setId (err) { function setId (err) {
if (err) return log.error(err.message) if (err) log.error(err.message)
const idKey = new Registry({ const idKey = new Registry({
hive: Registry.HKCU, hive: Registry.HKCU,
key: `\\Software\\Classes\\${id}` key: '\\Software\\Classes\\' + id
}) })
idKey.set('', Registry.REG_SZ, name, setIcon) idKey.set('', Registry.REG_SZ, name, setIcon)
} }
function setIcon (err) { function setIcon (err) {
if (err) return log.error(err.message) if (err) log.error(err.message)
const iconKey = new Registry({ const iconKey = new Registry({
hive: Registry.HKCU, hive: Registry.HKCU,
key: `\\Software\\Classes\\${id}\\DefaultIcon` key: '\\Software\\Classes\\' + id + '\\DefaultIcon'
}) })
iconKey.set('', Registry.REG_SZ, icon, setCommand) iconKey.set('', Registry.REG_SZ, icon, setCommand)
} }
function setCommand (err) { function setCommand (err) {
if (err) return log.error(err.message) if (err) log.error(err.message)
const commandKey = new Registry({ const commandKey = new Registry({
hive: Registry.HKCU, hive: Registry.HKCU,
key: `\\Software\\Classes\\${id}\\shell\\open\\command` key: '\\Software\\Classes\\' + id + '\\shell\\open\\command'
}) })
commandKey.set('', Registry.REG_SZ, `${commandToArgs(command)} "%1"`, done) commandKey.set('', Registry.REG_SZ, `${commandToArgs(command)} "%1"`, done)
} }
function done (err) { function done (err) {
if (err) return log.error(err.message) if (err) log.error(err.message)
} }
} }
} }
@@ -215,9 +217,9 @@ function uninstallWin32 () {
function getCommand () { function getCommand () {
const commandKey = new Registry({ const commandKey = new Registry({
hive: Registry.HKCU, // HKEY_CURRENT_USER hive: Registry.HKCU, // HKEY_CURRENT_USER
key: `\\Software\\Classes\\${protocol}\\shell\\open\\command` key: '\\Software\\Classes\\' + protocol + '\\shell\\open\\command'
}) })
commandKey.get('', (err, item) => { commandKey.get('', function (err, item) {
if (!err && item.value.indexOf(commandToArgs(command)) >= 0) { if (!err && item.value.indexOf(commandToArgs(command)) >= 0) {
destroyProtocol() destroyProtocol()
} }
@@ -227,9 +229,9 @@ function uninstallWin32 () {
function destroyProtocol () { function destroyProtocol () {
const protocolKey = new Registry({ const protocolKey = new Registry({
hive: Registry.HKCU, hive: Registry.HKCU,
key: `\\Software\\Classes\\${protocol}` key: '\\Software\\Classes\\' + protocol
}) })
protocolKey.destroy(() => {}) protocolKey.destroy(function () {})
} }
} }
@@ -239,7 +241,7 @@ function uninstallWin32 () {
function eraseId () { function eraseId () {
const idKey = new Registry({ const idKey = new Registry({
hive: Registry.HKCU, // HKEY_CURRENT_USER hive: Registry.HKCU, // HKEY_CURRENT_USER
key: `\\Software\\Classes\\${id}` key: '\\Software\\Classes\\' + id
}) })
idKey.destroy(getExt) idKey.destroy(getExt)
} }
@@ -247,9 +249,9 @@ function uninstallWin32 () {
function getExt () { function getExt () {
const extKey = new Registry({ const extKey = new Registry({
hive: Registry.HKCU, hive: Registry.HKCU,
key: `\\Software\\Classes\\${ext}` key: '\\Software\\Classes\\' + ext
}) })
extKey.get('', (err, item) => { extKey.get('', function (err, item) {
if (!err && item.value === id) { if (!err && item.value === id) {
destroyExt() destroyExt()
} }
@@ -259,9 +261,9 @@ function uninstallWin32 () {
function destroyExt () { function destroyExt () {
const extKey = new Registry({ const extKey = new Registry({
hive: Registry.HKCU, // HKEY_CURRENT_USER hive: Registry.HKCU, // HKEY_CURRENT_USER
key: `\\Software\\Classes\\${ext}` key: '\\Software\\Classes\\' + ext
}) })
extKey.destroy(() => {}) extKey.destroy(function () {})
} }
} }
} }
@@ -298,11 +300,10 @@ function installLinux () {
? path.dirname(process.execPath) ? path.dirname(process.execPath)
: config.ROOT_PATH : config.ROOT_PATH
desktopFile = desktopFile desktopFile = desktopFile.replace(/\$APP_NAME/g, config.APP_NAME)
.replace(/\$APP_NAME/g, config.APP_NAME) desktopFile = desktopFile.replace(/\$APP_PATH/g, appPath)
.replace(/\$APP_PATH/g, appPath) desktopFile = desktopFile.replace(/\$EXEC_PATH/g, EXEC_COMMAND.join(' '))
.replace(/\$EXEC_PATH/g, EXEC_COMMAND.join(' ')) desktopFile = desktopFile.replace(/\$TRY_EXEC_PATH/g, process.execPath)
.replace(/\$TRY_EXEC_PATH/g, process.execPath)
const desktopFilePath = path.join( const desktopFilePath = path.join(
os.homedir(), os.homedir(),
@@ -312,7 +313,7 @@ function installLinux () {
'webtorrent-desktop.desktop' 'webtorrent-desktop.desktop'
) )
fs.mkdirp(path.dirname(desktopFilePath)) fs.mkdirp(path.dirname(desktopFilePath))
fs.writeFile(desktopFilePath, desktopFile, err => { fs.writeFile(desktopFilePath, desktopFile, function (err) {
if (err) return log.error(err.message) if (err) return log.error(err.message)
}) })
} }
@@ -334,9 +335,9 @@ function installLinux () {
'icons', 'icons',
'webtorrent-desktop.png' 'webtorrent-desktop.png'
) )
mkdirp(path.dirname(iconFilePath), err => { mkdirp(path.dirname(iconFilePath), (err) => {
if (err) return log.error(err.message) if (err) return log.error(err.message)
fs.writeFile(iconFilePath, iconFile, err => { fs.writeFile(iconFilePath, iconFile, (err) => {
if (err) log.error(err.message) if (err) log.error(err.message)
}) })
}) })

View File

@@ -12,6 +12,8 @@ const log = require('./log')
const menu = require('./menu') const menu = require('./menu')
const State = require('../renderer/lib/state') const State = require('../renderer/lib/state')
const windows = require('./windows') const windows = require('./windows')
const Plugins = require('./plugins')
const plugins = new Plugins()
let shouldQuit = false let shouldQuit = false
let argv = sliceArgv(process.argv) let argv = sliceArgv(process.argv)
@@ -74,23 +76,47 @@ function init () {
isReady = true isReady = true
const state = results.state const state = results.state
windows.main.init(state, { hidden: hidden }) // init new plugins then notify user
windows.webtorrent.init() plugins.subscribe(() => {
menu.init() // update menu and windows
// passing thru new plugin decorators
initApp(state)
})
plugins.init(state)
initApp(state)
// To keep app startup fast, some code is delayed. // To keep app startup fast, some code is delayed.
setTimeout(() => { setTimeout(delayedInit, config.DELAYED_INIT)
delayedInit(state)
}, config.DELAYED_INIT)
// Report uncaught exceptions // Report uncaught exceptions
process.on('uncaughtException', (err) => { process.on('uncaughtException', (err) => {
console.error(err) console.error(err)
const error = { message: err.message, stack: err.stack } const error = {message: err.message, stack: err.stack}
windows.main.dispatch('uncaughtError', 'main', error) windows.main.dispatch('uncaughtError', 'main', error)
}) })
} }
function initApp (state) {
// decorate app
plugins.onApp(app)
// init decorate menu
menu.init((tpl) => plugins.decorateMenu(tpl))
// init and decorate window
windows.main.init(
state,
{hidden: hidden},
(options) => plugins.decorateWindow(options)
)
windows.webtorrent.init((options) => plugins.decorateWindow(options))
plugins.onWindow([
windows.main.win,
windows.webtorrent.win
])
}
app.on('open-file', onOpen) app.on('open-file', onOpen)
app.on('open-url', onOpen) app.on('open-url', onOpen)
@@ -124,24 +150,17 @@ function init () {
}) })
} }
function delayedInit (state) { function delayedInit () {
if (app.isQuitting) return if (app.isQuitting) return
const announcement = require('./announcement') const announcement = require('./announcement')
const dock = require('./dock') const dock = require('./dock')
const updater = require('./updater') const updater = require('./updater')
const FolderWatcher = require('./folder-watcher')
const folderWatcher = new FolderWatcher({ window: windows.main, state })
announcement.init() announcement.init()
dock.init() dock.init()
updater.init() updater.init()
ipc.setModule('folderWatcher', folderWatcher)
if (folderWatcher.isEnabled()) {
folderWatcher.start()
}
if (process.platform === 'win32') { if (process.platform === 'win32') {
const userTasks = require('./user-tasks') const userTasks = require('./user-tasks')
userTasks.init() userTasks.init()

View File

@@ -1,6 +1,5 @@
module.exports = { module.exports = {
init, init
setModule
} }
const electron = require('electron') const electron = require('electron')
@@ -14,14 +13,6 @@ const windows = require('./windows')
// 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 = []
// Will hold modules injected from the app that will be used on fired
// IPC events.
const modules = {}
function setModule (name, module) {
modules[name] = module
}
function init () { function init () {
const ipc = electron.ipcMain const ipc = electron.ipcMain
@@ -67,7 +58,7 @@ function init () {
}) })
/** /**
* Player Events * Events
*/ */
ipc.on('onPlayerOpen', function () { ipc.on('onPlayerOpen', function () {
@@ -115,28 +106,6 @@ function init () {
thumbar.onPlayerPause() thumbar.onPlayerPause()
}) })
/**
* Folder Watcher Events
*/
ipc.on('startFolderWatcher', function () {
if (!modules['folderWatcher']) {
log('IPC ERR: folderWatcher module is not defined.')
return
}
modules['folderWatcher'].start()
})
ipc.on('stopFolderWatcher', function () {
if (!modules['folderWatcher']) {
log('IPC ERR: folderWatcher module is not defined.')
return
}
modules['folderWatcher'].stop()
})
/** /**
* Shell * Shell
*/ */

View File

@@ -17,8 +17,11 @@ const windows = require('./windows')
let menu = null let menu = null
function init () { function init (decorate) {
menu = electron.Menu.buildFromTemplate(getMenuTemplate()) let template = getMenuTemplate()
if (decorate) template = decorate(template)
menu = electron.Menu.buildFromTemplate(template)
electron.Menu.setApplicationMenu(menu) electron.Menu.setApplicationMenu(menu)
} }

424
src/main/plugins.js Normal file
View File

@@ -0,0 +1,424 @@
const {exec} = require('child_process')
const {resolve, basename} = require('path')
const {writeFileSync} = require('fs')
const State = require('../renderer/lib/state')
const notifier = require('node-notifier')
const {app} = require('electron')
const {sync: mkdirpSync} = require('mkdirp')
const ms = require('ms')
const shellEnv = require('shell-env')
const crypto = require('crypto')
const config = require('../config')
module.exports = class Plugins {
constructor () {
// modules path
this.path = resolve(config.getConfigPath(), 'plugins')
log('Path: ', this.path)
this.availableExtensions = new Set([
'onApp', 'onWindow', 'decorateMenu', 'decorateWindow', 'decorateConfig'
])
this.forceUpdate = false
this.updating = false
this.watchers = []
}
init (state) {
this.state = state
// initialize state
this.state.saved = Object.assign(this.state.saved || {})
// init plugin directories if not present
mkdirpSync(this.path)
// caches
this.plugins = config.getPlugins()
this.paths = this.getPaths(this.plugins)
this.id = this.getId(this.plugins)
this.modules = this.requirePlugins()
// we listen on configuration updates to trigger
// plugin installation
config.subscribe(() => {
const plugins = config.getPlugins()
if (plugins !== this.plugins) {
const id = this.getId(plugins)
if (this.id !== id) {
this.alert('Installing plugins...')
log('UPDATING...')
this.id = id
this.plugins = plugins
this.paths = this.getPaths(this.plugins)
this.updatePlugins()
}
}
})
// schedule the initial plugins update
// a bit after the user launches the app
// to prevent slowness
if (this.needsUpdate()) {
setTimeout(() => {
this.updatePlugins()
}, 5000)
log('Installation scheduled')
}
// update plugins every 5 hours
setInterval(() => {
this.updatePlugins()
}, ms('5h'))
}
on (action, params) {
log(`ON ${action}:`, params)
this.modules.forEach(plugin => {
const actionName = this.capitalizeFirstLetter(action)
const methodName = `on${actionName}`
if (typeof plugin[methodName] === 'function') {
plugin[methodName](params)
}
})
}
capitalizeFirstLetter (string) {
return string.charAt(0).toUpperCase() + string.slice(1)
}
didPluginsChange () {
return this.state.saved.installedPlugins !== this.id
}
hasPlugins () {
return !this.isEmptyObject(this.plugins)
}
isFirstInstall () {
return (!this.state.saved.installedPlugins && this.hasPlugins())
}
needsUpdate () {
return (this.didPluginsChange() || this.isFirstInstall())
}
getId (plugins) {
const hash = crypto.createHash('sha256')
hash.update(JSON.stringify(plugins))
return hash.digest('hex')
}
updatePlugins (forceUpdate = false) {
this.forceUpdate = forceUpdate
if (this.updating) {
// TODO
// return notify('Plugin update in progress')
}
this.updating = true
this.syncPackageJSON()
this.installPackages((err) => this.loadPlugins(err))
}
loadPlugins (err, localOnly = false) {
this.updating = false
// handle errors first
if (err) {
console.error(err.stack)
if (/not a recognized/.test(err.message) || /command not found/.test(err.message)) {
this.alert(
'Error updating plugins: We could not find the "npm" command. Make sure it\'s in $PATH'
)
return
}
this.alert(`Error updating plugins: Check '${this.path}/npm-debug.log' for more information.`)
return
}
// update state with latest plugins
this.state.saved.plugins = this.plugins
// cache modules
this.modules = this.requirePlugins()
// clear require cache
this.clearCache()
// we're done with local plugins
if (localOnly) return
// OK, no errors
// flag successful plugin update
this.state.saved.installedPlugins = this.id
// check if package based plugins were updated
const loaded = this.modules.length
const total = this.paths.plugins.length
const pluginVersions = JSON.stringify(this.getPluginVersions())
const changed = this.state.saved.installedPluginVersions !== pluginVersions && loaded === total
this.state.saved.installedPluginVersions = pluginVersions
// notify watchers
if (this.forceUpdate || changed) {
this.watchers.forEach(fn => fn(err, {forceUpdate: this.forceUpdate}))
this.alert('Installation completed')
log('Installation completed')
}
// save state
State.save(this.state)
}
getPluginVersions () {
const paths_ = this.paths.plugins
return paths_.map(path => {
let version = null
try {
// eslint-disable-next-line import/no-dynamic-require
version = require(resolve(path, 'package.json')).version
} catch (err) { }
return [
basename(path),
version
]
})
}
clearCache () {
// trigger unload hooks
this.modules.forEach(mod => {
if (mod.onUnload) {
mod.onUnload(app)
}
})
// clear require cache
for (const entry in require.cache) {
if (entry.indexOf(this.path) === 0) {
delete require.cache[entry]
}
}
}
isEmptyObject (obj) {
return (Object.keys(obj).length === 0)
}
syncPackageJSON () {
const dependencies = this.toDependencies(this.plugins)
const pkg = {
name: 'webtorrent-plugins',
description: 'Auto-generated from WebTorrent config.',
private: true,
version: '0.0.1',
repository: 'feross/webtorrent-desktop',
license: 'MIT',
homepage: 'https://webtorrent.io',
dependencies
}
const file = resolve(this.path, 'package.json')
try {
writeFileSync(file, JSON.stringify(pkg, null, 2))
return true
} catch (err) {
this.alert(`An error occurred writing to ${file}`)
}
}
alert (message) {
notifier.notify({
title: 'WebTorrent Plugins',
// icon: config.icon, // TODO: save icon in webtorrent local folder and set config.icon
message: message
})
}
isLocalPath (string) {
// matches unix and windows local paths
return string.match(/^(\/|[a-z]:\/)/i)
}
toDependencies (plugins) {
const obj = {}
const pluginNames = Object.keys(plugins)
pluginNames.forEach(name => {
let url = plugins[name]
if (this.isLocalPath(url)) return
obj[name] = url
})
return obj
}
installPackages (fn) {
const {shell = '', npmRegistry} = config
shellEnv(shell).then(env => {
if (npmRegistry) {
env.NPM_CONFIG_REGISTRY = npmRegistry
}
/* eslint-disable camelcase */
env.npm_config_runtime = 'electron'
env.npm_config_target = process.versions.electron
env.npm_config_disturl = 'https://atom.io/download/atom-shell'
/* eslint-enable camelcase */
// Shell-specific installation commands
const installCommands = {
fish: 'npm prune; and npm install --production',
posix: 'npm prune && npm install --production'
}
// determine the shell we're running in
const whichShell = shell.match(/fish/) ? 'fish' : 'posix'
// Use the install command that is appropriate for our shell
exec(installCommands[whichShell], {
cwd: this.path
}, err => {
if (err) return fn(err)
fn(null)
})
}).catch(fn)
}
subscribe (fn) {
this.watchers.push(fn)
return () => {
this.watchers.splice(this.watchers.indexOf(fn), 1)
}
}
getPaths (plugins) {
const pluginNames = Object.keys(plugins)
return {
plugins: pluginNames.map(name => {
let url = plugins[name]
// plugin is already on a local folder
// directly load it from its current location
if (this.isLocalPath(url)) return url
// plugin will be installed with npm install from a remote url
return resolve(this.path, 'node_modules', name.split('#')[0])
})
}
}
requirePlugins () {
const {plugins} = this.paths
let installNeeded = false
const load = (path) => {
if (!path.match(/\/$/)) {
path += '/'
}
const mainPath = `${path}main.js`
try {
const Plugin = require(mainPath) // eslint-disable import/no-dynamic-require
const plugin = new Plugin()
const exposed = plugin && Object.keys(plugin).some(key => this.availableExtensions.has(key))
if (!exposed) return
// populate the name for internal errors here
plugin._name = basename(mainPath)
return plugin
} catch (err) {
log('Require plugins ERROR:', err)
this.alert(`Error loading plugin: ${mainPath}`)
// plugin not installed
// node_modules removed? did a manual plugin uninstall?
// try installing and then loading if successfull
installNeeded = true
}
}
// Plugin installation happens on the MAIN process.
// If plugins haven't finished installing, wait for them.
if (installNeeded) {
log('Plugins install needed, wait...')
setTimeout(() => {
this.requirePlugins()
}, 3000)
}
return plugins.map(load).filter(v => Boolean(v))
}
onApp (app) {
this.modules.forEach(plugin => {
if (plugin.onApp) {
plugin.onApp(app)
}
})
}
onWindow (win) {
this.modules.forEach(plugin => {
if (plugin.onWindow) {
plugin.onWindow(win)
}
})
}
// decorates the base object by calling plugin[key]
// for all the available plugins
decorateObject (base, key) {
let decorated = base
this.modules.forEach(plugin => {
if (plugin[key]) {
const res = plugin[key](decorated)
if (res && typeof res === 'object') {
decorated = res
} else {
this.alert(`Plugin error: "${plugin._name}": invalid return type for \`${key}\``)
}
}
})
return decorated
}
decorateMenu (tpl) {
return this.decorateObject(tpl, 'decorateMenu')
}
decorateWindow (options) {
return this.decorateObject(options, 'decorateWindow')
}
getDecoratedEnv (baseEnv) {
return this.decorateObject(baseEnv, 'decorateEnv')
}
getDecoratedConfig () {
const baseConfig = config.getConfig()
return this.decorateObject(baseConfig, 'decorateConfig')
}
getDecoratedBrowserOptions (defaults) {
return this.decorateObject(defaults, 'decorateBrowserOptions')
}
}
/**
* Logs passed arguments to console using a prefix.
*
*/
function log () {
const prefix = '[ PLUGINS.Main ]-->'
const args = [prefix]
for (var i = 0; i < arguments.length; ++i) {
args.push(arguments[i])
}
console.log.apply(console, args)
}

View File

@@ -23,14 +23,14 @@ const config = require('../../config')
const log = require('../log') const log = require('../log')
const menu = require('../menu') const menu = require('../menu')
function init (state, options) { function init (state, options, decorate) {
if (main.win) { if (main.win) {
return main.win.show() return main.win.show()
} }
const initialBounds = Object.assign(config.WINDOW_INITIAL_BOUNDS, state.saved.bounds) const initialBounds = Object.assign(config.WINDOW_INITIAL_BOUNDS, state.saved.bounds)
const win = main.win = new electron.BrowserWindow({ let windowOptions = {
backgroundColor: '#282828', backgroundColor: '#282828',
backgroundThrottling: false, // do not throttle animations/timers when page is background backgroundThrottling: false, // do not throttle animations/timers when page is background
darkTheme: true, // Forces dark theme (GTK+3) darkTheme: true, // Forces dark theme (GTK+3)
@@ -45,7 +45,10 @@ function init (state, options) {
width: initialBounds.width, width: initialBounds.width,
x: initialBounds.x, x: initialBounds.x,
y: initialBounds.y y: initialBounds.y
}) }
if (decorate) windowOptions = decorate(windowOptions)
const win = main.win = new electron.BrowserWindow(windowOptions)
win.loadURL(config.WINDOW_MAIN) win.loadURL(config.WINDOW_MAIN)
@@ -57,7 +60,7 @@ function init (state, options) {
win.setSheetOffset(config.UI_HEADER_HEIGHT) win.setSheetOffset(config.UI_HEADER_HEIGHT)
} }
win.webContents.on('dom-ready', () => { win.webContents.on('dom-ready', function () {
menu.onToggleFullScreen(main.win.isFullScreen()) menu.onToggleFullScreen(main.win.isFullScreen())
}) })
@@ -73,27 +76,27 @@ function init (state, options) {
win.on('hide', onWindowBlur) win.on('hide', onWindowBlur)
win.on('show', onWindowFocus) win.on('show', onWindowFocus)
win.on('enter-full-screen', () => { win.on('enter-full-screen', function () {
menu.onToggleFullScreen(true) menu.onToggleFullScreen(true)
send('fullscreenChanged', true) send('fullscreenChanged', true)
win.setMenuBarVisibility(false) win.setMenuBarVisibility(false)
}) })
win.on('leave-full-screen', () => { win.on('leave-full-screen', function () {
menu.onToggleFullScreen(false) menu.onToggleFullScreen(false)
send('fullscreenChanged', false) send('fullscreenChanged', false)
win.setMenuBarVisibility(true) win.setMenuBarVisibility(true)
}) })
win.on('move', debounce(e => { win.on('move', debounce(function (e) {
send('windowBoundsChanged', e.sender.getBounds()) send('windowBoundsChanged', e.sender.getBounds())
}, 1000)) }, 1000))
win.on('resize', debounce(e => { win.on('resize', debounce(function (e) {
send('windowBoundsChanged', e.sender.getBounds()) send('windowBoundsChanged', e.sender.getBounds())
}, 1000)) }, 1000))
win.on('close', e => { win.on('close', function (e) {
if (process.platform !== 'darwin') { if (process.platform !== 'darwin') {
const tray = require('../tray') const tray = require('../tray')
if (!tray.hasTray()) { if (!tray.hasTray()) {
@@ -138,29 +141,37 @@ function setAspectRatio (aspectRatio) {
function setBounds (bounds, maximize) { function setBounds (bounds, maximize) {
// Do nothing in fullscreen // Do nothing in fullscreen
if (!main.win || main.win.isFullScreen()) { if (!main.win || main.win.isFullScreen()) {
log(`setBounds: not setting bounds because we're in full screen`) log('setBounds: not setting bounds because we\'re in full screen')
return return
} }
// Maximize or minimize, if the second argument is present // Maximize or minimize, if the second argument is present
if (maximize === true && !main.win.isMaximized()) { let willBeMaximized
if (maximize === true) {
if (!main.win.isMaximized()) {
log('setBounds: maximizing') log('setBounds: maximizing')
main.win.maximize() main.win.maximize()
} else if (maximize === false && main.win.isMaximized()) { }
willBeMaximized = true
} else if (maximize === false) {
if (main.win.isMaximized()) {
log('setBounds: unmaximizing') log('setBounds: unmaximizing')
main.win.unmaximize() main.win.unmaximize()
} }
willBeMaximized = false
} else {
willBeMaximized = main.win.isMaximized()
}
const willBeMaximized = typeof maximize === 'boolean' ? maximize : main.win.isMaximized()
// Assuming we're not maximized or maximizing, set the window size // Assuming we're not maximized or maximizing, set the window size
if (!willBeMaximized) { if (!willBeMaximized) {
log(`setBounds: setting bounds to ${JSON.stringify(bounds)}`) log('setBounds: setting bounds to ' + JSON.stringify(bounds))
if (bounds.x === null && bounds.y === null) { if (bounds.x === null && bounds.y === null) {
// X and Y not specified? By default, center on current screen // X and Y not specified? By default, center on current screen
const scr = electron.screen.getDisplayMatching(main.win.getBounds()) const scr = electron.screen.getDisplayMatching(main.win.getBounds())
bounds.x = Math.round(scr.bounds.x + (scr.bounds.width / 2) - (bounds.width / 2)) bounds.x = Math.round(scr.bounds.x + (scr.bounds.width / 2) - (bounds.width / 2))
bounds.y = Math.round(scr.bounds.y + (scr.bounds.height / 2) - (bounds.height / 2)) bounds.y = Math.round(scr.bounds.y + (scr.bounds.height / 2) - (bounds.height / 2))
log(`setBounds: centered to ${JSON.stringify(bounds)}`) log('setBounds: centered to ' + JSON.stringify(bounds))
} }
// Resize the window's content area (so window border doesn't need to be taken // Resize the window's content area (so window border doesn't need to be taken
// into account) // into account)

View File

@@ -10,8 +10,8 @@ const electron = require('electron')
const config = require('../../config') const config = require('../../config')
function init () { function init (decorate) {
const win = webtorrent.win = new electron.BrowserWindow({ let options = {
backgroundColor: '#1E1E1E', backgroundColor: '#1E1E1E',
backgroundThrottling: false, // do not throttle animations/timers when page is background backgroundThrottling: false, // do not throttle animations/timers when page is background
center: true, center: true,
@@ -26,7 +26,9 @@ function init () {
title: 'webtorrent-hidden-window', title: 'webtorrent-hidden-window',
useContentSize: true, useContentSize: true,
width: 150 width: 150
}) }
if (decorate) options = decorate(options)
const win = webtorrent.win = new electron.BrowserWindow(options)
win.loadURL(config.WINDOW_WEBTORRENT) win.loadURL(config.WINDOW_WEBTORRENT)

View File

@@ -1,6 +1,6 @@
const React = require('react') const React = require('react')
const { dispatcher } = require('../lib/dispatcher') const {dispatcher} = require('../lib/dispatcher')
module.exports = class CreateTorrentErrorPage extends React.Component { module.exports = class CreateTorrentErrorPage extends React.Component {
render () { render () {

View File

@@ -1,6 +1,6 @@
const React = require('react') const React = require('react')
const { dispatcher } = require('../lib/dispatcher') const {dispatcher} = require('../lib/dispatcher')
class Header extends React.Component { class Header extends React.Component {
render () { render () {

View File

@@ -5,7 +5,7 @@ const RaisedButton = require('material-ui/RaisedButton').default
module.exports = class ModalOKCancel extends React.Component { module.exports = class ModalOKCancel extends React.Component {
render () { render () {
const cancelStyle = { marginRight: 10, color: 'black' } const cancelStyle = { marginRight: 10, color: 'black' }
const { cancelText, onCancel, okText, onOK } = this.props const {cancelText, onCancel, okText, onOK} = this.props
return ( return (
<div className='float-right'> <div className='float-right'>
<FlatButton <FlatButton

View File

@@ -2,7 +2,7 @@ const React = require('react')
const TextField = require('material-ui/TextField').default const TextField = require('material-ui/TextField').default
const ModalOKCancel = require('./modal-ok-cancel') const ModalOKCancel = require('./modal-ok-cancel')
const { dispatch, dispatcher } = require('../lib/dispatcher') const {dispatch, dispatcher} = require('../lib/dispatcher')
module.exports = class OpenTorrentAddressModal extends React.Component { module.exports = class OpenTorrentAddressModal extends React.Component {
render () { render () {

View File

@@ -1,7 +1,7 @@
const React = require('react') const React = require('react')
const ModalOKCancel = require('./modal-ok-cancel') const ModalOKCancel = require('./modal-ok-cancel')
const { dispatch, dispatcher } = require('../lib/dispatcher') const {dispatch, dispatcher} = require('../lib/dispatcher')
module.exports = class RemoveTorrentModal extends React.Component { module.exports = class RemoveTorrentModal extends React.Component {
render () { render () {

View File

@@ -2,7 +2,7 @@ const React = require('react')
const electron = require('electron') const electron = require('electron')
const ModalOKCancel = require('./modal-ok-cancel') const ModalOKCancel = require('./modal-ok-cancel')
const { dispatcher } = require('../lib/dispatcher') const {dispatcher} = require('../lib/dispatcher')
module.exports = class UnsupportedMediaModal extends React.Component { module.exports = class UnsupportedMediaModal extends React.Component {
render () { render () {

View File

@@ -2,7 +2,7 @@ const React = require('react')
const electron = require('electron') const electron = require('electron')
const ModalOKCancel = require('./modal-ok-cancel') const ModalOKCancel = require('./modal-ok-cancel')
const { dispatch } = require('../lib/dispatcher') const {dispatch} = require('../lib/dispatcher')
module.exports = class UpdateAvailableModal extends React.Component { module.exports = class UpdateAvailableModal extends React.Component {
render () { render () {

View File

@@ -1,13 +0,0 @@
const { ipcRenderer } = require('electron')
module.exports = class FolderWatcherController {
start () {
console.log('-- IPC: start folder watcher')
ipcRenderer.send('startFolderWatcher')
}
stop () {
console.log('-- IPC: stop folder watcher')
ipcRenderer.send('stopFolderWatcher')
}
}

View File

@@ -1,4 +1,4 @@
const { ipcRenderer } = require('electron') const {ipcRenderer} = require('electron')
const telemetry = require('../lib/telemetry') const telemetry = require('../lib/telemetry')
const Playlist = require('../lib/playlist') const Playlist = require('../lib/playlist')

View File

@@ -2,9 +2,9 @@ const electron = require('electron')
const path = require('path') const path = require('path')
const Cast = require('../lib/cast') const Cast = require('../lib/cast')
const { dispatch } = require('../lib/dispatcher') const {dispatch} = require('../lib/dispatcher')
const telemetry = require('../lib/telemetry') const telemetry = require('../lib/telemetry')
const { UnplayableFileError, UnplayableTorrentError } = require('../lib/errors') const {UnplayableFileError, UnplayableTorrentError} = 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')
@@ -292,7 +292,7 @@ module.exports = class PlaybackController {
} }
function getAudioMetadata () { function getAudioMetadata () {
if (state.playing.type === 'audio') { if (state.playing.type === 'audio' && !fileSummary.audioInfo) {
ipcRenderer.send('wt-get-audio-metadata', torrentSummary.infoHash, index) ipcRenderer.send('wt-get-audio-metadata', torrentSummary.infoHash, index)
} }
} }

View File

@@ -1,4 +1,4 @@
const { dispatch } = require('../lib/dispatcher') const {dispatch} = require('../lib/dispatcher')
const ipcRenderer = require('electron').ipcRenderer const ipcRenderer = require('electron').ipcRenderer
// Controls the Preferences screen // Controls the Preferences screen
@@ -16,22 +16,45 @@ 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: Object.assign({}, state.saved.prefs)
})
ipcRenderer.send('setAllowNav', false) ipcRenderer.send('setAllowNav', false)
cb() cb()
}, },
destroy: () => { destroy: () => {
ipcRenderer.send('setAllowNav', true) ipcRenderer.send('setAllowNav', true)
this.save()
} }
}) })
} }
// Updates a single property in the saved prefs // Updates a single property in the UNSAVED prefs
// For example: updatePreferences('isFileHandler', true) // For example: updatePreferences('foo.bar', 'baz')
// Call save() to save to config.json
update (property, value) { update (property, value) {
if (property === 'isFileHandler') ipcRenderer.send('setDefaultFileHandler', value) const path = property.split('.')
else if (property === 'startup') ipcRenderer.send('setStartup', value) let obj = this.state.unsaved.prefs
let i
for (i = 0; i < path.length - 1; i++) {
if (typeof obj[path[i]] === 'undefined') {
obj[path[i]] = {}
}
obj = obj[path[i]]
}
obj[path[i]] = value
}
this.state.saved.prefs[property] = value // All unsaved prefs take effect atomically, and are saved to config.json
save () {
const state = this.state
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)
dispatch('stateSaveImmediate') dispatch('stateSaveImmediate')
dispatch('checkDownloadPath') dispatch('checkDownloadPath')
} }

View File

@@ -5,7 +5,7 @@ const parallel = require('run-parallel')
const remote = electron.remote const remote = electron.remote
const { dispatch } = require('../lib/dispatcher') const {dispatch} = require('../lib/dispatcher')
module.exports = class SubtitlesController { module.exports = class SubtitlesController {
constructor (state) { constructor (state) {

View File

@@ -3,7 +3,7 @@ const ipcRenderer = require('electron').ipcRenderer
const TorrentSummary = require('../lib/torrent-summary') const TorrentSummary = require('../lib/torrent-summary')
const sound = require('../lib/sound') const sound = require('../lib/sound')
const { dispatch } = require('../lib/dispatcher') const {dispatch} = require('../lib/dispatcher')
module.exports = class TorrentController { module.exports = class TorrentController {
constructor (state) { constructor (state) {

View File

@@ -2,8 +2,8 @@ const fs = require('fs')
const path = require('path') 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 {TorrentKeyNotFoundError} = require('../lib/errors')
const sound = require('../lib/sound') const sound = require('../lib/sound')
const TorrentSummary = require('../lib/torrent-summary') const TorrentSummary = require('../lib/torrent-summary')
@@ -174,7 +174,6 @@ module.exports = class TorrentListController {
resumePausedTorrents () { resumePausedTorrents () {
console.log('Playback Priority: resuming paused torrents') console.log('Playback Priority: resuming paused torrents')
if (!this.state.saved.torrentsToResume || !this.state.saved.torrentsToResume.length) return
this.state.saved.torrentsToResume.map((infoHash) => { this.state.saved.torrentsToResume.map((infoHash) => {
this.toggleTorrent(infoHash) this.toggleTorrent(infoHash)
}) })

View File

@@ -1,4 +1,4 @@
const { dispatch } = require('../lib/dispatcher') const {dispatch} = require('../lib/dispatcher')
// Controls the UI checking for new versions of the app, prompting install // Controls the UI checking for new versions of the app, prompting install
module.exports = class UpdateController { module.exports = class UpdateController {

View File

@@ -13,10 +13,8 @@ module.exports = {
setRate setRate
} }
const http = require('http')
const config = require('../../config') const config = require('../../config')
const { CastingError } = require('./errors') 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
@@ -83,7 +81,7 @@ function testPlayer (type) {
} }
function getDevices () { function getDevices () {
return [{ name: type + '-1' }, { name: type + '-2' }] return [{name: type + '-1'}, {name: type + '-2'}]
} }
function open () {} function open () {}
@@ -132,35 +130,11 @@ function chromecastPlayer () {
}) })
} }
function serveSubtitles (callback) {
const subtitles = state.playing.subtitles
const selectedSubtitle = subtitles.tracks[subtitles.selectedIndex]
if (!selectedSubtitle) {
callback()
} else {
ret.subServer = http.createServer((req, res) => {
res.writeHead(200, {
'Content-Type': 'text/vtt;charset=utf-8',
'Access-Control-Allow-Origin': '*',
'Transfer-Encoding': 'chunked'
})
res.end(Buffer.from(selectedSubtitle.buffer.substr(21), 'base64'))
}).listen(0, function () {
const port = ret.subServer.address().port
const subtitlesUrl = 'http://' + state.server.networkAddress + ':' + port + '/'
callback(subtitlesUrl)
})
}
}
function open () { function open () {
const torrentSummary = state.saved.torrents.find((x) => x.infoHash === state.playing.infoHash) const torrentSummary = state.saved.torrents.find((x) => x.infoHash === state.playing.infoHash)
serveSubtitles(function (subtitlesUrl) {
ret.device.play(state.server.networkURL + '/' + state.playing.fileIndex, { ret.device.play(state.server.networkURL + '/' + state.playing.fileIndex, {
type: 'video/mp4', type: 'video/mp4',
title: config.APP_NAME + ' - ' + torrentSummary.name, title: config.APP_NAME + ' - ' + torrentSummary.name
subtitles: subtitlesUrl ? [subtitlesUrl] : [],
autoSubtitles: !!subtitlesUrl
}, function (err) { }, function (err) {
if (err) { if (err) {
state.playing.location = 'local' state.playing.location = 'local'
@@ -173,7 +147,6 @@ function chromecastPlayer () {
} }
update() update()
}) })
})
} }
function play (callback) { function play (callback) {
@@ -186,9 +159,6 @@ function chromecastPlayer () {
function stop (callback) { function stop (callback) {
ret.device.stop(callback) ret.device.stop(callback)
if (ret.subServer) {
ret.subServer.close()
}
} }
function status () { function status () {
@@ -429,11 +399,11 @@ function toggleMenu (location) {
} }
// Show a menu // Show a menu
state.devices.castMenu = { location, devices } state.devices.castMenu = {location, devices}
} }
function selectDevice (index) { function selectDevice (index) {
const { location, devices } = state.devices.castMenu const {location, devices} = state.devices.castMenu
// Start casting // Start casting
const player = getPlayer(location) const player = getPlayer(location)

View File

@@ -1,11 +0,0 @@
const mediaExtensions = {
audio: [
'.aac', '.asf', '.flac', '.m2a', '.m4a', '.m4b', '.mp2', '.mp4',
'.mp3', '.oga', '.ogg', '.opus', '.wma', '.wav', '.wv', '.wvp'],
video: [
'.avi', '.mp4', '.m4v', '.webm', '.mov', '.mkv', 'mpg', 'mpeg',
'.ogv', '.webm', '.wmv'],
image: ['.gif', '.jpg', '.jpeg', '.png']
}
module.exports = mediaExtensions

View File

@@ -3,7 +3,7 @@ module.exports = {
} }
const config = require('../../config') const config = require('../../config')
const { InvalidSoundNameError } = require('./errors') const {InvalidSoundNameError} = require('./errors')
const path = require('path') const path = require('path')
const VOLUME = 0.25 const VOLUME = 0.25

View File

@@ -1,6 +1,6 @@
const appConfig = require('application-config')('WebTorrent') const appConfig = require('application-config')('WebTorrent')
const path = require('path') const path = require('path')
const { EventEmitter } = require('events') const {EventEmitter} = require('events')
const config = require('../../config') const config = require('../../config')
@@ -113,6 +113,7 @@ function setupStateSaved (cb) {
const cpFile = require('cp-file') const cpFile = require('cp-file')
const fs = require('fs') const fs = require('fs')
const parseTorrent = require('parse-torrent') const parseTorrent = require('parse-torrent')
const parallel = require('run-parallel')
const saved = { const saved = {
prefs: { prefs: {
@@ -120,10 +121,7 @@ function setupStateSaved (cb) {
isFileHandler: false, isFileHandler: false,
openExternalPlayer: false, openExternalPlayer: false,
externalPlayerPath: null, externalPlayerPath: null,
startup: false, startup: false
autoAddTorrents: false,
torrentsFolderPath: '',
highestPlaybackPriority: true
}, },
torrents: config.DEFAULT_TORRENTS.map(createTorrentObject), torrents: config.DEFAULT_TORRENTS.map(createTorrentObject),
torrentsToResume: [], torrentsToResume: [],
@@ -132,25 +130,26 @@ function setupStateSaved (cb) {
const tasks = [] const tasks = []
config.DEFAULT_TORRENTS.forEach((t, i) => { config.DEFAULT_TORRENTS.map(function (t, i) {
const infoHash = saved.torrents[i].infoHash const infoHash = saved.torrents[i].infoHash
tasks.push( tasks.push(function (cb) {
cpFile( cpFile(
path.join(config.STATIC_PATH, t.posterFileName), path.join(config.STATIC_PATH, t.posterFileName),
path.join(config.POSTER_PATH, infoHash + path.extname(t.posterFileName)) path.join(config.POSTER_PATH, infoHash + path.extname(t.posterFileName))
) ).then(cb).catch(cb)
) })
tasks.push( tasks.push(function (cb) {
cpFile( cpFile(
path.join(config.STATIC_PATH, t.torrentFileName), path.join(config.STATIC_PATH, t.torrentFileName),
path.join(config.TORRENT_PATH, infoHash + '.torrent') path.join(config.TORRENT_PATH, infoHash + '.torrent')
) ).then(cb).catch(cb)
) })
}) })
Promise.all(tasks) parallel(tasks, function (err) {
.then(() => cb(null, saved)) if (err) return cb(err)
.catch(err => cb(err)) cb(null, saved)
})
function createTorrentObject (t) { function createTorrentObject (t) {
// TODO: Doing several fs.readFileSync calls during first startup is not ideal // TODO: Doing several fs.readFileSync calls during first startup is not ideal

View File

@@ -121,7 +121,7 @@ function getTorrentStats (state) {
} }
// Then, round all the counts and sums to the nearest power of 2 // Then, round all the counts and sums to the nearest power of 2
const ret = roundTorrentStats({ count, sizeMB }) const ret = roundTorrentStats({count, sizeMB})
ret.byStatus = { ret.byStatus = {
new: roundTorrentStats(byStatus.new), new: roundTorrentStats(byStatus.new),
downloading: roundTorrentStats(byStatus.downloading), downloading: roundTorrentStats(byStatus.downloading),
@@ -198,7 +198,7 @@ function logUncaughtError (procName, e) {
// Log the app version *at the time of the error* // Log the app version *at the time of the error*
const version = config.APP_VERSION const version = config.APP_VERSION
telemetry.uncaughtErrors.push({ process: procName, message, stack, version }) telemetry.uncaughtErrors.push({process: procName, message, stack, version})
} }
// Turns a DOM element into a string, eg "DIV.my-class.visible" // Turns a DOM element into a string, eg "DIV.my-class.visible"

View File

@@ -8,8 +8,6 @@ module.exports = {
const path = require('path') const path = require('path')
const mediaExtensions = require('./media-extensions')
// Checks whether a fileSummary or file path is audio/video that we can play, // Checks whether a fileSummary or file path is audio/video that we can play,
// based on the file extension // based on the file extension
function isPlayable (file) { function isPlayable (file) {
@@ -18,12 +16,30 @@ function isPlayable (file) {
// Checks whether a fileSummary or file path is playable video // Checks whether a fileSummary or file path is playable video
function isVideo (file) { function isVideo (file) {
return mediaExtensions.video.includes(getFileExtension(file)) return [
'.avi',
'.m4v',
'.mkv',
'.mov',
'.mp4',
'.mpg',
'.ogv',
'.webm',
'.wmv'
].includes(getFileExtension(file))
} }
// Checks whether a fileSummary or file path is playable audio // Checks whether a fileSummary or file path is playable audio
function isAudio (file) { function isAudio (file) {
return mediaExtensions.audio.includes(getFileExtension(file)) return [
'.aac',
'.ac3',
'.mp3',
'.ogg',
'.wav',
'.flac',
'.m4a'
].includes(getFileExtension(file))
} }
// Checks if the argument is either: // Checks if the argument is either:

View File

@@ -3,143 +3,39 @@ module.exports = torrentPoster
const captureFrame = require('capture-frame') const captureFrame = require('capture-frame')
const path = require('path') const path = require('path')
const mediaExtensions = require('./media-extensions')
const msgNoSuitablePoster = 'Cannot generate a poster from any files in the torrent'
function torrentPoster (torrent, cb) { function torrentPoster (torrent, cb) {
// First, try to use a poster image if available // First, try to use a poster image if available
const posterFile = torrent.files.filter(function (file) { const posterFile = torrent.files.filter(function (file) {
return /^poster\.(jpg|png|gif)$/.test(file.name) return /^poster\.(jpg|png|gif)$/.test(file.name)
})[0] })[0]
if (posterFile) return extractPoster(posterFile, cb) if (posterFile) return torrentPosterFromImage(posterFile, torrent, cb)
// 'score' each media type based on total size present in torrent // Second, try to use the largest video file
const bestScore = ['audio', 'video', 'image'].map(mediaType => { // Filter out file formats that the <video> tag definitely can't play
return { const videoFile = getLargestFileByExtension(torrent, ['.mp4', '.m4v', '.webm', '.mov', '.mkv'])
type: mediaType, if (videoFile) return torrentPosterFromVideo(videoFile, torrent, cb)
size: calculateDataLengthByExtension(torrent, mediaExtensions[mediaType]) }
}).sort((a, b) => { // sort descending on size
return b.size - a.size
})[0]
if (bestScore.size === 0) { // Third, try to use the largest image file
// Admit defeat, no video, audio or image had a significant presence const imgFile = getLargestFileByExtension(torrent, ['.gif', '.jpg', '.jpeg', '.png'])
return cb(new Error(msgNoSuitablePoster)) if (imgFile) return torrentPosterFromImage(imgFile, torrent, cb)
}
// Based on which media type is dominant we select the corresponding poster function // TODO: generate a waveform from the largest sound file
switch (bestScore.type) { // Finally, admit defeat
case 'audio': return cb(new Error('Cannot generate a poster from any files in the torrent'))
return torrentPosterFromAudio(torrent, cb)
case 'image':
return torrentPosterFromImage(torrent, cb)
case 'video':
return torrentPosterFromVideo(torrent, cb)
}
} }
/**
* Calculate the total data size of file matching one of the provided extensions
* @param torrent
* @param extensions List of extension to match
* @returns {number} total size, of matches found (>= 0)
*/
function calculateDataLengthByExtension (torrent, extensions) {
const files = filterOnExtension(torrent, extensions)
if (files.length === 0) return 0
return files
.map(file => file.length)
.reduce((a, b) => {
return a + b
})
}
/**
* Get the largest file of a given torrent, filtered by provided extension
* @param torrent Torrent to search in
* @param extensions Extension whitelist filter
* @returns Torrent file object
*/
function getLargestFileByExtension (torrent, extensions) { function getLargestFileByExtension (torrent, extensions) {
const files = filterOnExtension(torrent, extensions) const files = torrent.files.filter(function (file) {
const extname = path.extname(file.name).toLowerCase()
return extensions.indexOf(extname) !== -1
})
if (files.length === 0) return undefined if (files.length === 0) return undefined
return files.reduce((a, b) => { return files.reduce(function (a, b) {
return a.length > b.length ? a : b return a.length > b.length ? a : b
}) })
} }
/** function torrentPosterFromVideo (file, torrent, cb) {
* Filter file on a list extension, can be used to find al image files
* @param torrent Torrent to filter files from
* @param extensions File extensions to filter on
* @returns {number} Array of torrent file objects matching one of the given extensions
*/
function filterOnExtension (torrent, extensions) {
return torrent.files.filter(file => {
const extname = path.extname(file.name).toLowerCase()
return extensions.indexOf(extname) !== -1
})
}
/**
* Returns a score how likely the file is suitable as a poster
* @param imgFile File object of an image
* @returns {number} Score, higher score is a better match
*/
function scoreAudioCoverFile (imgFile) {
const fileName = path.basename(imgFile.name, path.extname(imgFile.name)).toLowerCase()
const relevanceScore = {
cover: 80,
folder: 80,
album: 80,
front: 80,
back: 20,
spectrogram: -80
}
for (let keyword in relevanceScore) {
if (fileName === keyword) {
return relevanceScore[keyword]
}
if (fileName.indexOf(keyword) !== -1) {
return relevanceScore[keyword]
}
}
return 0
}
function torrentPosterFromAudio (torrent, cb) {
const imageFiles = filterOnExtension(torrent, mediaExtensions.image)
if (imageFiles.length === 0) return cb(new Error(msgNoSuitablePoster))
const bestCover = imageFiles.map(file => {
return {
file: file,
score: scoreAudioCoverFile(file)
}
}).reduce((a, b) => {
if (a.score > b.score) {
return a
}
if (b.score > a.score) {
return b
}
// If score is equal, pick the largest file, aiming for highest resolution
if (a.file.length > b.file.length) {
return a
}
return b
})
const extname = path.extname(bestCover.file.name)
bestCover.file.getBuffer((err, buf) => cb(err, buf, extname))
}
function torrentPosterFromVideo (torrent, cb) {
const file = getLargestFileByExtension(torrent, mediaExtensions.video)
const index = torrent.files.indexOf(file) const index = torrent.files.indexOf(file)
const server = torrent.createServer(0) const server = torrent.createServer(0)
@@ -174,19 +70,14 @@ function torrentPosterFromVideo (torrent, cb) {
server.destroy() server.destroy()
if (buf.length === 0) return cb(new Error(msgNoSuitablePoster)) if (buf.length === 0) return cb(new Error('Generated poster contains no data'))
cb(null, buf, '.jpg') cb(null, buf, '.jpg')
} }
} }
} }
function torrentPosterFromImage (torrent, cb) { function torrentPosterFromImage (file, torrent, cb) {
const file = getLargestFileByExtension(torrent, mediaExtensions.image)
extractPoster(file, cb)
}
function extractPoster (file, cb) {
const extname = path.extname(file.name) const extname = path.extname(file.name)
file.getBuffer((err, buf) => { return cb(err, buf, extname) }) file.getBuffer((err, buf) => cb(err, buf, extname))
} }

View File

@@ -36,6 +36,9 @@ const telemetry = require('./lib/telemetry')
const sound = require('./lib/sound') const sound = require('./lib/sound')
const TorrentPlayer = require('./lib/torrent-player') const TorrentPlayer = require('./lib/torrent-player')
const Plugins = require('./plugins')
const plugins = new Plugins()
// Perf optimization: Needed immediately, so do not lazy load it below // Perf optimization: Needed immediately, so do not lazy load it below
const TorrentListController = require('./controllers/torrent-list-controller') const TorrentListController = require('./controllers/torrent-list-controller')
@@ -111,13 +114,11 @@ function onState (err, _state) {
update: createGetter(() => { update: createGetter(() => {
const UpdateController = require('./controllers/update-controller') const UpdateController = require('./controllers/update-controller')
return new UpdateController(state) return new UpdateController(state)
}),
folderWatcher: createGetter(() => {
const FolderWatcherController = require('./controllers/folder-watcher-controller')
return new FolderWatcherController()
}) })
} }
plugins.init({dispatch, state})
// Add first page to location history // Add first page to location history
state.location.go({ state.location.go({
url: 'home', url: 'home',
@@ -300,8 +301,6 @@ const dispatchHandlers = {
'preferences': () => controllers.prefs().show(), 'preferences': () => controllers.prefs().show(),
'updatePreferences': (key, value) => controllers.prefs().update(key, value), 'updatePreferences': (key, value) => controllers.prefs().update(key, value),
'checkDownloadPath': checkDownloadPath, 'checkDownloadPath': checkDownloadPath,
'startFolderWatcher': () => controllers.folderWatcher().start(),
'stopFolderWatcher': () => controllers.folderWatcher().stop(),
// Update (check for new versions on Linux, where there's no auto updater) // Update (check for new versions on Linux, where there's no auto updater)
'updateAvailable': (version) => controllers.update().updateAvailable(version), 'updateAvailable': (version) => controllers.update().updateAvailable(version),
@@ -338,6 +337,7 @@ function dispatch (action, ...args) {
} }
const handler = dispatchHandlers[action] const handler = dispatchHandlers[action]
plugins.on(action, args)
if (handler) handler(...args) if (handler) handler(...args)
else console.error('Missing dispatch handler: ' + action) else console.error('Missing dispatch handler: ' + action)
@@ -450,7 +450,7 @@ function setDimensions (dimensions) {
) )
ipcRenderer.send('setAspectRatio', aspectRatio) ipcRenderer.send('setAspectRatio', aspectRatio)
ipcRenderer.send('setBounds', { contentBounds: true, x: null, y: null, width, height }) ipcRenderer.send('setBounds', {contentBounds: true, x: null, y: null, width, height})
state.playing.aspectRatio = aspectRatio state.playing.aspectRatio = aspectRatio
} }
@@ -459,13 +459,6 @@ function setDimensions (dimensions) {
function onOpen (files) { function onOpen (files) {
if (!Array.isArray(files)) files = [ files ] if (!Array.isArray(files)) files = [ files ]
// File API seems to transform "magnet:?foo" in "magnet:///?foo"
// this is a sanitization
files = files.map(file => {
if (typeof file !== 'string') return file
return file.replace(/^magnet:\/+\?/i, 'magnet:?')
})
const url = state.location.url() const url = state.location.url()
const allTorrents = files.every(TorrentPlayer.isTorrent) const allTorrents = files.every(TorrentPlayer.isTorrent)
const allSubtitles = files.every(controllers.subtitles().isSubtitle) const allSubtitles = files.every(controllers.subtitles().isSubtitle)

View File

@@ -3,7 +3,7 @@ const path = require('path')
const prettyBytes = require('prettier-bytes') const prettyBytes = require('prettier-bytes')
const React = require('react') const React = require('react')
const { dispatch, dispatcher } = require('../lib/dispatcher') const {dispatch, dispatcher} = require('../lib/dispatcher')
const FlatButton = require('material-ui/FlatButton').default const FlatButton = require('material-ui/FlatButton').default
const RaisedButton = require('material-ui/RaisedButton').default const RaisedButton = require('material-ui/RaisedButton').default
@@ -35,7 +35,7 @@ class CreateTorrentPage extends React.Component {
// Then, exclude .DS_Store and other dotfiles // Then, exclude .DS_Store and other dotfiles
const files = info.files const files = info.files
.filter((f) => !containsDots(f.path, pathPrefix)) .filter((f) => !containsDots(f.path, pathPrefix))
.map((f) => ({ name: f.name, path: f.path, size: f.size })) .map((f) => ({name: f.name, path: f.path, size: f.size}))
if (files.length === 0) return (<CreateTorrentErrorPage state={state} />) if (files.length === 0) return (<CreateTorrentErrorPage state={state} />)
// Then, use the name of the base folder (or sole file, for a single file torrent) // Then, use the name of the base folder (or sole file, for a single file torrent)
@@ -65,9 +65,9 @@ class CreateTorrentPage extends React.Component {
} }
// Create React event handlers only once // Create React event handlers only once
this.setIsPrivate = (_, isPrivate) => this.setState({ isPrivate }) this.setIsPrivate = (_, isPrivate) => this.setState({isPrivate})
this.setComment = (_, comment) => this.setState({ comment }) this.setComment = (_, comment) => this.setState({comment})
this.setTrackers = (_, trackers) => this.setState({ trackers }) this.setTrackers = (_, trackers) => this.setState({trackers})
this.handleSubmit = handleSubmit.bind(this) this.handleSubmit = handleSubmit.bind(this)
} }
@@ -141,7 +141,7 @@ class CreateTorrentPage extends React.Component {
<label>Private:</label> <label>Private:</label>
<Checkbox <Checkbox
className='torrent-is-private control' className='torrent-is-private control'
style={{ display: '' }} style={{display: ''}}
checked={this.state.isPrivate} checked={this.state.isPrivate}
onCheck={this.setIsPrivate} /> onCheck={this.setIsPrivate} />
</div> </div>
@@ -191,14 +191,9 @@ function handleSubmit () {
path: this.state.basePath, path: this.state.basePath,
files: this.state.files, files: this.state.files,
announce: announceList, announce: announceList,
private: this.state.isPrivate,
comment: this.state.comment.trim() comment: this.state.comment.trim()
} }
// If torrent is not private, leave private flag unset. This ensures that
// the torrent info hash will match the result generated by other tools,
// including webtorrent-cli.
if (this.state.isPrivate) options.private = true
dispatch('createTorrent', options) dispatch('createTorrent', options)
} }

View File

@@ -5,7 +5,7 @@ const zeroFill = require('zero-fill')
const TorrentSummary = require('../lib/torrent-summary') const TorrentSummary = require('../lib/torrent-summary')
const Playlist = require('../lib/playlist') const Playlist = require('../lib/playlist')
const { dispatch, dispatcher } = require('../lib/dispatcher') const {dispatch, dispatcher} = require('../lib/dispatcher')
const config = require('../../config') const config = require('../../config')
// Shows a streaming video player. Standard features + Chromecast + Airplay // Shows a streaming video player. Standard features + Chromecast + Airplay
@@ -160,7 +160,6 @@ function renderMedia (state) {
} else { } else {
// When the last video completes, pause the video instead of looping // When the last video completes, pause the video instead of looping
state.playing.isPaused = true state.playing.isPaused = true
if (state.window.isFullScreen) dispatch('toggleFullScreen')
} }
} }
@@ -204,41 +203,28 @@ function renderOverlay (state) {
) )
} }
/**
* Render track or disk number string
* @param common metadata.common part
* @param key should be either 'track' or 'disk'
* @return track or disk number metadata as JSX block
*/
function renderTrack (common, key) {
// Audio metadata: track-number
if (common[key] && common[key].no) {
let str = `${common[key].no}`
if (common[key].of) {
str += ` of ${common[key].of}`
}
const style = { textTransform: 'capitalize' }
return (
<div className={`audio-${key}`}>
<label style={style}>{key}</label> {str}
</div>
)
}
}
function renderAudioMetadata (state) { function renderAudioMetadata (state) {
const fileSummary = state.getPlayingFileSummary() const fileSummary = state.getPlayingFileSummary()
if (!fileSummary.audioInfo) return if (!fileSummary.audioInfo) return
const common = fileSummary.audioInfo.common || {} const info = fileSummary.audioInfo
// Get audio track info // Get audio track info
const title = common.title ? common.title : fileSummary.name let title = info.title
if (!title) {
title = fileSummary.name
}
let artist = info.artist && info.artist[0]
let album = info.album
if (album && info.year && !album.includes(info.year)) {
album += ' (' + info.year + ')'
}
let track
if (info.track && info.track.no && info.track.of) {
track = info.track.no + ' of ' + info.track.of
}
// Show a small info box in the middle of the screen with title/album/etc // Show a small info box in the middle of the screen with title/album/etc
const elems = [] const elems = []
// Audio metadata: artist(s)
const artist = common.artist || common.albumartist
if (artist) { if (artist) {
elems.push(( elems.push((
<div key='artist' className='audio-artist'> <div key='artist' className='audio-artist'>
@@ -246,86 +232,17 @@ function renderAudioMetadata (state) {
</div> </div>
)) ))
} }
if (album) {
// Audio metadata: disk & track-number
const count = ['track', 'disk']
count.forEach(key => {
const nrElem = renderTrack(common, key)
if (nrElem) {
elems.push(nrElem)
}
})
// Audio metadata: album
if (common.album) {
elems.push(( elems.push((
<div key='album' className='audio-album'> <div key='album' className='audio-album'>
<label>Album</label>{common.album} <label>Album</label>{album}
</div> </div>
)) ))
} }
if (track) {
// Audio metadata: year
if (common.year) {
elems.push(( elems.push((
<div key='year' className='audio-year'> <div key='track' className='audio-track'>
<label>Year</label>{common.year} <label>Track</label>{track}
</div>
))
}
// Audio metadata: release information (label & catalog-number)
if (common.label || common.catalognumber) {
const releaseInfo = []
if (common.label && common.catalognumber &&
common.label.length === common.catalognumber.length) {
// Assume labels & catalog-numbers are pairs
for (let n = 0; n < common.label.length; ++n) {
releaseInfo.push(common.label[0] + ' / ' + common.catalognumber[n])
}
} else {
if (common.label) {
releaseInfo.push(...common.label)
}
if (common.catalognumber) {
releaseInfo.push(...common.catalognumber)
}
}
elems.push((
<div key='release' className='audio-release'>
<label>Release</label>{ releaseInfo.join(', ') }
</div>
))
}
// Audio metadata: format
const format = []
fileSummary.audioInfo.format = fileSummary.audioInfo.format || ''
if (fileSummary.audioInfo.format.dataformat) {
format.push(fileSummary.audioInfo.format.dataformat)
}
if (fileSummary.audioInfo.format.bitrate) {
format.push(Math.round(fileSummary.audioInfo.format.bitrate / 1000) + ' kbps') // 128 kbps
}
if (fileSummary.audioInfo.format.sampleRate) {
format.push(Math.round(fileSummary.audioInfo.format.sampleRate / 100) / 10 + ' kHz') // 44.1 kHz
}
if (fileSummary.audioInfo.format.bitsPerSample) {
format.push(fileSummary.audioInfo.format.bitsPerSample + ' bit')
}
if (format.length > 0) {
elems.push((
<div key='format' className='audio-format'>
<label>Format</label>{ format.join(', ') }
</div>
))
}
// Audio metadata: comments
if (common.comment) {
elems.push((
<div key='comments' className='audio-comments'>
<label>Comments</label>{common.comment.join(' / ')}
</div> </div>
)) ))
} }
@@ -356,7 +273,7 @@ function renderLoadingSpinner (state) {
return ( return (
<div key='loading' className='media-stalled'> <div key='loading' className='media-stalled'>
<div key='loading-spinner' className='loading-spinner' /> <div key='loading-spinner' className='loading-spinner'>&nbsp;</div>
<div key='loading-progress' className='loading-status ellipsis'> <div key='loading-progress' className='loading-status ellipsis'>
<span className='progress'>{fileProgress}%</span> downloaded <span className='progress'>{fileProgress}%</span> downloaded
<span> {prettyBytes(prog.downloadSpeed || 0)}/s</span> <span> {prettyBytes(prog.downloadSpeed || 0)}/s</span>
@@ -386,7 +303,7 @@ function renderCastScreen (state) {
isCast = false isCast = false
} else if (state.playing.location === 'error') { } else if (state.playing.location === 'error') {
castIcon = 'error_outline' castIcon = 'error_outline'
castType = 'Unable to Play' castType = 'Error'
isCast = false isCast = false
} }
@@ -416,7 +333,7 @@ function renderCastScreen (state) {
function renderCastOptions (state) { function renderCastOptions (state) {
if (!state.devices.castMenu) return if (!state.devices.castMenu) return
const { location, devices } = state.devices.castMenu const {location, devices} = state.devices.castMenu
const player = state.devices[location] const player = state.devices[location]
const items = devices.map(function (device, ix) { const items = devices.map(function (device, ix) {
@@ -539,9 +456,9 @@ function renderPlayerControls (state) {
// Add the cast buttons. Icons for each cast type, connected/disconnected: // Add the cast buttons. Icons for each cast type, connected/disconnected:
const buttonIcons = { const buttonIcons = {
'chromecast': { true: 'cast_connected', false: 'cast' }, 'chromecast': {true: 'cast_connected', false: 'cast'},
'airplay': { true: 'airplay', false: 'airplay' }, 'airplay': {true: 'airplay', false: 'airplay'},
'dlna': { true: 'tv', false: 'tv' } 'dlna': {true: 'tv', false: 'tv'}
} }
castTypes.forEach(function (castType) { castTypes.forEach(function (castType) {
// Do we show this button (eg. the Chromecast button) at all? // Do we show this button (eg. the Chromecast button) at all?
@@ -606,8 +523,8 @@ function renderPlayerControls (state) {
)) ))
// Show video playback progress // Show video playback progress
const currentTimeStr = formatTime(state.playing.currentTime, state.playing.duration) const currentTimeStr = formatTime(state.playing.currentTime)
const durationStr = formatTime(state.playing.duration, state.playing.duration) const durationStr = formatTime(state.playing.duration)
elements.push(( elements.push((
<span key='time' className='time float-left'> <span key='time' className='time float-left'>
{currentTimeStr} / {durationStr} {currentTimeStr} / {durationStr}
@@ -696,7 +613,7 @@ function renderLoadingBar (state) {
for (let i = fileProg.startPiece; i <= fileProg.endPiece; i++) { for (let i = fileProg.startPiece; i <= fileProg.endPiece; i++) {
const partPresent = Bitfield.prototype.get.call(prog.bitfield, i) const partPresent = Bitfield.prototype.get.call(prog.bitfield, i)
if (partPresent && !lastPiecePresent) { if (partPresent && !lastPiecePresent) {
parts.push({ start: i - fileProg.startPiece, count: 1 }) parts.push({start: i - fileProg.startPiece, count: 1})
} else if (partPresent) { } else if (partPresent) {
parts[parts.length - 1].count++ parts[parts.length - 1].count++
} }
@@ -729,19 +646,17 @@ function cssBackgroundImageDarkGradient () {
'rgba(0,0,0,0.4) 0%, rgba(0,0,0,1) 100%)' 'rgba(0,0,0,0.4) 0%, rgba(0,0,0,1) 100%)'
} }
function formatTime (time, total) { function formatTime (time) {
if (typeof time !== 'number' || Number.isNaN(time)) { if (typeof time !== 'number' || Number.isNaN(time)) {
return '0:00' return '0:00'
} }
let totalHours = Math.floor(total / 3600)
let totalMinutes = Math.floor(total % 3600 / 60)
let hours = Math.floor(time / 3600) let hours = Math.floor(time / 3600)
let minutes = Math.floor(time % 3600 / 60) let minutes = Math.floor(time % 3600 / 60)
if (totalMinutes > 9) { if (hours > 0) {
minutes = zeroFill(2, minutes) minutes = zeroFill(2, minutes)
} }
let seconds = zeroFill(2, Math.floor(time % 60)) let seconds = zeroFill(2, Math.floor(time % 60))
return (totalHours > 0 ? hours + ':' : '') + minutes + ':' + seconds return (hours > 0 ? hours + ':' : '') + minutes + ':' + seconds
} }

View File

@@ -7,7 +7,7 @@ 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 { dispatch } = require('../lib/dispatcher') const {dispatch} = require('../lib/dispatcher')
const config = require('../../config') const config = require('../../config')
class PreferencesPage extends React.Component { class PreferencesPage extends React.Component {
@@ -37,7 +37,7 @@ class PreferencesPage extends React.Component {
}} }}
onChange={this.handleDownloadPathChange} onChange={this.handleDownloadPathChange}
title='Download location' title='Download location'
value={this.props.state.saved.prefs.downloadPath} /> value={this.props.state.unsaved.prefs.downloadPath} />
</Preference> </Preference>
) )
} }
@@ -51,7 +51,7 @@ class PreferencesPage extends React.Component {
<Preference> <Preference>
<Checkbox <Checkbox
className='control' className='control'
checked={!this.props.state.saved.prefs.openExternalPlayer} checked={!this.props.state.unsaved.prefs.openExternalPlayer}
label={'Play torrent media files using WebTorrent'} label={'Play torrent media files using WebTorrent'}
onCheck={this.handleOpenExternalPlayerChange} /> onCheck={this.handleOpenExternalPlayerChange} />
</Preference> </Preference>
@@ -67,7 +67,7 @@ class PreferencesPage extends React.Component {
<Preference> <Preference>
<Checkbox <Checkbox
className='control' className='control'
checked={this.props.state.saved.prefs.highestPlaybackPriority} checked={this.props.state.unsaved.prefs.highestPlaybackPriority}
label={'Highest Playback Priority'} label={'Highest Playback Priority'}
onCheck={this.handleHighestPlaybackPriorityChange} onCheck={this.handleHighestPlaybackPriorityChange}
/> />
@@ -81,10 +81,10 @@ class PreferencesPage extends React.Component {
} }
externalPlayerPathSelector () { externalPlayerPathSelector () {
const playerPath = this.props.state.saved.prefs.externalPlayerPath const playerPath = this.props.state.unsaved.prefs.externalPlayerPath
const playerName = this.props.state.getExternalPlayerName() const playerName = this.props.state.getExternalPlayerName()
const description = this.props.state.saved.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}.`
: `Torrent media files will play in ${playerName} if WebTorrent cannot play them.` : `Torrent media files will play in ${playerName} if WebTorrent cannot play them.`
@@ -108,61 +108,8 @@ class PreferencesPage extends React.Component {
dispatch('updatePreferences', 'externalPlayerPath', filePath) dispatch('updatePreferences', 'externalPlayerPath', filePath)
} }
autoAddTorrentsCheckbox () {
return (
<Preference>
<Checkbox
className='control'
checked={this.props.state.saved.prefs.autoAddTorrents}
label={'Watch for new .torrent files and add them immediately'}
onCheck={(e, value) => { this.handleAutoAddTorrentsChange(e, value) }}
/>
</Preference>
)
}
handleAutoAddTorrentsChange (e, isChecked) {
const torrentsFolderPath = this.props.state.saved.prefs.torrentsFolderPath
if (isChecked && !torrentsFolderPath) {
alert('Select a torrents folder first.') // eslint-disable-line
e.preventDefault()
return
}
dispatch('updatePreferences', 'autoAddTorrents', isChecked)
if (isChecked) {
dispatch('startFolderWatcher', null)
return
}
dispatch('stopFolderWatcher', null)
}
torrentsFolderPathSelector () {
const torrentsFolderPath = this.props.state.saved.prefs.torrentsFolderPath
return (
<Preference>
<PathSelector
dialog={{
title: 'Select folder to watch for new torrents',
properties: [ 'openDirectory' ]
}}
displayValue={torrentsFolderPath || ''}
onChange={this.handletorrentsFolderPathChange}
title='Folder to watch'
value={torrentsFolderPath ? path.dirname(torrentsFolderPath) : null} />
</Preference>
)
}
handletorrentsFolderPathChange (filePath) {
dispatch('updatePreferences', 'torrentsFolderPath', filePath)
}
setDefaultAppButton () { setDefaultAppButton () {
const isFileHandler = this.props.state.saved.prefs.isFileHandler const isFileHandler = this.props.state.unsaved.prefs.isFileHandler
if (isFileHandler) { if (isFileHandler) {
return ( return (
<Preference> <Preference>
@@ -195,7 +142,7 @@ class PreferencesPage extends React.Component {
<Preference> <Preference>
<Checkbox <Checkbox
className='control' className='control'
checked={this.props.state.saved.prefs.startup} checked={this.props.state.unsaved.prefs.startup}
label={'Open WebTorrent on startup.'} label={'Open WebTorrent on startup.'}
onCheck={this.handleStartupChange} onCheck={this.handleStartupChange}
/> />
@@ -216,10 +163,8 @@ class PreferencesPage extends React.Component {
} }
return ( return (
<div style={style}> <div style={style}>
<PreferencesSection title='Folders'> <PreferencesSection title='Downloads'>
{this.downloadPathSelector()} {this.downloadPathSelector()}
{this.autoAddTorrentsCheckbox()}
{this.torrentsFolderPathSelector()}
</PreferencesSection> </PreferencesSection>
<PreferencesSection title='Playback'> <PreferencesSection title='Playback'>
{this.openExternalPlayerCheckbox()} {this.openExternalPlayerCheckbox()}

View File

@@ -6,7 +6,7 @@ const LinearProgress = require('material-ui/LinearProgress').default
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, dispatch } = require('../lib/dispatcher') const {dispatcher} = require('../lib/dispatcher')
module.exports = class TorrentList extends React.Component { module.exports = class TorrentList extends React.Component {
render () { render () {
@@ -216,7 +216,7 @@ module.exports = class TorrentList extends React.Component {
} else { // torrentSummary.status is 'new' or something unexpected } else { // torrentSummary.status is 'new' or something unexpected
status = '' status = ''
} }
return (<span key='torrent-status'>{status}</span>) return (<span>{status}</span>)
} }
} }
@@ -365,8 +365,8 @@ module.exports = class TorrentList extends React.Component {
renderRadialProgressBar (fraction, cssClass) { renderRadialProgressBar (fraction, cssClass) {
const rotation = 360 * fraction const rotation = 360 * fraction
const transformFill = { transform: 'rotate(' + (rotation / 2) + 'deg)' } const transformFill = {transform: 'rotate(' + (rotation / 2) + 'deg)'}
const transformFix = { transform: 'rotate(' + rotation + 'deg)' } const transformFix = {transform: 'rotate(' + rotation + 'deg)'}
return ( return (
<div key='radial-progress' className={'radial-progress ' + cssClass}> <div key='radial-progress' className={'radial-progress ' + cssClass}>
@@ -394,20 +394,10 @@ function getErrorMessage (torrentSummary) {
if (err === 'path-missing') { if (err === 'path-missing') {
return ( return (
<span> <span>
Torrent data missing.<br /> Path missing.<br />
Fix or click to Fix and restart the app, or delete the torrent.
<a onClick={(e) => {
e.stopPropagation()
restartTorrent(torrentSummary)
}}>re-download torrent</a>.
</span> </span>
) )
} }
return 'Error' return 'Error'
} }
function restartTorrent (summary) {
delete summary.path
delete summary.error
dispatch('startTorrentingSummary', summary.torrentKey)
}

287
src/renderer/plugins.js Normal file
View File

@@ -0,0 +1,287 @@
const {resolve, basename} = require('path')
const notifier = require('node-notifier')
const crypto = require('crypto')
const config = require('../config')
module.exports = class Plugins {
constructor () {
// modules path
this.path = resolve(config.getConfigPath(), 'plugins')
log('Path: ', this.path)
this.availableExtensions = [
'onCheckForSubtitles'
]
this.forceUpdate = false
this.updating = false
this.watchers = []
this.state = {}
}
init (params) {
this.state = params.state
// initialize state
this.state.saved = Object.assign(this.state.saved || {})
// caches
this.plugins = config.getPlugins()
this.paths = this.getPaths(this.plugins)
this.id = this.getId(this.plugins)
this.modules = this.requirePlugins()
// TODO: fire an event when plugins finish updating and listen to that.
// Listen to plugin changes on config.
// New plugins added, plugins removed or updated.
// The actual plugin update action will take place in the MAIN process.
config.subscribe(() => {
const plugins = config.getPlugins()
if (plugins !== this.plugins) {
const id = this.getId(plugins)
if (this.id !== id) {
log('UPDATING...')
this.id = id
this.plugins = plugins
this.paths = this.getPaths(this.plugins)
}
}
})
// Plugins will be updated on the MAIN process after 5s.
if (this.needsUpdate()) {
setTimeout(() => {
this.init(params)
}, 6000)
log('Plugins need update, init scheduled')
return
}
this.loadPlugins()
this.initPlugins(params)
}
initPlugins (params) {
this.modules.forEach(plugin => {
if (plugin.init) {
plugin.init(params)
}
})
}
on (action, params) {
log(`ON ${action}:`, params)
this.modules.forEach(plugin => {
const actionName = this.capitalizeFirstLetter(action)
const methodName = `on${actionName}`
if (typeof plugin[methodName] === 'function') {
plugin[methodName](params)
}
})
}
capitalizeFirstLetter (string) {
return string.charAt(0).toUpperCase() + string.slice(1)
}
didPluginsChange () {
return this.state.saved.installedPlugins !== this.id
}
hasPlugins () {
return !this.isEmptyObject(this.plugins)
}
isFirstInstall () {
return (!this.state.saved.installedPlugins && this.hasPlugins())
}
needsUpdate () {
return (this.didPluginsChange() || this.isFirstInstall())
}
getId (plugins) {
const hash = crypto.createHash('sha256')
hash.update(JSON.stringify(plugins))
return hash.digest('hex')
}
loadPlugins (err, localOnly = false) {
this.updating = false
// handle errors first
if (err) {
console.error(err.stack)
if (/not a recognized/.test(err.message) || /command not found/.test(err.message)) {
this.alert(
'Error updating plugins: We could not find the "npm" command. Make sure it\'s in $PATH'
)
return
}
this.alert(`Error updating plugins: Check '${this.path}/npm-debug.log' for more information.`)
return
}
// update state with latest plugins
this.state.saved.plugins = this.plugins
// cache modules
this.modules = this.requirePlugins()
// clear require cache
this.clearCache()
}
getPluginVersions () {
const paths_ = this.paths.plugins
return paths_.map(path => {
let version = null
try {
// eslint-disable-next-line import/no-dynamic-require
version = require(resolve(path, 'package.json')).version
} catch (err) { }
return [
basename(path),
version
]
})
}
clearCache () {
// clear require cache
for (const entry in require.cache) {
if (entry.indexOf(this.path) === 0) {
delete require.cache[entry]
}
}
}
isEmptyObject (obj) {
return (Object.keys(obj).length === 0)
}
alert (message) {
notifier.notify({
title: 'WebTorrent Plugins',
// icon: config.icon, // TODO: save icon in webtorrent local folder and set config.icon
message: message
})
}
subscribe (fn) {
this.watchers.push(fn)
return () => {
this.watchers.splice(this.watchers.indexOf(fn), 1)
}
}
isLocalPath (string) {
// matches unix and windows local paths
return string.match(/^(\/|[a-z]:\/)/i)
}
getPaths (plugins) {
const pluginNames = Object.keys(plugins)
return {
plugins: pluginNames.map(name => {
let url = plugins[name]
// plugin is already on a local folder
// directly load it from its current location
if (this.isLocalPath(url)) return url
// plugin will be installed with npm install from a remote url
return resolve(this.path, 'node_modules', name.split('#')[0])
})
}
}
exposesSupportedApi (plugin) {
if (!plugin) return false
return this.availableExtensions.some((methodName) => {
return (typeof plugin[methodName] === 'function')
})
}
requirePlugins () {
const {plugins} = this.paths
let installNeeded = false
const load = (path) => {
if (!path.match(/\/$/)) {
path += '/'
}
const rendererPath = `${path}renderer.js`
try {
const Plugin = require(rendererPath) // eslint-disable import/no-dynamic-require
const plugin = new Plugin()
const exposed = this.exposesSupportedApi(plugin)
if (!exposed) {
log('Plugin not exposing any available extensions.', rendererPath, Object.keys(plugin))
return
}
// populate the name for internal errors here
plugin._name = basename(rendererPath)
return plugin
} catch (err) {
log('Require plugins ERROR:', err)
this.alert(`Error loading plugin: ${rendererPath}`)
// plugin not installed
// node_modules removed? did a manual plugin uninstall?
// try installing and then loading if successfull
installNeeded = true
}
}
// Plugin installation happens on the MAIN process.
// If plugins haven't finished installing, wait for them.
if (installNeeded) {
log('Plugins install needed, wait...')
setTimeout(() => {
this.requirePlugins()
}, 3000)
}
return plugins.map(load).filter(v => Boolean(v))
}
// decorates the base object by calling plugin[key]
// for all the available plugins
decorateObject (base, key) {
let decorated = base
this.modules.forEach(plugin => {
if (plugin[key]) {
const res = plugin[key](decorated)
if (res && typeof res === 'object') {
decorated = res
} else {
this.alert(`Plugin error: "${plugin._name}": invalid return type for \`${key}\``)
}
}
})
return decorated
}
}
/**
* Logs passed arguments to console using a prefix.
*
*/
function log () {
const prefix = '[ PLUGINS.Renderer ]-->'
const args = [prefix]
for (var i = 0; i < arguments.length; ++i) {
args.push(arguments[i])
}
console.log.apply(console, args)
}

View File

@@ -8,7 +8,7 @@ const defaultAnnounceList = require('create-torrent').announceList
const electron = require('electron') const electron = require('electron')
const fs = require('fs') const fs = require('fs')
const mkdirp = require('mkdirp') const mkdirp = require('mkdirp')
const mm = require('music-metadata') const musicmetadata = require('musicmetadata')
const networkAddress = require('network-address') const networkAddress = require('network-address')
const path = require('path') const path = require('path')
const WebTorrent = require('webtorrent') const WebTorrent = require('webtorrent')
@@ -16,7 +16,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 {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
@@ -97,7 +97,7 @@ function init () {
ipc.send('ipcReadyWebTorrent') ipc.send('ipcReadyWebTorrent')
window.addEventListener('error', (e) => window.addEventListener('error', (e) =>
ipc.send('wt-uncaught-error', { message: e.error.message, stack: e.error.stack }), ipc.send('wt-uncaught-error', {message: e.error.message, stack: e.error.stack}),
true) true)
setInterval(updateTorrentProgress, 1000) setInterval(updateTorrentProgress, 1000)
@@ -251,7 +251,7 @@ function generateTorrentPoster (torrentKey) {
function updateTorrentProgress () { function updateTorrentProgress () {
const progress = getTorrentProgress() const progress = getTorrentProgress()
// TODO: diff torrent-by-torrent, not once for the whole update // TODO: diff torrent-by-torrent, not once for the whole update
if (prevProgress && deepEqual(progress, prevProgress, { strict: true })) { if (prevProgress && deepEqual(progress, prevProgress, {strict: true})) {
return /* don't send heavy object if it hasn't changed */ return /* don't send heavy object if it hasn't changed */
} }
ipc.send('wt-progress', progress) ipc.send('wt-progress', progress)
@@ -320,8 +320,7 @@ function startServerFromReadyTorrent (torrent, cb) {
const info = { const info = {
torrentKey: torrent.key, torrentKey: torrent.key,
localURL: 'http://localhost' + urlSuffix, localURL: 'http://localhost' + urlSuffix,
networkURL: 'http://' + networkAddress() + urlSuffix, networkURL: 'http://' + networkAddress() + urlSuffix
networkAddress: networkAddress()
} }
ipc.send('wt-server-running', info) ipc.send('wt-server-running', info)
@@ -335,33 +334,15 @@ function stopServer () {
server = null server = null
} }
console.log('Initializing...')
function getAudioMetadata (infoHash, index) { function getAudioMetadata (infoHash, index) {
const torrent = client.get(infoHash) const torrent = client.get(infoHash)
const file = torrent.files[index] const file = torrent.files[index]
musicmetadata(file.createReadStream(), function (err, info) {
// Set initial matadata to display the filename first. if (err) return console.log('error getting audio metadata for ' + infoHash + ':' + index, err)
const metadata = { title: file.name } const { artist, album, albumartist, title, year, track, disk, genre } = info
ipc.send('wt-audio-metadata', infoHash, index, metadata) const importantInfo = { artist, album, albumartist, title, year, track, disk, genre }
console.log('got audio metadata for %s: %o', file.name, importantInfo)
const options = { native: false, ipc.send('wt-audio-metadata', infoHash, index, importantInfo)
skipCovers: true,
fileSize: file.length,
observer: event => {
ipc.send('wt-audio-metadata', infoHash, index, event.metadata)
} }
const onMetaData = file.done
// If completed; use direct file access
? mm.parseFile(path.join(torrent.path, file.path), options)
// otherwise stream
: mm.parseStream(file.createReadStream(), file.name, options)
onMetaData
.then(() => {
console.log(`metadata for file='${file.name}' completed.`)
}).catch(function (err) {
return console.log('error getting audio metadata for ' + infoHash + ':' + index, err)
}) })
} }

View File

@@ -615,7 +615,6 @@ body.drag .app::after {
font-size: 13px; font-size: 13px;
margin: 9px 8px 8px 8px; margin: 9px 8px 8px 8px;
opacity: 0.8; opacity: 0.8;
font-variant-numeric: tabular-nums;
} }
.player .controls .icon.closed-caption { .player .controls .icon.closed-caption {
@@ -801,23 +800,6 @@ video::-webkit-media-text-track-container {
width: 40px; width: 40px;
height: 40px; height: 40px;
margin: 40px auto; margin: 40px auto;
border-top: 6px solid rgba(255, 255, 255, 0.2);
border-right: 6px solid rgba(255, 255, 255, 0.2);
border-bottom: 6px solid rgba(255, 255, 255, 0.2);
border-left: 6px solid #ffffff;
border-radius: 50%;
color: transparent;
animation: spinning 1.1s infinite linear;
}
@keyframes spinning {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
} }
.media-stalled .loading-status { .media-stalled .loading-status {
@@ -837,17 +819,12 @@ video::-webkit-media-text-track-container {
.audio-metadata label { .audio-metadata label {
display:inline-block; display:inline-block;
width: 120px; width: 100px;
text-align: right; text-align: right;
font-weight: normal; font-weight: normal;
margin-right: 25px; margin-right: 25px;
} }
.audio-metadata .audio-format,
.audio-metadata .audio-comments {
font-weight: normal;
}
/* /*
* ERRORS * ERRORS
*/ */

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB

After

Width:  |  Height:  |  Size: 1011 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 142 KiB

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 359 KiB

After

Width:  |  Height:  |  Size: 302 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 705 KiB

After

Width:  |  Height:  |  Size: 698 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 511 KiB

After

Width:  |  Height:  |  Size: 479 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 514 KiB

After

Width:  |  Height:  |  Size: 480 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 514 KiB

After

Width:  |  Height:  |  Size: 480 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 514 KiB

After

Width:  |  Height:  |  Size: 480 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 515 KiB

After

Width:  |  Height:  |  Size: 481 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 134 KiB

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 627 KiB

After

Width:  |  Height:  |  Size: 627 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 870 KiB

After

Width:  |  Height:  |  Size: 844 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1014 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 559 KiB

After

Width:  |  Height:  |  Size: 549 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 705 KiB

After

Width:  |  Height:  |  Size: 698 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB

After

Width:  |  Height:  |  Size: 1024 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1014 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB

After

Width:  |  Height:  |  Size: 1016 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 237 KiB

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 239 KiB

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 238 KiB

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 237 KiB

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 725 KiB

After

Width:  |  Height:  |  Size: 406 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 239 KiB

After

Width:  |  Height:  |  Size: 158 KiB

View File

@@ -32,7 +32,7 @@ function createApp (t) {
path: path.join(__dirname, '..', 'node_modules', '.bin', path: path.join(__dirname, '..', 'node_modules', '.bin',
'electron' + (process.platform === 'win32' ? '.cmd' : '')), 'electron' + (process.platform === 'win32' ? '.cmd' : '')),
args: ['-r', path.join(__dirname, 'mocks.js'), path.join(__dirname, '..')], args: ['-r', path.join(__dirname, 'mocks.js'), path.join(__dirname, '..')],
env: { NODE_ENV: 'test' }, env: {NODE_ENV: 'test'},
waitTimeout: 10e3 waitTimeout: 10e3
}) })
} }

View File

@@ -40,9 +40,10 @@ test('create-torrent', function (t) {
announce: [ announce: [
'udp://explodie.org:6969', 'udp://explodie.org:6969',
'udp://tracker.coppersurfer.tk:6969', 'udp://tracker.coppersurfer.tk:6969',
'udp://tracker.empire-js.us:1337',
'udp://tracker.leechers-paradise.org:6969', 'udp://tracker.leechers-paradise.org:6969',
'udp://tracker.openbittorrent.com:80',
'udp://tracker.opentrackr.org:1337', 'udp://tracker.opentrackr.org:1337',
'udp://zer0day.ch:1337',
'wss://tracker.btorrent.xyz', 'wss://tracker.btorrent.xyz',
'wss://tracker.fastcast.nz', 'wss://tracker.fastcast.nz',
'wss://tracker.openwebtorrent.com' 'wss://tracker.openwebtorrent.com'

View File

@@ -6,7 +6,7 @@ test('audio-streaming', function (t) {
t.timeoutAfter(60e3) t.timeoutAfter(60e3)
const app = setup.createApp() const app = setup.createApp()
setup.waitForLoad(app, t, { online: true }) setup.waitForLoad(app, t, {online: true})
.then(() => app.client.waitUntilTextExists('.torrent-list', 'Big Buck Bunny')) .then(() => app.client.waitUntilTextExists('.torrent-list', 'Big Buck Bunny'))
// Play Wired CD. Wait for it to start streaming. // Play Wired CD. Wait for it to start streaming.
.then(() => app.client.moveToObject('#torrent-wired')) .then(() => app.client.moveToObject('#torrent-wired'))

View File

@@ -1,25 +0,0 @@
const test = require('tape')
const fs = require('fs')
const path = require('path')
const WebTorrent = require('webtorrent')
const torrentPoster = require('../build/renderer/lib/torrent-poster')
const client = new WebTorrent()
test("get cover from: 'wiredCd.torrent'", (t) => {
const torrentPath = path.join(__dirname, '..', 'static', 'wiredCd.torrent')
const torrentData = fs.readFileSync(torrentPath)
client.add(torrentData, (torrent) => {
torrentPoster(torrent, (err, buf, extension) => {
if (err) {
t.fail(err)
} else {
t.equals(extension, '.jpg')
t.end()
}
})
})
})

View File

@@ -31,20 +31,22 @@ test('torrent-list: start, stop, and delete torrents', function (t) {
const app = setup.createApp() const app = setup.createApp()
setup.waitForLoad(app, t) setup.waitForLoad(app, t)
.then(() => app.client.waitUntilTextExists('.torrent-list', 'Big Buck Bunny')) .then(() => app.client.waitUntilTextExists('.torrent-list', 'Big Buck Bunny'))
.then(() => setup.screenshotCreateOrCompare(app, t, 'torrent-list')) // Mouse over the first torrent
.then(() => app.client.moveToObject('.torrent'))
.then(() => setup.screenshotCreateOrCompare(app, t, 'torrent-list-hover'))
// Click download on the first torrent, start downloading // Click download on the first torrent, start downloading
.then(() => app.client.click('.download input')) .then(() => app.client.click('.download input'))
.then(() => app.client.waitUntilTextExists('.torrent-list', '276 MB')) .then(() => app.client.waitUntilTextExists('.torrent-list', '276 MB'))
.then(() => setup.screenshotCreateOrCompare(app, t, 'torrent-list-start-download')) .then(() => setup.screenshotCreateOrCompare(app, t, 'torrent-list-start-download'))
// Click download on the first torrent again, stop downloading // Click download on the first torrent again, stop downloading
.then(() => app.client.click('.download input')) .then(() => app.client.click('.download input'))
.then(() => setup.screenshotCreateOrCompare(app, t, 'torrent-list-download')) .then(() => setup.screenshotCreateOrCompare(app, t, 'torrent-list-hover-download'))
// Click delete on the first torrent // Click delete on the first torrent
.then(() => app.client.click('.icon.delete')) .then(() => app.client.click('.icon.delete'))
.then(() => setup.screenshotCreateOrCompare(app, t, 'torrent-list-delete-prompt')) .then(() => setup.screenshotCreateOrCompare(app, t, 'torrent-list-delete-prompt'))
// Click cancel on the resulting confirmation dialog. Should be same as before. // Click cancel on the resulting confirmation dialog. Should be same as before.
.then(() => app.client.click('.control.cancel')) .then(() => app.client.click('.control.cancel'))
.then(() => setup.screenshotCreateOrCompare(app, t, 'torrent-list')) .then(() => setup.screenshotCreateOrCompare(app, t, 'torrent-list-hover'))
// Click delete on the first torrent again // Click delete on the first torrent again
.then(() => app.client.click('.icon.delete')) .then(() => app.client.click('.icon.delete'))
.then(() => setup.screenshotCreateOrCompare(app, t, 'torrent-list-delete-prompt')) .then(() => setup.screenshotCreateOrCompare(app, t, 'torrent-list-delete-prompt'))
@@ -61,6 +63,9 @@ test('torrent-list: expand torrent, unselect file', function (t) {
const app = setup.createApp() const app = setup.createApp()
setup.waitForLoad(app, t) setup.waitForLoad(app, t)
.then(() => app.client.waitUntilTextExists('.torrent-list', 'Big Buck Bunny')) .then(() => app.client.waitUntilTextExists('.torrent-list', 'Big Buck Bunny'))
// Mouse over the torrent
.then(() => app.client.moveToObject('#torrent-cosmos'))
.then(() => setup.screenshotCreateOrCompare(app, t, 'torrent-list-cosmos-hover'))
// Click on the torrent, expand // Click on the torrent, expand
.then(() => app.client.click('#torrent-cosmos')) .then(() => app.client.click('#torrent-cosmos'))
.then(() => setup.screenshotCreateOrCompare(app, t, 'torrent-list-cosmos-expand')) .then(() => setup.screenshotCreateOrCompare(app, t, 'torrent-list-cosmos-expand'))

View File

@@ -6,7 +6,7 @@ test('video-streaming', function (t) {
t.timeoutAfter(30e3) t.timeoutAfter(30e3)
const app = setup.createApp() const app = setup.createApp()
setup.waitForLoad(app, t, { online: true }) setup.waitForLoad(app, t, {online: true})
.then(() => app.client.waitUntilTextExists('.torrent-list', 'Big Buck Bunny')) .then(() => app.client.waitUntilTextExists('.torrent-list', 'Big Buck Bunny'))
// Play Big Buck Bunny. Wait for it to start streaming. // Play Big Buck Bunny. Wait for it to start streaming.
.then(() => app.client.moveToObject('.torrent')) .then(() => app.client.moveToObject('.torrent'))