Compare commits
86 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3cfdf857cf | ||
|
|
c59abb251b | ||
|
|
2fc1034cc5 | ||
|
|
a8fd60f46e | ||
|
|
0cbae6b4d5 | ||
|
|
d0733d3370 | ||
|
|
7b8751312a | ||
|
|
6d664f2086 | ||
|
|
4ebf7e25b7 | ||
|
|
54e70e7158 | ||
|
|
b950829de3 | ||
|
|
a489397f84 | ||
|
|
897dac354d | ||
|
|
beb4af1311 | ||
|
|
f0aeab0207 | ||
|
|
be1314422d | ||
|
|
c15711aae8 | ||
|
|
1668c4c614 | ||
|
|
7050ee849b | ||
|
|
dfe1e3b631 | ||
|
|
50c47dd657 | ||
|
|
a373141a93 | ||
|
|
24f5856649 | ||
|
|
f85e0a61b1 | ||
|
|
4319ef2853 | ||
|
|
c3a27dbebe | ||
|
|
bac43509d2 | ||
|
|
59b012e527 | ||
|
|
c615e285db | ||
|
|
1aca9fe753 | ||
|
|
349c5ee22e | ||
|
|
c44943cef7 | ||
|
|
7a61b52d64 | ||
|
|
e5df96c82e | ||
|
|
770327c3fa | ||
|
|
4bdc6e3d65 | ||
|
|
4799a032e5 | ||
|
|
b2d2a6a7a5 | ||
|
|
7676106914 | ||
|
|
fe5ea31f2c | ||
|
|
e34223fc94 | ||
|
|
15f733f11c | ||
|
|
7526b18507 | ||
|
|
0af6007632 | ||
|
|
1bc3cd1d51 | ||
|
|
92bafd695d | ||
|
|
78a2ee4e85 | ||
|
|
8b9346d767 | ||
|
|
06d3bd3f93 | ||
|
|
1af7e4ef19 | ||
|
|
8e64e4120b | ||
|
|
b983559763 | ||
|
|
e62527de23 | ||
|
|
1f51f35f8e | ||
|
|
c3686417e3 | ||
|
|
746e10c025 | ||
|
|
98389fc07c | ||
|
|
aaebf93db4 | ||
|
|
8f03ecedaa | ||
|
|
db20bd8eaf | ||
|
|
12500dfb64 | ||
|
|
acc8e7923a | ||
|
|
9aa5775528 | ||
|
|
2a2d71289a | ||
|
|
ae28e34fd5 | ||
|
|
6b175e7d40 | ||
|
|
2c6d74e8ef | ||
|
|
3b832595fe | ||
|
|
bf372029fb | ||
|
|
17ce7e519c | ||
|
|
1f6a112df7 | ||
|
|
9d3e26f15a | ||
|
|
8a95895254 | ||
|
|
5d71f9e9c6 | ||
|
|
0ec6fb5a93 | ||
|
|
5d410457ce | ||
|
|
c6cd21b8ff | ||
|
|
2235b2fa82 | ||
|
|
65e0b5d6e7 | ||
|
|
ea64411570 | ||
|
|
9348c61a84 | ||
|
|
d9aa3822ee | ||
|
|
e86bd26800 | ||
|
|
6d8cec17de | ||
|
|
572f084570 | ||
|
|
4a3ca5459d |
@@ -22,5 +22,9 @@
|
|||||||
- Mathias Rasmussen <mathiasvr@gmail.com>
|
- Mathias Rasmussen <mathiasvr@gmail.com>
|
||||||
- Sergey Bargamon <sergey@bargamon.ru>
|
- Sergey Bargamon <sergey@bargamon.ru>
|
||||||
- Thomas Watson Steen <w@tson.dk>
|
- Thomas Watson Steen <w@tson.dk>
|
||||||
|
- anonymlol <anonymlol7@gmail.com>
|
||||||
|
- Gediminas Petrikas <gedas18@gmail.com>
|
||||||
|
- Adam Gotlib <gotlib.adam+dev@gmail.com>
|
||||||
|
- Rémi Jouannet <remijouannet@gmail.com>
|
||||||
|
|
||||||
#### Generated by bin/update-authors.sh.
|
#### Generated by bin/update-authors.sh.
|
||||||
|
|||||||
62
CHANGELOG.md
62
CHANGELOG.md
@@ -1,5 +1,67 @@
|
|||||||
# WebTorrent Desktop Version History
|
# WebTorrent Desktop Version History
|
||||||
|
|
||||||
|
## v0.9.0 - 2016-07-20
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Save selected subtitles
|
||||||
|
- Ask for confirmation before deleting torrents
|
||||||
|
- Support Debian Jessie
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Only send telemetry in production
|
||||||
|
- Clean up the code. Split main.js, refactor lots of things
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Fix state.playing.jumpToTime behavior
|
||||||
|
- Remove torrent file and poster image when deleting a torrent
|
||||||
|
|
||||||
|
## v0.8.1 - 2016-06-24
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- New URI handler: stream-magnet
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- DLNA crashing bug
|
||||||
|
|
||||||
|
## v0.8.0 - 2016-06-23
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Cast menu: choose which Chromecast, Airplay, or DLNA device you want to use
|
||||||
|
- Telemetry: send basic data, plus stats on how often the play button works
|
||||||
|
- Make posters from jpeg files, not just jpg
|
||||||
|
- Support .wmv video via Play in VLC
|
||||||
|
- Windows thumbnail bar with a play/pause button
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Nicer modal styles
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Windows tray icon now stays in the right state
|
||||||
|
|
||||||
|
## v0.7.2 - 2016-06-02
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix exception that affects users upgrading from v0.5.1 or older
|
||||||
|
- Ensure `state.saved.prefs` configuration exists
|
||||||
|
- Fix window title on "About WebTorrent" window
|
||||||
|
|
||||||
|
## v0.7.1 - 2016-06-02
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Change "Step Forward" keyboard shortcut to `Alt+Left` (Windows)
|
||||||
|
- Change "Step Backward" keyboard shortcut to to `Alt+Right` (Windows)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- First time startup bug -- invalid torrent/poster paths
|
||||||
|
|
||||||
## v0.7.0 - 2016-06-02
|
## v0.7.0 - 2016-06-02
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -81,6 +81,12 @@ brew install wine
|
|||||||
|
|
||||||
(Requires the [Homebrew](http://brew.sh/) package manager.)
|
(Requires the [Homebrew](http://brew.sh/) package manager.)
|
||||||
|
|
||||||
|
### Privacy
|
||||||
|
|
||||||
|
WebTorrent Desktop collects some basic usage stats to help us make the app better. For example, we track how well the play button works. How often does it succeed? Time out? Show a missing codec error?
|
||||||
|
|
||||||
|
The app never sends personally identifying information, nor does it track which swarms you join.
|
||||||
|
|
||||||
### Code Style
|
### Code Style
|
||||||
|
|
||||||
[](https://github.com/feross/standard)
|
[](https://github.com/feross/standard)
|
||||||
|
|||||||
@@ -3,7 +3,48 @@
|
|||||||
var fs = require('fs')
|
var fs = require('fs')
|
||||||
var cp = require('child_process')
|
var cp = require('child_process')
|
||||||
|
|
||||||
var BUILT_IN_DEPS = ['child_process', 'electron', 'fs', 'os', 'path']
|
// We can't use `builtin-modules` here since our TravisCI
|
||||||
|
// setup expects this file to run with no dependencies
|
||||||
|
var BUILT_IN_NODE_MODULES = [
|
||||||
|
'assert',
|
||||||
|
'buffer',
|
||||||
|
'child_process',
|
||||||
|
'cluster',
|
||||||
|
'console',
|
||||||
|
'constants',
|
||||||
|
'crypto',
|
||||||
|
'dgram',
|
||||||
|
'dns',
|
||||||
|
'domain',
|
||||||
|
'events',
|
||||||
|
'fs',
|
||||||
|
'http',
|
||||||
|
'https',
|
||||||
|
'module',
|
||||||
|
'net',
|
||||||
|
'os',
|
||||||
|
'path',
|
||||||
|
'process',
|
||||||
|
'punycode',
|
||||||
|
'querystring',
|
||||||
|
'readline',
|
||||||
|
'repl',
|
||||||
|
'stream',
|
||||||
|
'string_decoder',
|
||||||
|
'timers',
|
||||||
|
'tls',
|
||||||
|
'tty',
|
||||||
|
'url',
|
||||||
|
'util',
|
||||||
|
'v8',
|
||||||
|
'vm',
|
||||||
|
'zlib'
|
||||||
|
]
|
||||||
|
|
||||||
|
var BUILT_IN_ELECTRON_MODULES = [ 'electron' ]
|
||||||
|
|
||||||
|
var BUILT_IN_DEPS = [].concat(BUILT_IN_NODE_MODULES, BUILT_IN_ELECTRON_MODULES)
|
||||||
|
|
||||||
var EXECUTABLE_DEPS = ['gh-release', 'standard']
|
var EXECUTABLE_DEPS = ['gh-release', 'standard']
|
||||||
|
|
||||||
main()
|
main()
|
||||||
@@ -19,10 +60,10 @@ function main () {
|
|||||||
var packageDeps = findPackageDeps()
|
var packageDeps = findPackageDeps()
|
||||||
|
|
||||||
var missingDeps = usedDeps.filter(
|
var missingDeps = usedDeps.filter(
|
||||||
(dep) => !packageDeps.includes(dep) && !BUILT_IN_DEPS.includes(dep)
|
(dep) => !includes(packageDeps, dep) && !includes(BUILT_IN_DEPS, dep)
|
||||||
)
|
)
|
||||||
var unusedDeps = packageDeps.filter(
|
var unusedDeps = packageDeps.filter(
|
||||||
(dep) => !usedDeps.includes(dep) && !EXECUTABLE_DEPS.includes(dep)
|
(dep) => !includes(usedDeps, dep) && !includes(EXECUTABLE_DEPS, dep)
|
||||||
)
|
)
|
||||||
|
|
||||||
if (missingDeps.length > 0) {
|
if (missingDeps.length > 0) {
|
||||||
@@ -52,3 +93,7 @@ function findUsedDeps () {
|
|||||||
var stdout = cp.execSync('./bin/list-deps.sh')
|
var stdout = cp.execSync('./bin/list-deps.sh')
|
||||||
return stdout.toString().trim().split('\n')
|
return stdout.toString().trim().split('\n')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function includes (arr, elem) {
|
||||||
|
return arr.indexOf(elem) >= 0
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,7 +2,5 @@
|
|||||||
|
|
||||||
var config = require('../config')
|
var config = require('../config')
|
||||||
var open = require('open')
|
var open = require('open')
|
||||||
var path = require('path')
|
|
||||||
|
|
||||||
var configPath = path.join(config.CONFIG_PATH, 'config.json')
|
open(config.CONFIG_PATH)
|
||||||
open(configPath)
|
|
||||||
|
|||||||
@@ -206,6 +206,12 @@ function buildDarwin (cb) {
|
|||||||
CFBundleURLIconFile: path.basename(config.APP_FILE_ICON) + '.icns',
|
CFBundleURLIconFile: path.basename(config.APP_FILE_ICON) + '.icns',
|
||||||
CFBundleURLName: 'BitTorrent Magnet URL',
|
CFBundleURLName: 'BitTorrent Magnet URL',
|
||||||
CFBundleURLSchemes: [ 'magnet' ]
|
CFBundleURLSchemes: [ 'magnet' ]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
CFBundleTypeRole: 'Editor',
|
||||||
|
CFBundleURLIconFile: path.basename(config.APP_FILE_ICON) + '.icns',
|
||||||
|
CFBundleURLName: 'BitTorrent Stream-Magnet URL',
|
||||||
|
CFBundleURLSchemes: [ 'stream-magnet' ]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -460,7 +466,7 @@ function buildLinux (cb) {
|
|||||||
info: {
|
info: {
|
||||||
arch: destArch === 'x64' ? 'amd64' : 'i386',
|
arch: destArch === 'x64' ? 'amd64' : 'i386',
|
||||||
targetDir: DIST_PATH,
|
targetDir: DIST_PATH,
|
||||||
depends: 'libc6 (>= 2.4)',
|
depends: 'gconf2, libgtk2.0-0, libnss3, libxss1',
|
||||||
scripts: {
|
scripts: {
|
||||||
postinst: path.join(config.STATIC_PATH, 'linux', 'postinst'),
|
postinst: path.join(config.STATIC_PATH, 'linux', 'postinst'),
|
||||||
prerm: path.join(config.STATIC_PATH, 'linux', 'prerm')
|
prerm: path.join(config.STATIC_PATH, 'linux', 'prerm')
|
||||||
|
|||||||
57
config.js
57
config.js
@@ -10,6 +10,9 @@ var PORTABLE_PATH = path.join(path.dirname(process.execPath), 'Portable Settings
|
|||||||
|
|
||||||
module.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',
|
||||||
|
CRASH_REPORT_URL: 'https://webtorrent.io/desktop/crash-report',
|
||||||
|
TELEMETRY_URL: 'https://webtorrent.io/desktop/telemetry',
|
||||||
|
|
||||||
APP_COPYRIGHT: 'Copyright © 2014-2016 ' + APP_TEAM,
|
APP_COPYRIGHT: 'Copyright © 2014-2016 ' + APP_TEAM,
|
||||||
APP_FILE_ICON: path.join(__dirname, 'static', 'WebTorrentFile'),
|
APP_FILE_ICON: path.join(__dirname, 'static', 'WebTorrentFile'),
|
||||||
@@ -19,16 +22,40 @@ module.exports = {
|
|||||||
APP_VERSION: APP_VERSION,
|
APP_VERSION: APP_VERSION,
|
||||||
APP_WINDOW_TITLE: APP_NAME + ' (BETA)',
|
APP_WINDOW_TITLE: APP_NAME + ' (BETA)',
|
||||||
|
|
||||||
AUTO_UPDATE_URL: 'https://webtorrent.io/desktop/update',
|
|
||||||
|
|
||||||
CRASH_REPORT_URL: 'https://webtorrent.io/desktop/crash-report',
|
|
||||||
|
|
||||||
CONFIG_PATH: getConfigPath(),
|
CONFIG_PATH: getConfigPath(),
|
||||||
CONFIG_POSTER_PATH: path.join(getConfigPath(), 'Posters'),
|
|
||||||
CONFIG_TORRENT_PATH: path.join(getConfigPath(), 'Torrents'),
|
DEFAULT_TORRENTS: [
|
||||||
|
{
|
||||||
|
name: 'Big Buck Bunny',
|
||||||
|
posterFileName: 'bigBuckBunny.jpg',
|
||||||
|
torrentFileName: 'bigBuckBunny.torrent'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Cosmos Laundromat (Preview)',
|
||||||
|
posterFileName: 'cosmosLaundromat.jpg',
|
||||||
|
torrentFileName: 'cosmosLaundromat.torrent'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Sintel',
|
||||||
|
posterFileName: 'sintel.jpg',
|
||||||
|
torrentFileName: 'sintel.torrent'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Tears of Steel',
|
||||||
|
posterFileName: 'tearsOfSteel.jpg',
|
||||||
|
torrentFileName: 'tearsOfSteel.torrent'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'The WIRED CD - Rip. Sample. Mash. Share.',
|
||||||
|
posterFileName: 'wiredCd.jpg',
|
||||||
|
torrentFileName: 'wiredCd.torrent'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
DELAYED_INIT: 3000 /* 3 seconds */,
|
DELAYED_INIT: 3000 /* 3 seconds */,
|
||||||
|
|
||||||
|
DEFAULT_DOWNLOAD_PATH: getDefaultDownloadPath(),
|
||||||
|
|
||||||
GITHUB_URL: 'https://github.com/feross/webtorrent-desktop',
|
GITHUB_URL: 'https://github.com/feross/webtorrent-desktop',
|
||||||
GITHUB_URL_ISSUES: 'https://github.com/feross/webtorrent-desktop/issues',
|
GITHUB_URL_ISSUES: 'https://github.com/feross/webtorrent-desktop/issues',
|
||||||
GITHUB_URL_RAW: 'https://raw.githubusercontent.com/feross/webtorrent-desktop/master',
|
GITHUB_URL_RAW: 'https://raw.githubusercontent.com/feross/webtorrent-desktop/master',
|
||||||
@@ -38,8 +65,10 @@ module.exports = {
|
|||||||
IS_PORTABLE: isPortable(),
|
IS_PORTABLE: isPortable(),
|
||||||
IS_PRODUCTION: isProduction(),
|
IS_PRODUCTION: isProduction(),
|
||||||
|
|
||||||
|
POSTER_PATH: path.join(getConfigPath(), 'Posters'),
|
||||||
ROOT_PATH: __dirname,
|
ROOT_PATH: __dirname,
|
||||||
STATIC_PATH: path.join(__dirname, 'static'),
|
STATIC_PATH: path.join(__dirname, 'static'),
|
||||||
|
TORRENT_PATH: path.join(getConfigPath(), 'Torrents'),
|
||||||
|
|
||||||
WINDOW_ABOUT: 'file://' + path.join(__dirname, 'renderer', 'about.html'),
|
WINDOW_ABOUT: 'file://' + path.join(__dirname, 'renderer', 'about.html'),
|
||||||
WINDOW_MAIN: 'file://' + path.join(__dirname, 'renderer', 'main.html'),
|
WINDOW_MAIN: 'file://' + path.join(__dirname, 'renderer', 'main.html'),
|
||||||
@@ -57,6 +86,22 @@ function getConfigPath () {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getDefaultDownloadPath () {
|
||||||
|
if (!process || !process.type) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPortable()) {
|
||||||
|
return path.join(getConfigPath(), 'Downloads')
|
||||||
|
}
|
||||||
|
|
||||||
|
var electron = require('electron')
|
||||||
|
|
||||||
|
return process.type === 'renderer'
|
||||||
|
? electron.remote.app.getPath('downloads')
|
||||||
|
: electron.app.getPath('downloads')
|
||||||
|
}
|
||||||
|
|
||||||
function isPortable () {
|
function isPortable () {
|
||||||
try {
|
try {
|
||||||
return process.platform === 'win32' && isProduction() && !!fs.statSync(PORTABLE_PATH)
|
return process.platform === 'win32' && isProduction() && !!fs.statSync(PORTABLE_PATH)
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ function installDarwin () {
|
|||||||
// On OS X, only protocols that are listed in `Info.plist` can be set as the
|
// On OS X, only protocols that are listed in `Info.plist` can be set as the
|
||||||
// default handler at runtime.
|
// default handler at runtime.
|
||||||
app.setAsDefaultProtocolClient('magnet')
|
app.setAsDefaultProtocolClient('magnet')
|
||||||
|
app.setAsDefaultProtocolClient('stream-magnet')
|
||||||
|
|
||||||
// File handlers are defined in `Info.plist`.
|
// File handlers are defined in `Info.plist`.
|
||||||
}
|
}
|
||||||
@@ -63,6 +64,12 @@ function installWin32 () {
|
|||||||
iconPath,
|
iconPath,
|
||||||
EXEC_COMMAND
|
EXEC_COMMAND
|
||||||
)
|
)
|
||||||
|
registerProtocolHandlerWin32(
|
||||||
|
'stream-magnet',
|
||||||
|
'URL:BitTorrent Stream-Magnet URL',
|
||||||
|
iconPath,
|
||||||
|
EXEC_COMMAND
|
||||||
|
)
|
||||||
registerFileHandlerWin32(
|
registerFileHandlerWin32(
|
||||||
'.torrent',
|
'.torrent',
|
||||||
'io.webtorrent.torrent',
|
'io.webtorrent.torrent',
|
||||||
@@ -201,6 +208,7 @@ function uninstallWin32 () {
|
|||||||
var Registry = require('winreg')
|
var Registry = require('winreg')
|
||||||
|
|
||||||
unregisterProtocolHandlerWin32('magnet', EXEC_COMMAND)
|
unregisterProtocolHandlerWin32('magnet', EXEC_COMMAND)
|
||||||
|
unregisterProtocolHandlerWin32('stream-magnet', EXEC_COMMAND)
|
||||||
unregisterFileHandlerWin32('.torrent', 'io.webtorrent.torrent', EXEC_COMMAND)
|
unregisterFileHandlerWin32('.torrent', 'io.webtorrent.torrent', EXEC_COMMAND)
|
||||||
|
|
||||||
function unregisterProtocolHandlerWin32 (protocol, command) {
|
function unregisterProtocolHandlerWin32 (protocol, command) {
|
||||||
|
|||||||
@@ -68,6 +68,13 @@ function init () {
|
|||||||
|
|
||||||
// To keep app startup fast, some code is delayed.
|
// To keep app startup fast, some code is delayed.
|
||||||
setTimeout(delayedInit, config.DELAYED_INIT)
|
setTimeout(delayedInit, config.DELAYED_INIT)
|
||||||
|
|
||||||
|
// Report uncaught exceptions
|
||||||
|
process.on('uncaughtException', (err) => {
|
||||||
|
console.error(err)
|
||||||
|
var errJSON = {message: err.message, stack: err.stack}
|
||||||
|
windows.main.dispatch('uncaughtError', 'main', errJSON)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
app.once('ipcReady', function () {
|
app.once('ipcReady', function () {
|
||||||
@@ -83,7 +90,10 @@ function init () {
|
|||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
windows.main.dispatch('saveState') // try to save state on exit
|
windows.main.dispatch('saveState') // try to save state on exit
|
||||||
ipcMain.once('savedState', () => app.quit())
|
ipcMain.once('savedState', () => app.quit())
|
||||||
setTimeout(() => app.quit(), 2000) // quit after 2 secs, at most
|
setTimeout(() => {
|
||||||
|
console.error('Saving state took too long. Quitting.')
|
||||||
|
app.quit()
|
||||||
|
}, 2000) // quit after 2 secs, at most
|
||||||
})
|
})
|
||||||
|
|
||||||
app.on('activate', function () {
|
app.on('activate', function () {
|
||||||
|
|||||||
23
main/ipc.js
23
main/ipc.js
@@ -15,6 +15,7 @@ var shell = require('./shell')
|
|||||||
var shortcuts = require('./shortcuts')
|
var shortcuts = require('./shortcuts')
|
||||||
var vlc = require('./vlc')
|
var vlc = require('./vlc')
|
||||||
var windows = require('./windows')
|
var windows = require('./windows')
|
||||||
|
var thumbar = require('./thumbar')
|
||||||
|
|
||||||
// Messages from the main process, to be sent once the WebTorrent process starts
|
// Messages from the main process, to be sent once the WebTorrent process starts
|
||||||
var messageQueueMainToWebTorrent = []
|
var messageQueueMainToWebTorrent = []
|
||||||
@@ -60,20 +61,27 @@ function init () {
|
|||||||
|
|
||||||
ipc.on('onPlayerOpen', function () {
|
ipc.on('onPlayerOpen', function () {
|
||||||
menu.onPlayerOpen()
|
menu.onPlayerOpen()
|
||||||
shortcuts.onPlayerOpen()
|
powerSaveBlocker.enable()
|
||||||
|
shortcuts.enable()
|
||||||
|
thumbar.enable()
|
||||||
})
|
})
|
||||||
|
|
||||||
ipc.on('onPlayerClose', function () {
|
ipc.on('onPlayerClose', function () {
|
||||||
menu.onPlayerClose()
|
menu.onPlayerClose()
|
||||||
shortcuts.onPlayerOpen()
|
powerSaveBlocker.disable()
|
||||||
|
shortcuts.disable()
|
||||||
|
thumbar.disable()
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
ipc.on('onPlayerPlay', function () {
|
||||||
* Power Save Blocker
|
powerSaveBlocker.enable()
|
||||||
*/
|
thumbar.onPlayerPlay()
|
||||||
|
})
|
||||||
|
|
||||||
ipc.on('blockPowerSave', () => powerSaveBlocker.start())
|
ipc.on('onPlayerPause', function () {
|
||||||
ipc.on('unblockPowerSave', () => powerSaveBlocker.stop())
|
powerSaveBlocker.disable()
|
||||||
|
thumbar.onPlayerPause()
|
||||||
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shell
|
* Shell
|
||||||
@@ -81,6 +89,7 @@ function init () {
|
|||||||
|
|
||||||
ipc.on('openItem', (e, ...args) => shell.openItem(...args))
|
ipc.on('openItem', (e, ...args) => shell.openItem(...args))
|
||||||
ipc.on('showItemInFolder', (e, ...args) => shell.showItemInFolder(...args))
|
ipc.on('showItemInFolder', (e, ...args) => shell.showItemInFolder(...args))
|
||||||
|
ipc.on('moveItemToTrash', (e, ...args) => shell.moveItemToTrash(...args))
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Windows: Main
|
* Windows: Main
|
||||||
|
|||||||
12
main/menu.js
12
main/menu.js
@@ -217,14 +217,18 @@ function getMenuTemplate () {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Step Forward',
|
label: 'Step Forward',
|
||||||
accelerator: 'CmdOrCtrl+Alt+Right',
|
accelerator: process.platform === 'darwin'
|
||||||
click: () => windows.main.dispatch('skip', 1),
|
? 'CmdOrCtrl+Alt+Right'
|
||||||
|
: 'Alt+Right',
|
||||||
|
click: () => windows.main.dispatch('skip', 10),
|
||||||
enabled: false
|
enabled: false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Step Backward',
|
label: 'Step Backward',
|
||||||
accelerator: 'CmdOrCtrl+Alt+Left',
|
accelerator: process.platform === 'darwin'
|
||||||
click: () => windows.main.dispatch('skip', -1),
|
? 'CmdOrCtrl+Alt+Left'
|
||||||
|
: 'Alt+Left',
|
||||||
|
click: () => windows.main.dispatch('skip', -10),
|
||||||
enabled: false
|
enabled: false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
start,
|
enable,
|
||||||
stop
|
disable
|
||||||
}
|
}
|
||||||
|
|
||||||
var electron = require('electron')
|
var electron = require('electron')
|
||||||
@@ -12,19 +12,19 @@ var blockId = 0
|
|||||||
* Block the system from entering low-power (sleep) mode or turning off the
|
* Block the system from entering low-power (sleep) mode or turning off the
|
||||||
* display.
|
* display.
|
||||||
*/
|
*/
|
||||||
function start () {
|
function enable () {
|
||||||
stop() // Stop the previous power saver block, if one exists.
|
disable() // Stop the previous power saver block, if one exists.
|
||||||
blockId = electron.powerSaveBlocker.start('prevent-display-sleep')
|
blockId = electron.powerSaveBlocker.start('prevent-display-sleep')
|
||||||
log(`powerSaveBlocker.start: ${blockId}`)
|
log(`powerSaveBlocker.enable: ${blockId}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stop blocking the system from entering low-power mode.
|
* Stop blocking the system from entering low-power mode.
|
||||||
*/
|
*/
|
||||||
function stop () {
|
function disable () {
|
||||||
if (!electron.powerSaveBlocker.isStarted(blockId)) {
|
if (!electron.powerSaveBlocker.isStarted(blockId)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
electron.powerSaveBlocker.stop(blockId)
|
electron.powerSaveBlocker.stop(blockId)
|
||||||
log(`powerSaveBlocker.stop: ${blockId}`)
|
log(`powerSaveBlocker.disable: ${blockId}`)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
openExternal,
|
openExternal,
|
||||||
openItem,
|
openItem,
|
||||||
showItemInFolder
|
showItemInFolder,
|
||||||
|
moveItemToTrash
|
||||||
}
|
}
|
||||||
|
|
||||||
var electron = require('electron')
|
var electron = require('electron')
|
||||||
@@ -30,3 +31,11 @@ function showItemInFolder (path) {
|
|||||||
log(`showItemInFolder: ${path}`)
|
log(`showItemInFolder: ${path}`)
|
||||||
electron.shell.showItemInFolder(path)
|
electron.shell.showItemInFolder(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move the given file to trash and returns a boolean status for the operation.
|
||||||
|
*/
|
||||||
|
function moveItemToTrash (path) {
|
||||||
|
log(`moveItemToTrash: ${path}`)
|
||||||
|
electron.shell.moveItemToTrash(path)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
onPlayerClose,
|
disable,
|
||||||
onPlayerOpen
|
enable
|
||||||
}
|
}
|
||||||
|
|
||||||
var electron = require('electron')
|
var electron = require('electron')
|
||||||
var windows = require('./windows')
|
var windows = require('./windows')
|
||||||
|
|
||||||
function onPlayerOpen () {
|
function enable () {
|
||||||
// Register play/pause media key, available on some keyboards.
|
// Register play/pause media key, available on some keyboards.
|
||||||
electron.globalShortcut.register(
|
electron.globalShortcut.register(
|
||||||
'MediaPlayPause',
|
'MediaPlayPause',
|
||||||
@@ -14,7 +14,7 @@ function onPlayerOpen () {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function onPlayerClose () {
|
function disable () {
|
||||||
// Return the media key to the OS, so other apps can use it.
|
// Return the media key to the OS, so other apps can use it.
|
||||||
electron.globalShortcut.unregister('MediaPlayPause')
|
electron.globalShortcut.unregister('MediaPlayPause')
|
||||||
}
|
}
|
||||||
|
|||||||
54
main/thumbar.js
Normal file
54
main/thumbar.js
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
module.exports = {
|
||||||
|
disable,
|
||||||
|
enable,
|
||||||
|
onPlayerPause,
|
||||||
|
onPlayerPlay
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* On Windows, add a "thumbnail toolbar" with a play/pause button in the taskbar.
|
||||||
|
* This provides users a way to access play/pause functionality without restoring
|
||||||
|
* or activating the window.
|
||||||
|
*/
|
||||||
|
|
||||||
|
var path = require('path')
|
||||||
|
var config = require('../config')
|
||||||
|
|
||||||
|
var windows = require('./windows')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the Windows thumbnail toolbar buttons.
|
||||||
|
*/
|
||||||
|
function enable () {
|
||||||
|
update(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hide the Windows thumbnail toolbar buttons.
|
||||||
|
*/
|
||||||
|
function disable () {
|
||||||
|
windows.main.win.setThumbarButtons([])
|
||||||
|
}
|
||||||
|
|
||||||
|
function onPlayerPause () {
|
||||||
|
update(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onPlayerPlay () {
|
||||||
|
update(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
function update (isPaused) {
|
||||||
|
var icon = isPaused
|
||||||
|
? 'PlayThumbnailBarButton.png'
|
||||||
|
: 'PauseThumbnailBarButton.png'
|
||||||
|
|
||||||
|
var buttons = [
|
||||||
|
{
|
||||||
|
tooltip: isPaused ? 'Play' : 'Pause',
|
||||||
|
icon: path.join(config.STATIC_PATH, icon),
|
||||||
|
click: () => windows.main.dispatch('playPause')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
windows.main.win.setThumbarButtons(buttons)
|
||||||
|
}
|
||||||
@@ -22,6 +22,7 @@ function init () {
|
|||||||
resizable: false,
|
resizable: false,
|
||||||
show: false,
|
show: false,
|
||||||
skipTaskbar: true,
|
skipTaskbar: true,
|
||||||
|
title: 'About ' + config.APP_WINDOW_TITLE,
|
||||||
useContentSize: true,
|
useContentSize: true,
|
||||||
width: 300
|
width: 300
|
||||||
})
|
})
|
||||||
@@ -31,7 +32,7 @@ function init () {
|
|||||||
// No menu on the About window
|
// No menu on the About window
|
||||||
win.setMenu(null)
|
win.setMenu(null)
|
||||||
|
|
||||||
win.webContents.on('did-finish-load', function () {
|
win.webContents.once('did-finish-load', function () {
|
||||||
win.show()
|
win.show()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -51,15 +51,11 @@ function init () {
|
|||||||
menu.onToggleFullScreen(main.win.isFullScreen())
|
menu.onToggleFullScreen(main.win.isFullScreen())
|
||||||
})
|
})
|
||||||
|
|
||||||
win.on('blur', function () {
|
win.on('blur', onWindowBlur)
|
||||||
menu.onWindowBlur()
|
win.on('focus', onWindowFocus)
|
||||||
tray.onWindowBlur()
|
|
||||||
})
|
|
||||||
|
|
||||||
win.on('focus', function () {
|
win.on('hide', onWindowBlur)
|
||||||
menu.onWindowFocus()
|
win.on('show', onWindowFocus)
|
||||||
tray.onWindowFocus()
|
|
||||||
})
|
|
||||||
|
|
||||||
win.on('enter-full-screen', function () {
|
win.on('enter-full-screen', function () {
|
||||||
menu.onToggleFullScreen(true)
|
menu.onToggleFullScreen(true)
|
||||||
@@ -78,7 +74,7 @@ function init () {
|
|||||||
app.quit()
|
app.quit()
|
||||||
} else if (!app.isQuitting) {
|
} else if (!app.isQuitting) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
win.hide()
|
hide()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -207,6 +203,16 @@ function toggleFullScreen (flag) {
|
|||||||
main.win.setFullScreen(flag)
|
main.win.setFullScreen(flag)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onWindowBlur () {
|
||||||
|
menu.onWindowBlur()
|
||||||
|
tray.onWindowBlur()
|
||||||
|
}
|
||||||
|
|
||||||
|
function onWindowFocus () {
|
||||||
|
menu.onWindowFocus()
|
||||||
|
tray.onWindowFocus()
|
||||||
|
}
|
||||||
|
|
||||||
function getIconPath () {
|
function getIconPath () {
|
||||||
return process.platform === 'win32'
|
return process.platform === 'win32'
|
||||||
? config.APP_ICON + '.ico'
|
? config.APP_ICON + '.ico'
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
{
|
{
|
||||||
"name": "webtorrent-desktop",
|
"name": "webtorrent-desktop",
|
||||||
"description": "WebTorrent, the streaming torrent client. For OS X, Windows, and Linux.",
|
"description": "WebTorrent, the streaming torrent client. For OS X, Windows, and Linux.",
|
||||||
"version": "0.7.0",
|
"version": "0.9.0",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "WebTorrent, LLC",
|
"name": "WebTorrent, LLC",
|
||||||
"email": "feross@feross.org",
|
"email": "feross@webtorrent.io",
|
||||||
"url": "https://webtorrent.io"
|
"url": "https://webtorrent.io"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -30,6 +30,7 @@
|
|||||||
"main-loop": "^3.2.0",
|
"main-loop": "^3.2.0",
|
||||||
"musicmetadata": "^2.0.2",
|
"musicmetadata": "^2.0.2",
|
||||||
"network-address": "^1.1.0",
|
"network-address": "^1.1.0",
|
||||||
|
"parse-torrent": "^5.7.3",
|
||||||
"prettier-bytes": "^1.0.1",
|
"prettier-bytes": "^1.0.1",
|
||||||
"run-parallel": "^1.1.6",
|
"run-parallel": "^1.1.6",
|
||||||
"semver": "^5.1.0",
|
"semver": "^5.1.0",
|
||||||
|
|||||||
56
renderer/controllers/media-controller.js
Normal file
56
renderer/controllers/media-controller.js
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
const electron = require('electron')
|
||||||
|
|
||||||
|
const ipcRenderer = electron.ipcRenderer
|
||||||
|
|
||||||
|
// Controls local play back: the <video>/<audio> tag and VLC
|
||||||
|
// Does not control remote casting (Chromecast etc)
|
||||||
|
module.exports = class MediaController {
|
||||||
|
constructor (state) {
|
||||||
|
this.state = state
|
||||||
|
}
|
||||||
|
|
||||||
|
mediaSuccess () {
|
||||||
|
this.state.playing.result = 'success'
|
||||||
|
}
|
||||||
|
|
||||||
|
mediaStalled () {
|
||||||
|
this.state.playing.isStalled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
mediaError (error) {
|
||||||
|
var state = this.state
|
||||||
|
if (state.location.url() === 'player') {
|
||||||
|
state.playing.result = 'error'
|
||||||
|
state.playing.location = 'error'
|
||||||
|
ipcRenderer.send('checkForVLC')
|
||||||
|
ipcRenderer.once('checkForVLC', function (e, isInstalled) {
|
||||||
|
state.modal = {
|
||||||
|
id: 'unsupported-media-modal',
|
||||||
|
error: error,
|
||||||
|
vlcInstalled: isInstalled
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mediaTimeUpdate () {
|
||||||
|
this.state.playing.lastTimeUpdate = new Date().getTime()
|
||||||
|
this.state.playing.isStalled = false
|
||||||
|
}
|
||||||
|
|
||||||
|
mediaMouseMoved () {
|
||||||
|
this.state.playing.mouseStationarySince = new Date().getTime()
|
||||||
|
}
|
||||||
|
|
||||||
|
vlcPlay () {
|
||||||
|
ipcRenderer.send('vlcPlay', this.state.server.localURL)
|
||||||
|
this.state.playing.location = 'vlc'
|
||||||
|
}
|
||||||
|
|
||||||
|
vlcNotFound () {
|
||||||
|
var modal = this.state.modal
|
||||||
|
if (modal && modal.id === 'unsupported-media-modal') {
|
||||||
|
modal.vlcNotFound = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
309
renderer/controllers/playback-controller.js
Normal file
309
renderer/controllers/playback-controller.js
Normal file
@@ -0,0 +1,309 @@
|
|||||||
|
const electron = require('electron')
|
||||||
|
const path = require('path')
|
||||||
|
|
||||||
|
const Cast = require('../lib/cast')
|
||||||
|
const {dispatch} = require('../lib/dispatcher')
|
||||||
|
const telemetry = require('../lib/telemetry')
|
||||||
|
const errors = require('../lib/errors')
|
||||||
|
const sound = require('../lib/sound')
|
||||||
|
const TorrentPlayer = require('../lib/torrent-player')
|
||||||
|
const TorrentSummary = require('../lib/torrent-summary')
|
||||||
|
const State = require('../lib/state')
|
||||||
|
|
||||||
|
const ipcRenderer = electron.ipcRenderer
|
||||||
|
|
||||||
|
// Controls playback of torrents and files within torrents
|
||||||
|
// both local (<video>,<audio>,VLC) and remote (cast)
|
||||||
|
module.exports = class PlaybackController {
|
||||||
|
constructor (state, config, update) {
|
||||||
|
this.state = state
|
||||||
|
this.config = config
|
||||||
|
this.update = update
|
||||||
|
}
|
||||||
|
|
||||||
|
// Play a file in a torrent.
|
||||||
|
// * Start torrenting, if necessary
|
||||||
|
// * Stream, if not already fully downloaded
|
||||||
|
// * If no file index is provided, pick the default file to play
|
||||||
|
playFile (infoHash, index /* optional */) {
|
||||||
|
this.state.location.go({
|
||||||
|
url: 'player',
|
||||||
|
onbeforeload: (cb) => {
|
||||||
|
this.play()
|
||||||
|
openPlayer(this.state, infoHash, index, cb)
|
||||||
|
},
|
||||||
|
onbeforeunload: (cb) => closePlayer(this.state, this.config, cb)
|
||||||
|
}, (err) => {
|
||||||
|
if (err) dispatch('error', err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show a file in the OS, eg in Finder on a Mac
|
||||||
|
openItem (infoHash, index) {
|
||||||
|
var torrentSummary = TorrentSummary.getByKey(this.state, infoHash)
|
||||||
|
var filePath = path.join(
|
||||||
|
torrentSummary.path,
|
||||||
|
torrentSummary.files[index].path)
|
||||||
|
ipcRenderer.send('openItem', filePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle (play or pause) the currently playing media
|
||||||
|
playPause () {
|
||||||
|
var state = this.state
|
||||||
|
if (state.location.url() !== 'player') return
|
||||||
|
|
||||||
|
// force rerendering if window is hidden,
|
||||||
|
// in order to bypass `raf` and play/pause media immediately
|
||||||
|
var mediaTag = document.querySelector('video,audio')
|
||||||
|
if (!state.window.isVisible && mediaTag) {
|
||||||
|
if (state.playing.isPaused) mediaTag.play()
|
||||||
|
else mediaTag.pause()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.playing.isPaused) this.play()
|
||||||
|
else this.pause()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Play (unpause) the current media
|
||||||
|
play () {
|
||||||
|
var state = this.state
|
||||||
|
if (!state.playing.isPaused) return
|
||||||
|
state.playing.isPaused = false
|
||||||
|
if (isCasting(state)) {
|
||||||
|
Cast.play()
|
||||||
|
}
|
||||||
|
ipcRenderer.send('onPlayerPlay')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pause the currently playing media
|
||||||
|
pause () {
|
||||||
|
var state = this.state
|
||||||
|
if (state.playing.isPaused) return
|
||||||
|
state.playing.isPaused = true
|
||||||
|
if (isCasting(state)) {
|
||||||
|
Cast.pause()
|
||||||
|
}
|
||||||
|
ipcRenderer.send('onPlayerPause')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip specified number of seconds (backwards if negative)
|
||||||
|
skip (time) {
|
||||||
|
this.skipTo(this.state.playing.currentTime + time)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip (aka seek) to a specific point, in seconds
|
||||||
|
skipTo (time) {
|
||||||
|
if (isCasting(this.state)) Cast.seek(time)
|
||||||
|
else this.state.playing.jumpToTime = time
|
||||||
|
}
|
||||||
|
|
||||||
|
// Change playback speed. 1 = faster, -1 = slower
|
||||||
|
// Playback speed ranges from 16 (fast forward) to 1 (normal playback)
|
||||||
|
// to 0.25 (quarter-speed playback), then goes to -0.25, -0.5, -1, -2, etc
|
||||||
|
// until -16 (fast rewind)
|
||||||
|
changePlaybackRate (direction) {
|
||||||
|
var state = this.state
|
||||||
|
var rate = state.playing.playbackRate
|
||||||
|
if (direction > 0 && rate >= 0.25 && rate < 2) {
|
||||||
|
rate += 0.25
|
||||||
|
} else if (direction < 0 && rate > 0.25 && rate <= 2) {
|
||||||
|
rate -= 0.25
|
||||||
|
} else if (direction < 0 && rate === 0.25) { /* when we set playback rate at 0 in html 5, playback hangs ;( */
|
||||||
|
rate = -1
|
||||||
|
} else if (direction > 0 && rate === -1) {
|
||||||
|
rate = 0.25
|
||||||
|
} else if ((direction > 0 && rate >= 1 && rate < 16) || (direction < 0 && rate > -16 && rate <= -1)) {
|
||||||
|
rate *= 2
|
||||||
|
} else if ((direction < 0 && rate > 1 && rate <= 16) || (direction > 0 && rate >= -16 && rate < -1)) {
|
||||||
|
rate /= 2
|
||||||
|
}
|
||||||
|
state.playing.playbackRate = rate
|
||||||
|
if (isCasting(state) && !Cast.setRate(rate)) {
|
||||||
|
state.playing.playbackRate = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Change the volume, in range [0, 1], by some amount
|
||||||
|
// For example, volume muted (0), changeVolume (0.3) increases to 30% volume
|
||||||
|
changeVolume (delta) {
|
||||||
|
// change volume with delta value
|
||||||
|
this.setVolume(this.state.playing.volume + delta)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the volume to some value in [0, 1]
|
||||||
|
setVolume (volume) {
|
||||||
|
// check if its in [0.0 - 1.0] range
|
||||||
|
volume = Math.max(0, Math.min(1, volume))
|
||||||
|
|
||||||
|
var state = this.state
|
||||||
|
if (isCasting(state)) {
|
||||||
|
Cast.setVolume(volume)
|
||||||
|
} else {
|
||||||
|
state.playing.setVolume = volume
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide player controls while playing video, if the mouse stays still for a while
|
||||||
|
// Never hide the controls when:
|
||||||
|
// * The mouse is over the controls or we're scrubbing (see CSS)
|
||||||
|
// * The video is paused
|
||||||
|
// * The video is playing remotely on Chromecast or Airplay
|
||||||
|
showOrHidePlayerControls () {
|
||||||
|
var state = this.state
|
||||||
|
var hideControls = state.location.url() === 'player' &&
|
||||||
|
state.playing.mouseStationarySince !== 0 &&
|
||||||
|
new Date().getTime() - state.playing.mouseStationarySince > 2000 &&
|
||||||
|
!state.playing.isPaused &&
|
||||||
|
state.playing.location === 'local'
|
||||||
|
|
||||||
|
if (hideControls !== state.playing.hideControls) {
|
||||||
|
state.playing.hideControls = hideControls
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Opens the video player to a specific torrent
|
||||||
|
function openPlayer (state, infoHash, index, cb) {
|
||||||
|
var torrentSummary = TorrentSummary.getByKey(state, infoHash)
|
||||||
|
|
||||||
|
// automatically choose which file in the torrent to play, if necessary
|
||||||
|
if (index === undefined) index = torrentSummary.defaultPlayFileIndex
|
||||||
|
if (index === undefined) index = TorrentPlayer.pickFileToPlay(torrentSummary.files)
|
||||||
|
if (index === undefined) return cb(new errors.UnplayableError())
|
||||||
|
|
||||||
|
// update UI to show pending playback
|
||||||
|
if (torrentSummary.progress !== 1) sound.play('PLAY')
|
||||||
|
// TODO: remove torrentSummary.playStatus
|
||||||
|
torrentSummary.playStatus = 'requested'
|
||||||
|
this.update()
|
||||||
|
|
||||||
|
var timeout = setTimeout(() => {
|
||||||
|
telemetry.logPlayAttempt('timeout')
|
||||||
|
// TODO: remove torrentSummary.playStatus
|
||||||
|
torrentSummary.playStatus = 'timeout' /* no seeders available? */
|
||||||
|
sound.play('ERROR')
|
||||||
|
cb(new Error('Playback timed out. Try again.'))
|
||||||
|
this.update()
|
||||||
|
}, 10000) /* give it a few seconds */
|
||||||
|
|
||||||
|
if (torrentSummary.status === 'paused') {
|
||||||
|
dispatch('startTorrentingSummary', torrentSummary)
|
||||||
|
ipcRenderer.once('wt-ready-' + torrentSummary.infoHash,
|
||||||
|
() => openPlayerFromActiveTorrent(state, torrentSummary, index, timeout, cb))
|
||||||
|
} else {
|
||||||
|
openPlayerFromActiveTorrent(state, torrentSummary, index, timeout, cb)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openPlayerFromActiveTorrent (state, torrentSummary, index, timeout, cb) {
|
||||||
|
var fileSummary = torrentSummary.files[index]
|
||||||
|
|
||||||
|
// update state
|
||||||
|
state.playing.infoHash = torrentSummary.infoHash
|
||||||
|
state.playing.fileIndex = index
|
||||||
|
state.playing.type = TorrentPlayer.isVideo(fileSummary) ? 'video'
|
||||||
|
: TorrentPlayer.isAudio(fileSummary) ? 'audio'
|
||||||
|
: 'other'
|
||||||
|
|
||||||
|
// pick up where we left off
|
||||||
|
if (fileSummary.currentTime) {
|
||||||
|
var fraction = fileSummary.currentTime / fileSummary.duration
|
||||||
|
var secondsLeft = fileSummary.duration - fileSummary.currentTime
|
||||||
|
if (fraction < 0.9 && secondsLeft > 10) {
|
||||||
|
state.playing.jumpToTime = fileSummary.currentTime
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if it's audio, parse out the metadata (artist, title, etc)
|
||||||
|
if (state.playing.type === 'audio' && !fileSummary.audioInfo) {
|
||||||
|
ipcRenderer.send('wt-get-audio-metadata', torrentSummary.infoHash, index)
|
||||||
|
}
|
||||||
|
|
||||||
|
// if it's video, check for subtitles files that are done downloading
|
||||||
|
dispatch('checkForSubtitles')
|
||||||
|
|
||||||
|
// enable previously selected subtitle track
|
||||||
|
if (fileSummary.selectedSubtitle) {
|
||||||
|
dispatch('addSubtitles', [fileSummary.selectedSubtitle], true)
|
||||||
|
}
|
||||||
|
|
||||||
|
ipcRenderer.send('wt-start-server', torrentSummary.infoHash, index)
|
||||||
|
ipcRenderer.once('wt-server-' + torrentSummary.infoHash, (e, info) => {
|
||||||
|
clearTimeout(timeout)
|
||||||
|
|
||||||
|
// if we timed out (user clicked play a long time ago), don't autoplay
|
||||||
|
var timedOut = torrentSummary.playStatus === 'timeout'
|
||||||
|
delete torrentSummary.playStatus
|
||||||
|
if (timedOut) {
|
||||||
|
ipcRenderer.send('wt-stop-server')
|
||||||
|
return this.update()
|
||||||
|
}
|
||||||
|
|
||||||
|
// otherwise, play the video
|
||||||
|
state.window.title = torrentSummary.files[state.playing.fileIndex].name
|
||||||
|
this.update()
|
||||||
|
|
||||||
|
ipcRenderer.send('onPlayerOpen')
|
||||||
|
|
||||||
|
cb()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function closePlayer (state, config, cb) {
|
||||||
|
console.log('closePlayer')
|
||||||
|
|
||||||
|
// Quit any external players, like Chromecast/Airplay/etc or VLC
|
||||||
|
if (isCasting(state)) {
|
||||||
|
Cast.stop()
|
||||||
|
}
|
||||||
|
if (state.playing.location === 'vlc') {
|
||||||
|
ipcRenderer.send('vlcQuit')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save volume (this session only, not in state.saved)
|
||||||
|
state.previousVolume = state.playing.volume
|
||||||
|
|
||||||
|
// Telemetry: track what happens after the user clicks play
|
||||||
|
var result = state.playing.result // 'success' or 'error'
|
||||||
|
if (result === 'success') telemetry.logPlayAttempt('success') // first frame displayed
|
||||||
|
else if (result === 'error') telemetry.logPlayAttempt('error') // codec missing, etc
|
||||||
|
else if (result === undefined) telemetry.logPlayAttempt('abandoned') // user exited before first frame
|
||||||
|
else console.error('Unknown state.playing.result', state.playing.result)
|
||||||
|
|
||||||
|
// Reset the window contents back to the home screen
|
||||||
|
state.window.title = config.APP_WINDOW_TITLE
|
||||||
|
state.playing = State.getDefaultPlayState()
|
||||||
|
state.server = null
|
||||||
|
|
||||||
|
// Reset the window size and location back to where it was
|
||||||
|
if (state.window.isFullScreen) {
|
||||||
|
dispatch('toggleFullScreen', false)
|
||||||
|
}
|
||||||
|
restoreBounds(state)
|
||||||
|
|
||||||
|
// Tell the WebTorrent process to kill the torrent-to-HTTP server
|
||||||
|
ipcRenderer.send('wt-stop-server')
|
||||||
|
|
||||||
|
ipcRenderer.send('onPlayerClose')
|
||||||
|
|
||||||
|
this.update()
|
||||||
|
cb()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checks whether we are connected and already casting
|
||||||
|
// Returns false if we not casting (state.playing.location === 'local')
|
||||||
|
// or if we're trying to connect but haven't yet ('chromecast-pending', etc)
|
||||||
|
function isCasting (state) {
|
||||||
|
return state.playing.location === 'chromecast' ||
|
||||||
|
state.playing.location === 'airplay' ||
|
||||||
|
state.playing.location === 'dlna'
|
||||||
|
}
|
||||||
|
|
||||||
|
function restoreBounds (state) {
|
||||||
|
ipcRenderer.send('setAspectRatio', 0)
|
||||||
|
if (state.window.bounds) {
|
||||||
|
ipcRenderer.send('setBounds', state.window.bounds, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
51
renderer/controllers/prefs-controller.js
Normal file
51
renderer/controllers/prefs-controller.js
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
const State = require('../lib/state')
|
||||||
|
|
||||||
|
// Controls the Preferences screen
|
||||||
|
module.exports = class PrefsController {
|
||||||
|
constructor (state, config) {
|
||||||
|
this.state = state
|
||||||
|
this.config = config
|
||||||
|
}
|
||||||
|
|
||||||
|
// Goes to the Preferences screen
|
||||||
|
show () {
|
||||||
|
var state = this.state
|
||||||
|
state.location.go({
|
||||||
|
url: 'preferences',
|
||||||
|
onbeforeload: function (cb) {
|
||||||
|
// initialize preferences
|
||||||
|
state.window.title = 'Preferences'
|
||||||
|
state.unsaved = Object.assign(state.unsaved || {}, {prefs: state.saved.prefs || {}})
|
||||||
|
cb()
|
||||||
|
},
|
||||||
|
onbeforeunload: (cb) => {
|
||||||
|
// save state after preferences
|
||||||
|
this.save()
|
||||||
|
state.window.title = this.config.APP_WINDOW_TITLE
|
||||||
|
cb()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Updates a single property in the UNSAVED prefs
|
||||||
|
// For example: updatePreferences("foo.bar", "baz")
|
||||||
|
// Call savePreferences to save to config.json
|
||||||
|
update (property, value) {
|
||||||
|
var path = property.split('.')
|
||||||
|
var key = this.state.unsaved.prefs
|
||||||
|
for (var i = 0; i < path.length - 1; i++) {
|
||||||
|
if (typeof key[path[i]] === 'undefined') {
|
||||||
|
key[path[i]] = {}
|
||||||
|
}
|
||||||
|
key = key[path[i]]
|
||||||
|
}
|
||||||
|
key[path[i]] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
// All unsaved prefs take effect atomically, and are saved to config.json
|
||||||
|
save () {
|
||||||
|
var state = this.state
|
||||||
|
state.saved.prefs = Object.assign(state.saved.prefs || {}, state.unsaved.prefs)
|
||||||
|
State.save(state)
|
||||||
|
}
|
||||||
|
}
|
||||||
137
renderer/controllers/subtitles-controller.js
Normal file
137
renderer/controllers/subtitles-controller.js
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
const electron = require('electron')
|
||||||
|
const fs = require('fs-extra')
|
||||||
|
const path = require('path')
|
||||||
|
const parallel = require('run-parallel')
|
||||||
|
|
||||||
|
const {dispatch} = require('../lib/dispatcher')
|
||||||
|
|
||||||
|
module.exports = class SubtitlesController {
|
||||||
|
constructor (state) {
|
||||||
|
this.state = state
|
||||||
|
}
|
||||||
|
|
||||||
|
openSubtitles () {
|
||||||
|
electron.remote.dialog.showOpenDialog({
|
||||||
|
title: 'Select a subtitles file.',
|
||||||
|
filters: [ { name: 'Subtitles', extensions: ['vtt', 'srt'] } ],
|
||||||
|
properties: [ 'openFile' ]
|
||||||
|
}, (filenames) => {
|
||||||
|
if (!Array.isArray(filenames)) return
|
||||||
|
this.addSubtitles(filenames, true)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
selectSubtitle (ix) {
|
||||||
|
this.state.playing.subtitles.selectedIndex = ix
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleSubtitlesMenu () {
|
||||||
|
var subtitles = this.state.playing.subtitles
|
||||||
|
subtitles.showMenu = !subtitles.showMenu
|
||||||
|
}
|
||||||
|
|
||||||
|
addSubtitles (files, autoSelect) {
|
||||||
|
var state = this.state
|
||||||
|
// Subtitles are only supported when playing video files
|
||||||
|
if (state.playing.type !== 'video') return
|
||||||
|
if (files.length === 0) return
|
||||||
|
var subtitles = state.playing.subtitles
|
||||||
|
|
||||||
|
// Read the files concurrently, then add all resulting subtitle tracks
|
||||||
|
var tasks = files.map((file) => (cb) => loadSubtitle(file, cb))
|
||||||
|
parallel(tasks, function (err, tracks) {
|
||||||
|
if (err) return dispatch('error', err)
|
||||||
|
|
||||||
|
for (var i = 0; i < tracks.length; i++) {
|
||||||
|
// No dupes allowed
|
||||||
|
var track = tracks[i]
|
||||||
|
var trackIndex = state.playing.subtitles.tracks
|
||||||
|
.findIndex((t) => track.filePath === t.filePath)
|
||||||
|
|
||||||
|
// Add the track
|
||||||
|
if (trackIndex === -1) {
|
||||||
|
trackIndex = state.playing.subtitles.tracks.push(track) - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we're auto-selecting a track, try to find one in the user's language
|
||||||
|
if (autoSelect && (i === 0 || isSystemLanguage(track.language))) {
|
||||||
|
state.playing.subtitles.selectedIndex = trackIndex
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finally, make sure no two tracks have the same label
|
||||||
|
relabelSubtitles(subtitles)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
checkForSubtitles () {
|
||||||
|
if (this.state.playing.type !== 'video') return
|
||||||
|
var torrentSummary = this.state.getPlayingTorrentSummary()
|
||||||
|
if (!torrentSummary || !torrentSummary.progress) return
|
||||||
|
|
||||||
|
torrentSummary.progress.files.forEach((fp, ix) => {
|
||||||
|
if (fp.numPieces !== fp.numPiecesPresent) return // ignore incomplete files
|
||||||
|
var file = torrentSummary.files[ix]
|
||||||
|
if (!this.isSubtitle(file.name)) return
|
||||||
|
var filePath = path.join(torrentSummary.path, file.path)
|
||||||
|
this.addSubtitles([filePath], false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
isSubtitle (file) {
|
||||||
|
var name = typeof file === 'string' ? file : file.name
|
||||||
|
var ext = path.extname(name).toLowerCase()
|
||||||
|
return ext === '.srt' || ext === '.vtt'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadSubtitle (file, cb) {
|
||||||
|
// Lazy load to keep startup fast
|
||||||
|
var concat = require('simple-concat')
|
||||||
|
var LanguageDetect = require('languagedetect')
|
||||||
|
var srtToVtt = require('srt-to-vtt')
|
||||||
|
|
||||||
|
// Read the .SRT or .VTT file, parse it, add subtitle track
|
||||||
|
var filePath = file.path || file
|
||||||
|
|
||||||
|
var vttStream = fs.createReadStream(filePath).pipe(srtToVtt())
|
||||||
|
|
||||||
|
concat(vttStream, function (err, buf) {
|
||||||
|
if (err) return dispatch('error', 'Can\'t parse subtitles file.')
|
||||||
|
|
||||||
|
// Detect what language the subtitles are in
|
||||||
|
var vttContents = buf.toString().replace(/(.*-->.*)/g, '')
|
||||||
|
var langDetected = (new LanguageDetect()).detect(vttContents, 2)
|
||||||
|
langDetected = langDetected.length ? langDetected[0][0] : 'subtitle'
|
||||||
|
langDetected = langDetected.slice(0, 1).toUpperCase() + langDetected.slice(1)
|
||||||
|
|
||||||
|
var track = {
|
||||||
|
buffer: 'data:text/vtt;base64,' + buf.toString('base64'),
|
||||||
|
language: langDetected,
|
||||||
|
label: langDetected,
|
||||||
|
filePath: filePath
|
||||||
|
}
|
||||||
|
|
||||||
|
cb(null, track)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checks whether a language name like "English" or "German" matches the system
|
||||||
|
// language, aka the current locale
|
||||||
|
function isSystemLanguage (language) {
|
||||||
|
var iso639 = require('iso-639-1')
|
||||||
|
var osLangISO = window.navigator.language.split('-')[0] // eg "en"
|
||||||
|
var langIso = iso639.getCode(language) // eg "de" if language is "German"
|
||||||
|
return langIso === osLangISO
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure we don't have two subtitle tracks with the same label
|
||||||
|
// Labels each track by language, eg "German", "English", "English 2", ...
|
||||||
|
function relabelSubtitles (subtitles) {
|
||||||
|
var counts = {}
|
||||||
|
subtitles.tracks.forEach(function (track) {
|
||||||
|
var lang = track.language
|
||||||
|
counts[lang] = (counts[lang] || 0) + 1
|
||||||
|
track.label = counts[lang] > 1 ? (lang + ' ' + counts[lang]) : lang
|
||||||
|
})
|
||||||
|
}
|
||||||
192
renderer/controllers/torrent-controller.js
Normal file
192
renderer/controllers/torrent-controller.js
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
const path = require('path')
|
||||||
|
const ipcRenderer = require('electron').ipcRenderer
|
||||||
|
|
||||||
|
const TorrentSummary = require('../lib/torrent-summary')
|
||||||
|
const TorrentPlayer = require('../lib/torrent-player')
|
||||||
|
const sound = require('../lib/sound')
|
||||||
|
const {dispatch} = require('../lib/dispatcher')
|
||||||
|
|
||||||
|
module.exports = class TorrentController {
|
||||||
|
constructor (state) {
|
||||||
|
this.state = state
|
||||||
|
}
|
||||||
|
|
||||||
|
torrentInfoHash (torrentKey, infoHash) {
|
||||||
|
var torrentSummary = this.getTorrentSummary(torrentKey)
|
||||||
|
console.log('got infohash for %s torrent %s',
|
||||||
|
torrentSummary ? 'existing' : 'new', torrentKey)
|
||||||
|
|
||||||
|
if (!torrentSummary) {
|
||||||
|
var torrents = this.state.saved.torrents
|
||||||
|
|
||||||
|
// Check if an existing (non-active) torrent has the same info hash
|
||||||
|
if (torrents.find((t) => t.infoHash === infoHash)) {
|
||||||
|
ipcRenderer.send('wt-stop-torrenting', infoHash)
|
||||||
|
return dispatch('error', 'Cannot add duplicate torrent')
|
||||||
|
}
|
||||||
|
|
||||||
|
torrentSummary = {
|
||||||
|
torrentKey: torrentKey,
|
||||||
|
status: 'new'
|
||||||
|
}
|
||||||
|
torrents.unshift(torrentSummary)
|
||||||
|
sound.play('ADD')
|
||||||
|
}
|
||||||
|
|
||||||
|
torrentSummary.infoHash = infoHash
|
||||||
|
dispatch('update')
|
||||||
|
}
|
||||||
|
|
||||||
|
torrentWarning (torrentKey, message) {
|
||||||
|
console.log('warning for torrent %s: %s', torrentKey, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
torrentError (torrentKey, message) {
|
||||||
|
// TODO: WebTorrent needs semantic errors
|
||||||
|
if (message.startsWith('Cannot add duplicate torrent')) {
|
||||||
|
// Remove infohash from the message
|
||||||
|
message = 'Cannot add duplicate torrent'
|
||||||
|
}
|
||||||
|
dispatch('error', message)
|
||||||
|
|
||||||
|
var torrentSummary = this.getTorrentSummary(torrentKey)
|
||||||
|
if (torrentSummary) {
|
||||||
|
console.log('Pausing torrent %s due to error: %s', torrentSummary.infoHash, message)
|
||||||
|
torrentSummary.status = 'paused'
|
||||||
|
dispatch('update')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
torrentMetadata (torrentKey, torrentInfo) {
|
||||||
|
// Summarize torrent
|
||||||
|
var torrentSummary = this.getTorrentSummary(torrentKey)
|
||||||
|
torrentSummary.status = 'downloading'
|
||||||
|
torrentSummary.name = torrentSummary.displayName || torrentInfo.name
|
||||||
|
torrentSummary.path = torrentInfo.path
|
||||||
|
torrentSummary.magnetURI = torrentInfo.magnetURI
|
||||||
|
// TODO: make torrentInfo immutable, save separately as torrentSummary.info
|
||||||
|
// For now, check whether torrentSummary.files has already been set:
|
||||||
|
var hasDetailedFileInfo = torrentSummary.files && torrentSummary.files[0].path
|
||||||
|
if (!hasDetailedFileInfo) {
|
||||||
|
torrentSummary.files = torrentInfo.files
|
||||||
|
}
|
||||||
|
if (!torrentSummary.selections) {
|
||||||
|
torrentSummary.selections = torrentSummary.files.map((x) => true)
|
||||||
|
}
|
||||||
|
torrentSummary.defaultPlayFileIndex = TorrentPlayer.pickFileToPlay(torrentInfo.files)
|
||||||
|
dispatch('update')
|
||||||
|
|
||||||
|
// Save the .torrent file, if it hasn't been saved already
|
||||||
|
if (!torrentSummary.torrentFileName) ipcRenderer.send('wt-save-torrent-file', torrentKey)
|
||||||
|
|
||||||
|
// Auto-generate a poster image, if it hasn't been generated already
|
||||||
|
if (!torrentSummary.posterFileName) ipcRenderer.send('wt-generate-torrent-poster', torrentKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
torrentDone (torrentKey, torrentInfo) {
|
||||||
|
// Update the torrent summary
|
||||||
|
var torrentSummary = this.getTorrentSummary(torrentKey)
|
||||||
|
torrentSummary.status = 'seeding'
|
||||||
|
|
||||||
|
// Notify the user that a torrent finished, but only if we actually DL'd at least part of it.
|
||||||
|
// Don't notify if we merely finished verifying data files that were already on disk.
|
||||||
|
if (torrentInfo.bytesReceived > 0) {
|
||||||
|
if (!this.state.window.isFocused) {
|
||||||
|
this.state.dock.badge += 1
|
||||||
|
}
|
||||||
|
showDoneNotification(torrentSummary)
|
||||||
|
ipcRenderer.send('downloadFinished', getTorrentPath(torrentSummary))
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch('update')
|
||||||
|
}
|
||||||
|
|
||||||
|
torrentProgress (progressInfo) {
|
||||||
|
// Overall progress across all active torrents, 0 to 1
|
||||||
|
var progress = progressInfo.progress
|
||||||
|
var hasActiveTorrents = progressInfo.hasActiveTorrents
|
||||||
|
|
||||||
|
// Hide progress bar when client has no torrents, or progress is 100%
|
||||||
|
// TODO: isn't this equivalent to: if (progress === 1) ?
|
||||||
|
if (!hasActiveTorrents || progress === 1) {
|
||||||
|
progress = -1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show progress bar under the WebTorrent taskbar icon, on OSX
|
||||||
|
this.state.dock.progress = progress
|
||||||
|
|
||||||
|
// Update progress for each individual torrent
|
||||||
|
progressInfo.torrents.forEach((p) => {
|
||||||
|
var torrentSummary = this.getTorrentSummary(p.torrentKey)
|
||||||
|
if (!torrentSummary) {
|
||||||
|
console.log('warning: got progress for missing torrent %s', p.torrentKey)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
torrentSummary.progress = p
|
||||||
|
})
|
||||||
|
|
||||||
|
// TODO: Find an efficient way to re-enable this line, which allows subtitle
|
||||||
|
// files which are completed after a video starts to play to be added
|
||||||
|
// dynamically to the list of subtitles.
|
||||||
|
// checkForSubtitles()
|
||||||
|
|
||||||
|
dispatch('update')
|
||||||
|
}
|
||||||
|
|
||||||
|
torrentFileModtimes (torrentKey, fileModtimes) {
|
||||||
|
var torrentSummary = this.getTorrentSummary(torrentKey)
|
||||||
|
torrentSummary.fileModtimes = fileModtimes
|
||||||
|
dispatch('saveStateThrottled')
|
||||||
|
}
|
||||||
|
|
||||||
|
torrentFileSaved (torrentKey, torrentFileName) {
|
||||||
|
console.log('torrent file saved %s: %s', torrentKey, torrentFileName)
|
||||||
|
var torrentSummary = this.getTorrentSummary(torrentKey)
|
||||||
|
torrentSummary.torrentFileName = torrentFileName
|
||||||
|
dispatch('saveStateThrottled')
|
||||||
|
}
|
||||||
|
|
||||||
|
torrentPosterSaved (torrentKey, posterFileName) {
|
||||||
|
var torrentSummary = this.getTorrentSummary(torrentKey)
|
||||||
|
torrentSummary.posterFileName = posterFileName
|
||||||
|
dispatch('saveStateThrottled')
|
||||||
|
}
|
||||||
|
|
||||||
|
torrentAudioMetadata (infoHash, index, info) {
|
||||||
|
var torrentSummary = this.getTorrentSummary(infoHash)
|
||||||
|
var fileSummary = torrentSummary.files[index]
|
||||||
|
fileSummary.audioInfo = info
|
||||||
|
dispatch('update')
|
||||||
|
}
|
||||||
|
|
||||||
|
torrentServerRunning (serverInfo) {
|
||||||
|
this.state.server = serverInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gets a torrent summary {name, infoHash, status} from state.saved.torrents
|
||||||
|
// Returns undefined if we don't know that infoHash
|
||||||
|
getTorrentSummary (torrentKey) {
|
||||||
|
return TorrentSummary.getByKey(this.state, torrentKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTorrentPath (torrentSummary) {
|
||||||
|
var itemPath = TorrentSummary.getFileOrFolder(torrentSummary)
|
||||||
|
if (torrentSummary.files.length > 1) {
|
||||||
|
itemPath = path.dirname(itemPath)
|
||||||
|
}
|
||||||
|
return itemPath
|
||||||
|
}
|
||||||
|
|
||||||
|
function showDoneNotification (torrent) {
|
||||||
|
var notif = new window.Notification('Download Complete', {
|
||||||
|
body: torrent.name,
|
||||||
|
silent: true
|
||||||
|
})
|
||||||
|
|
||||||
|
notif.onclick = function () {
|
||||||
|
ipcRenderer.send('show')
|
||||||
|
}
|
||||||
|
|
||||||
|
sound.play('DONE')
|
||||||
|
}
|
||||||
282
renderer/controllers/torrent-list-controller.js
Normal file
282
renderer/controllers/torrent-list-controller.js
Normal file
@@ -0,0 +1,282 @@
|
|||||||
|
const fs = require('fs')
|
||||||
|
const path = require('path')
|
||||||
|
const electron = require('electron')
|
||||||
|
|
||||||
|
const {dispatch} = require('../lib/dispatcher')
|
||||||
|
const State = require('../lib/state')
|
||||||
|
const sound = require('../lib/sound')
|
||||||
|
const TorrentSummary = require('../lib/torrent-summary')
|
||||||
|
|
||||||
|
const ipcRenderer = electron.ipcRenderer
|
||||||
|
|
||||||
|
const instantIoRegex = /^(https:\/\/)?instant\.io\/#/
|
||||||
|
|
||||||
|
// Controls the torrent list: creating, adding, deleting, & manipulating torrents
|
||||||
|
module.exports = class TorrentListController {
|
||||||
|
constructor (state) {
|
||||||
|
this.state = state
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adds a torrent to the list, starts downloading/seeding. TorrentID can be a
|
||||||
|
// magnet URI, infohash, or torrent file: https://github.com/feross/webtorrent#clientaddtorrentid-opts-function-ontorrent-torrent-
|
||||||
|
addTorrent (torrentId) {
|
||||||
|
if (torrentId.path) {
|
||||||
|
// Use path string instead of W3C File object
|
||||||
|
torrentId = torrentId.path
|
||||||
|
}
|
||||||
|
// Allow a instant.io link to be pasted
|
||||||
|
// TODO: remove this once support is added to webtorrent core
|
||||||
|
if (typeof torrentId === 'string' && instantIoRegex.test(torrentId)) {
|
||||||
|
torrentId = torrentId.slice(torrentId.indexOf('#') + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
var torrentKey = this.state.nextTorrentKey++
|
||||||
|
var path = this.state.saved.prefs.downloadPath
|
||||||
|
|
||||||
|
ipcRenderer.send('wt-start-torrenting', torrentKey, torrentId, path)
|
||||||
|
|
||||||
|
dispatch('backToList')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shows the Create Torrent page with options to seed a given file or folder
|
||||||
|
showCreateTorrent (files) {
|
||||||
|
// Files will either be an array of file objects, which we can send directly
|
||||||
|
// to the create-torrent screen
|
||||||
|
if (files.length === 0 || typeof files[0] !== 'string') {
|
||||||
|
this.state.location.go({
|
||||||
|
url: 'create-torrent',
|
||||||
|
files: files
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ... or it will be an array of mixed file and folder paths. We have to walk
|
||||||
|
// through all the folders and find the files
|
||||||
|
findFilesRecursive(files, (allFiles) => this.showCreateTorrent(allFiles))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Switches between the advanced and simple Create Torrent UI
|
||||||
|
toggleCreateTorrentAdvanced () {
|
||||||
|
var info = this.state.location.current()
|
||||||
|
if (info.url !== 'create-torrent') return
|
||||||
|
info.showAdvanced = !info.showAdvanced
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creates a new torrent and start seeeding
|
||||||
|
createTorrent (options) {
|
||||||
|
var state = this.state
|
||||||
|
var torrentKey = state.nextTorrentKey++
|
||||||
|
ipcRenderer.send('wt-create-torrent', torrentKey, options)
|
||||||
|
state.location.backToFirst(function () {
|
||||||
|
state.location.clearForward('create-torrent')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Starts downloading and/or seeding a given torrentSummary.
|
||||||
|
startTorrentingSummary (torrentSummary) {
|
||||||
|
var s = torrentSummary
|
||||||
|
|
||||||
|
// Backward compatibility for config files save before we had torrentKey
|
||||||
|
if (!s.torrentKey) s.torrentKey = this.state.nextTorrentKey++
|
||||||
|
|
||||||
|
// Use Downloads folder by default
|
||||||
|
if (!s.path) s.path = this.state.saved.prefs.downloadPath
|
||||||
|
|
||||||
|
ipcRenderer.send('wt-start-torrenting',
|
||||||
|
s.torrentKey,
|
||||||
|
TorrentSummary.getTorrentID(s),
|
||||||
|
s.path,
|
||||||
|
s.fileModtimes,
|
||||||
|
s.selections)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: use torrentKey, not infoHash
|
||||||
|
toggleTorrent (infoHash) {
|
||||||
|
var torrentSummary = TorrentSummary.getByKey(this.state, infoHash)
|
||||||
|
if (torrentSummary.status === 'paused') {
|
||||||
|
torrentSummary.status = 'new'
|
||||||
|
this.startTorrentingSummary(torrentSummary)
|
||||||
|
sound.play('ENABLE')
|
||||||
|
} else {
|
||||||
|
torrentSummary.status = 'paused'
|
||||||
|
ipcRenderer.send('wt-stop-torrenting', torrentSummary.infoHash)
|
||||||
|
sound.play('DISABLE')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleTorrentFile (infoHash, index) {
|
||||||
|
var torrentSummary = TorrentSummary.getByKey(this.state, infoHash)
|
||||||
|
torrentSummary.selections[index] = !torrentSummary.selections[index]
|
||||||
|
|
||||||
|
// Let the WebTorrent process know to start or stop fetching that file
|
||||||
|
ipcRenderer.send('wt-select-files', infoHash, torrentSummary.selections)
|
||||||
|
}
|
||||||
|
|
||||||
|
confirmDeleteTorrent (infoHash, deleteData) {
|
||||||
|
this.state.modal = {
|
||||||
|
id: 'remove-torrent-modal',
|
||||||
|
infoHash,
|
||||||
|
deleteData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: use torrentKey, not infoHash
|
||||||
|
deleteTorrent (infoHash, deleteData) {
|
||||||
|
ipcRenderer.send('wt-stop-torrenting', infoHash)
|
||||||
|
|
||||||
|
var index = this.state.saved.torrents.findIndex((x) => x.infoHash === infoHash)
|
||||||
|
|
||||||
|
if (index > -1) {
|
||||||
|
var summary = this.state.saved.torrents[index]
|
||||||
|
|
||||||
|
// remove torrent and poster file
|
||||||
|
deleteFile(TorrentSummary.getTorrentPath(summary))
|
||||||
|
deleteFile(TorrentSummary.getPosterPath(summary)) // TODO: will the css path hack affect windows?
|
||||||
|
|
||||||
|
// optionally delete the torrent data
|
||||||
|
if (deleteData) moveItemToTrash(summary)
|
||||||
|
|
||||||
|
// remove torrent from saved list
|
||||||
|
this.state.saved.torrents.splice(index, 1)
|
||||||
|
State.saveThrottled(this.state)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.state.location.clearForward('player') // prevent user from going forward to a deleted torrent
|
||||||
|
sound.play('DELETE')
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleSelectTorrent (infoHash) {
|
||||||
|
if (this.state.selectedInfoHash === infoHash) {
|
||||||
|
this.state.selectedInfoHash = null
|
||||||
|
} else {
|
||||||
|
this.state.selectedInfoHash = infoHash
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
openTorrentContextMenu (infoHash) {
|
||||||
|
var torrentSummary = TorrentSummary.getByKey(this.state, infoHash)
|
||||||
|
var menu = new electron.remote.Menu()
|
||||||
|
|
||||||
|
menu.append(new electron.remote.MenuItem({
|
||||||
|
label: 'Remove From List',
|
||||||
|
click: () => dispatch('confirmDeleteTorrent', torrentSummary.infoHash, false)
|
||||||
|
}))
|
||||||
|
|
||||||
|
menu.append(new electron.remote.MenuItem({
|
||||||
|
label: 'Remove Data File',
|
||||||
|
click: () => dispatch('confirmDeleteTorrent', torrentSummary.infoHash, true)
|
||||||
|
}))
|
||||||
|
|
||||||
|
menu.append(new electron.remote.MenuItem({
|
||||||
|
type: 'separator'
|
||||||
|
}))
|
||||||
|
|
||||||
|
if (torrentSummary.files) {
|
||||||
|
menu.append(new electron.remote.MenuItem({
|
||||||
|
label: process.platform === 'darwin' ? 'Show in Finder' : 'Show in Folder',
|
||||||
|
click: () => showItemInFolder(torrentSummary)
|
||||||
|
}))
|
||||||
|
menu.append(new electron.remote.MenuItem({
|
||||||
|
type: 'separator'
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
menu.append(new electron.remote.MenuItem({
|
||||||
|
label: 'Copy Magnet Link to Clipboard',
|
||||||
|
click: () => electron.clipboard.writeText(torrentSummary.magnetURI)
|
||||||
|
}))
|
||||||
|
|
||||||
|
menu.append(new electron.remote.MenuItem({
|
||||||
|
label: 'Copy Instant.io Link to Clipboard',
|
||||||
|
click: () => electron.clipboard.writeText(`https://instant.io/#${torrentSummary.infoHash}`)
|
||||||
|
}))
|
||||||
|
|
||||||
|
menu.append(new electron.remote.MenuItem({
|
||||||
|
label: 'Save Torrent File As...',
|
||||||
|
click: () => saveTorrentFileAs(torrentSummary)
|
||||||
|
}))
|
||||||
|
|
||||||
|
menu.popup(electron.remote.getCurrentWindow())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recursively finds {name, path, size} for all files in a folder
|
||||||
|
// Calls `cb` on success, calls `onError` on failure
|
||||||
|
function findFilesRecursive (paths, cb) {
|
||||||
|
if (paths.length > 1) {
|
||||||
|
var numComplete = 0
|
||||||
|
var ret = []
|
||||||
|
paths.forEach(function (path) {
|
||||||
|
findFilesRecursive([path], function (fileObjs) {
|
||||||
|
ret = ret.concat(fileObjs)
|
||||||
|
if (++numComplete === paths.length) {
|
||||||
|
ret.sort((a, b) => a.path < b.path ? -1 : a.path > b.path)
|
||||||
|
cb(ret)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var fileOrFolder = paths[0]
|
||||||
|
fs.stat(fileOrFolder, function (err, stat) {
|
||||||
|
if (err) return dispatch('error', err)
|
||||||
|
|
||||||
|
// Files: return name, path, and size
|
||||||
|
if (!stat.isDirectory()) {
|
||||||
|
var filePath = fileOrFolder
|
||||||
|
return cb([{
|
||||||
|
name: path.basename(filePath),
|
||||||
|
path: filePath,
|
||||||
|
size: stat.size
|
||||||
|
}])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Folders: recurse, make a list of all the files
|
||||||
|
var folderPath = fileOrFolder
|
||||||
|
fs.readdir(folderPath, function (err, fileNames) {
|
||||||
|
if (err) return dispatch('error', err)
|
||||||
|
var paths = fileNames.map((fileName) => path.join(folderPath, fileName))
|
||||||
|
findFilesRecursive(paths, cb)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteFile (path) {
|
||||||
|
if (!path) return
|
||||||
|
fs.unlink(path, function (err) {
|
||||||
|
if (err) dispatch('error', err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete all files in a torrent
|
||||||
|
function moveItemToTrash (torrentSummary) {
|
||||||
|
var filePath = TorrentSummary.getFileOrFolder(torrentSummary)
|
||||||
|
ipcRenderer.send('moveItemToTrash', filePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
function showItemInFolder (torrentSummary) {
|
||||||
|
ipcRenderer.send('showItemInFolder', TorrentSummary.getFileOrFolder(torrentSummary))
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveTorrentFileAs (torrentSummary) {
|
||||||
|
var downloadPath = this.state.saved.prefs.downloadPath
|
||||||
|
var newFileName = path.parse(torrentSummary.name).name + '.torrent'
|
||||||
|
var opts = {
|
||||||
|
title: 'Save Torrent File',
|
||||||
|
defaultPath: path.join(downloadPath, newFileName),
|
||||||
|
filters: [
|
||||||
|
{ name: 'Torrent Files', extensions: ['torrent'] },
|
||||||
|
{ name: 'All Files', extensions: ['*'] }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
electron.remote.dialog.showSaveDialog(electron.remote.getCurrentWindow(), opts, function (savePath) {
|
||||||
|
var torrentPath = TorrentSummary.getTorrentPath(torrentSummary)
|
||||||
|
fs.readFile(torrentPath, function (err, torrentFile) {
|
||||||
|
if (err) return dispatch('error', err)
|
||||||
|
fs.writeFile(savePath, torrentFile, function (err) {
|
||||||
|
if (err) return dispatch('error', err)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
26
renderer/controllers/update-controller.js
Normal file
26
renderer/controllers/update-controller.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
const State = require('../lib/state')
|
||||||
|
|
||||||
|
// Controls the UI checking for new versions of the app, prompting install
|
||||||
|
module.exports = class UpdateController {
|
||||||
|
constructor (state) {
|
||||||
|
this.state = state
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shows a modal saying that we have an update
|
||||||
|
updateAvailable (version) {
|
||||||
|
var skipped = this.state.saved.skippedVersions
|
||||||
|
if (skipped && skipped.includes(version)) {
|
||||||
|
console.log('new version skipped by user: v' + version)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.state.modal = { id: 'update-available-modal', version: version }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't show the modal again until the next version
|
||||||
|
skipVersion (version) {
|
||||||
|
var skipped = this.state.saved.skippedVersions
|
||||||
|
if (!skipped) skipped = this.state.saved.skippedVersions = []
|
||||||
|
skipped.push(version)
|
||||||
|
State.saveThrottled(this.state)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,8 +3,9 @@
|
|||||||
// * Starts and stops casting, provides remote video controls
|
// * Starts and stops casting, provides remote video controls
|
||||||
module.exports = {
|
module.exports = {
|
||||||
init,
|
init,
|
||||||
open,
|
toggleMenu,
|
||||||
close,
|
selectDevice,
|
||||||
|
stop,
|
||||||
play,
|
play,
|
||||||
pause,
|
pause,
|
||||||
seek,
|
seek,
|
||||||
@@ -12,9 +13,8 @@ module.exports = {
|
|||||||
setRate
|
setRate
|
||||||
}
|
}
|
||||||
|
|
||||||
var airplayer = require('airplayer')()
|
// Lazy load these for a ~300ms improvement in startup time
|
||||||
var chromecasts = require('chromecasts')()
|
var airplayer, chromecasts, dlnacasts
|
||||||
var dlnacasts = require('dlnacasts')()
|
|
||||||
|
|
||||||
var config = require('../../config')
|
var config = require('../../config')
|
||||||
|
|
||||||
@@ -32,24 +32,54 @@ function init (appState, callback) {
|
|||||||
state = appState
|
state = appState
|
||||||
update = callback
|
update = callback
|
||||||
|
|
||||||
|
// Load modules, scan the network for devices
|
||||||
|
airplayer = require('airplayer')()
|
||||||
|
chromecasts = require('chromecasts')()
|
||||||
|
dlnacasts = require('dlnacasts')()
|
||||||
|
|
||||||
|
state.devices.chromecast = chromecastPlayer()
|
||||||
|
state.devices.dlna = dlnaPlayer()
|
||||||
|
state.devices.airplay = airplayPlayer()
|
||||||
|
|
||||||
// Listen for devices: Chromecast, DLNA and Airplay
|
// Listen for devices: Chromecast, DLNA and Airplay
|
||||||
chromecasts.on('update', function (player) {
|
chromecasts.on('update', function (device) {
|
||||||
state.devices.chromecast = chromecastPlayer(player)
|
// TODO: how do we tell if there are *no longer* any Chromecasts available?
|
||||||
|
// From looking at the code, chromecasts.players only grows, never shrinks
|
||||||
|
state.devices.chromecast.addDevice(device)
|
||||||
})
|
})
|
||||||
|
|
||||||
dlnacasts.on('update', function (player) {
|
dlnacasts.on('update', function (device) {
|
||||||
state.devices.dlna = dlnaPlayer(player)
|
state.devices.dlna.addDevice(device)
|
||||||
})
|
})
|
||||||
|
|
||||||
airplayer.on('update', function (player) {
|
airplayer.on('update', function (device) {
|
||||||
state.devices.airplay = airplayPlayer(player)
|
state.devices.airplay.addDevice(device)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// chromecast player implementation
|
// chromecast player implementation
|
||||||
function chromecastPlayer (player) {
|
function chromecastPlayer () {
|
||||||
function addEvents () {
|
var ret = {
|
||||||
player.on('error', function (err) {
|
device: null,
|
||||||
|
addDevice,
|
||||||
|
getDevices,
|
||||||
|
open,
|
||||||
|
play,
|
||||||
|
pause,
|
||||||
|
stop,
|
||||||
|
status,
|
||||||
|
seek,
|
||||||
|
volume
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
|
||||||
|
function getDevices () {
|
||||||
|
return chromecasts.players
|
||||||
|
}
|
||||||
|
|
||||||
|
function addDevice (device) {
|
||||||
|
device.on('error', function (err) {
|
||||||
|
if (device !== ret.device) return
|
||||||
state.playing.location = 'local'
|
state.playing.location = 'local'
|
||||||
state.errors.push({
|
state.errors.push({
|
||||||
time: new Date().getTime(),
|
time: new Date().getTime(),
|
||||||
@@ -57,7 +87,8 @@ function chromecastPlayer (player) {
|
|||||||
})
|
})
|
||||||
update()
|
update()
|
||||||
})
|
})
|
||||||
player.on('disconnect', function () {
|
device.on('disconnect', function () {
|
||||||
|
if (device !== ret.device) return
|
||||||
state.playing.location = 'local'
|
state.playing.location = 'local'
|
||||||
update()
|
update()
|
||||||
})
|
})
|
||||||
@@ -65,7 +96,7 @@ function chromecastPlayer (player) {
|
|||||||
|
|
||||||
function open () {
|
function open () {
|
||||||
var torrentSummary = state.saved.torrents.find((x) => x.infoHash === state.playing.infoHash)
|
var torrentSummary = state.saved.torrents.find((x) => x.infoHash === state.playing.infoHash)
|
||||||
player.play(state.server.networkURL, {
|
ret.device.play(state.server.networkURL, {
|
||||||
type: 'video/mp4',
|
type: 'video/mp4',
|
||||||
title: config.APP_NAME + ' - ' + torrentSummary.name
|
title: config.APP_NAME + ' - ' + torrentSummary.name
|
||||||
}, function (err) {
|
}, function (err) {
|
||||||
@@ -83,19 +114,19 @@ function chromecastPlayer (player) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function play (callback) {
|
function play (callback) {
|
||||||
player.play(null, null, callback)
|
ret.device.play(null, null, callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
function pause (callback) {
|
function pause (callback) {
|
||||||
player.pause(callback)
|
ret.device.pause(callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
function stop (callback) {
|
function stop (callback) {
|
||||||
player.stop(callback)
|
ret.device.stop(callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
function status () {
|
function status () {
|
||||||
player.status(function (err, status) {
|
ret.device.status(function (err, status) {
|
||||||
if (err) return console.log('error getting %s status: %o', state.playing.location, err)
|
if (err) return console.log('error getting %s status: %o', state.playing.location, err)
|
||||||
state.playing.isPaused = status.playerState === 'PAUSED'
|
state.playing.isPaused = status.playerState === 'PAUSED'
|
||||||
state.playing.currentTime = status.currentTime
|
state.playing.currentTime = status.currentTime
|
||||||
@@ -105,30 +136,31 @@ function chromecastPlayer (player) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function seek (time, callback) {
|
function seek (time, callback) {
|
||||||
player.seek(time, callback)
|
ret.device.seek(time, callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
function volume (volume, callback) {
|
function volume (volume, callback) {
|
||||||
player.volume(volume, callback)
|
ret.device.volume(volume, callback)
|
||||||
}
|
|
||||||
|
|
||||||
addEvents()
|
|
||||||
|
|
||||||
return {
|
|
||||||
player: player,
|
|
||||||
open: open,
|
|
||||||
play: play,
|
|
||||||
pause: pause,
|
|
||||||
stop: stop,
|
|
||||||
status: status,
|
|
||||||
seek: seek,
|
|
||||||
volume: volume
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// airplay player implementation
|
// airplay player implementation
|
||||||
function airplayPlayer (player) {
|
function airplayPlayer () {
|
||||||
function addEvents () {
|
var ret = {
|
||||||
|
device: null,
|
||||||
|
addDevice,
|
||||||
|
getDevices,
|
||||||
|
open,
|
||||||
|
play,
|
||||||
|
pause,
|
||||||
|
stop,
|
||||||
|
status,
|
||||||
|
seek,
|
||||||
|
volume
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
|
||||||
|
function addDevice (player) {
|
||||||
player.on('event', function (event) {
|
player.on('event', function (event) {
|
||||||
switch (event.state) {
|
switch (event.state) {
|
||||||
case 'loading':
|
case 'loading':
|
||||||
@@ -146,8 +178,12 @@ function airplayPlayer (player) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getDevices () {
|
||||||
|
return airplayer.players
|
||||||
|
}
|
||||||
|
|
||||||
function open () {
|
function open () {
|
||||||
player.play(state.server.networkURL, function (err, res) {
|
ret.device.play(state.server.networkURL, function (err, res) {
|
||||||
if (err) {
|
if (err) {
|
||||||
state.playing.location = 'local'
|
state.playing.location = 'local'
|
||||||
state.errors.push({
|
state.errors.push({
|
||||||
@@ -162,19 +198,19 @@ function airplayPlayer (player) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function play (callback) {
|
function play (callback) {
|
||||||
player.resume(callback)
|
ret.device.resume(callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
function pause (callback) {
|
function pause (callback) {
|
||||||
player.pause(callback)
|
ret.device.pause(callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
function stop (callback) {
|
function stop (callback) {
|
||||||
player.stop(callback)
|
ret.device.stop(callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
function status () {
|
function status () {
|
||||||
player.playbackInfo(function (err, res, status) {
|
ret.device.playbackInfo(function (err, res, status) {
|
||||||
if (err) {
|
if (err) {
|
||||||
state.playing.location = 'local'
|
state.playing.location = 'local'
|
||||||
state.errors.push({
|
state.errors.push({
|
||||||
@@ -190,7 +226,7 @@ function airplayPlayer (player) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function seek (time, callback) {
|
function seek (time, callback) {
|
||||||
player.scrub(time, callback)
|
ret.device.scrub(time, callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
function volume (volume, callback) {
|
function volume (volume, callback) {
|
||||||
@@ -198,25 +234,31 @@ function airplayPlayer (player) {
|
|||||||
// TODO: We should just disable the volume slider
|
// TODO: We should just disable the volume slider
|
||||||
state.playing.volume = volume
|
state.playing.volume = volume
|
||||||
}
|
}
|
||||||
|
|
||||||
addEvents()
|
|
||||||
|
|
||||||
return {
|
|
||||||
player: player,
|
|
||||||
open: open,
|
|
||||||
play: play,
|
|
||||||
pause: pause,
|
|
||||||
stop: stop,
|
|
||||||
status: status,
|
|
||||||
seek: seek,
|
|
||||||
volume: volume
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DLNA player implementation
|
// DLNA player implementation
|
||||||
function dlnaPlayer (player) {
|
function dlnaPlayer (player) {
|
||||||
function addEvents () {
|
var ret = {
|
||||||
player.on('error', function (err) {
|
device: null,
|
||||||
|
addDevice,
|
||||||
|
getDevices,
|
||||||
|
open,
|
||||||
|
play,
|
||||||
|
pause,
|
||||||
|
stop,
|
||||||
|
status,
|
||||||
|
seek,
|
||||||
|
volume
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
|
||||||
|
function getDevices () {
|
||||||
|
return dlnacasts.players
|
||||||
|
}
|
||||||
|
|
||||||
|
function addDevice (device) {
|
||||||
|
device.on('error', function (err) {
|
||||||
|
if (device !== ret.device) return
|
||||||
state.playing.location = 'local'
|
state.playing.location = 'local'
|
||||||
state.errors.push({
|
state.errors.push({
|
||||||
time: new Date().getTime(),
|
time: new Date().getTime(),
|
||||||
@@ -224,7 +266,8 @@ function dlnaPlayer (player) {
|
|||||||
})
|
})
|
||||||
update()
|
update()
|
||||||
})
|
})
|
||||||
player.on('disconnect', function () {
|
device.on('disconnect', function () {
|
||||||
|
if (device !== ret.device) return
|
||||||
state.playing.location = 'local'
|
state.playing.location = 'local'
|
||||||
update()
|
update()
|
||||||
})
|
})
|
||||||
@@ -232,7 +275,7 @@ function dlnaPlayer (player) {
|
|||||||
|
|
||||||
function open () {
|
function open () {
|
||||||
var torrentSummary = state.saved.torrents.find((x) => x.infoHash === state.playing.infoHash)
|
var torrentSummary = state.saved.torrents.find((x) => x.infoHash === state.playing.infoHash)
|
||||||
player.play(state.server.networkURL, {
|
ret.device.play(state.server.networkURL, {
|
||||||
type: 'video/mp4',
|
type: 'video/mp4',
|
||||||
title: config.APP_NAME + ' - ' + torrentSummary.name,
|
title: config.APP_NAME + ' - ' + torrentSummary.name,
|
||||||
seek: state.playing.currentTime > 10 ? state.playing.currentTime : 0
|
seek: state.playing.currentTime > 10 ? state.playing.currentTime : 0
|
||||||
@@ -251,19 +294,19 @@ function dlnaPlayer (player) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function play (callback) {
|
function play (callback) {
|
||||||
player.play(null, null, callback)
|
ret.device.play(null, null, callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
function pause (callback) {
|
function pause (callback) {
|
||||||
player.pause(callback)
|
ret.device.pause(callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
function stop (callback) {
|
function stop (callback) {
|
||||||
player.stop(callback)
|
ret.device.stop(callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
function status () {
|
function status () {
|
||||||
player.status(function (err, status) {
|
ret.device.status(function (err, status) {
|
||||||
if (err) return console.log('error getting %s status: %o', state.playing.location, err)
|
if (err) return console.log('error getting %s status: %o', state.playing.location, err)
|
||||||
state.playing.isPaused = status.playerState === 'PAUSED'
|
state.playing.isPaused = status.playerState === 'PAUSED'
|
||||||
state.playing.currentTime = status.currentTime
|
state.playing.currentTime = status.currentTime
|
||||||
@@ -273,61 +316,78 @@ function dlnaPlayer (player) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function seek (time, callback) {
|
function seek (time, callback) {
|
||||||
player.seek(time, callback)
|
ret.device.seek(time, callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
function volume (volume, callback) {
|
function volume (volume, callback) {
|
||||||
player.volume(volume, function (err) {
|
ret.device.volume(volume, function (err) {
|
||||||
// quick volume update
|
// quick volume update
|
||||||
state.playing.volume = volume
|
state.playing.volume = volume
|
||||||
callback(err)
|
callback(err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
addEvents()
|
|
||||||
|
|
||||||
return {
|
|
||||||
player: player,
|
|
||||||
open: open,
|
|
||||||
play: play,
|
|
||||||
pause: pause,
|
|
||||||
stop: stop,
|
|
||||||
status: status,
|
|
||||||
seek: seek,
|
|
||||||
volume: volume
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start polling cast device state, whenever we're connected
|
// Start polling cast device state, whenever we're connected
|
||||||
function startStatusInterval () {
|
function startStatusInterval () {
|
||||||
statusInterval = setInterval(function () {
|
statusInterval = setInterval(function () {
|
||||||
var device = getDevice()
|
var player = getPlayer()
|
||||||
if (device) {
|
if (player) player.status()
|
||||||
device.status()
|
|
||||||
}
|
|
||||||
}, 1000)
|
}, 1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
function open (location) {
|
/*
|
||||||
|
* Shows the device menu for a given cast type ('chromecast', 'airplay', etc)
|
||||||
|
* The menu lists eg. all Chromecasts detected; the user can click one to cast.
|
||||||
|
* If the menu was already showing for that type, hides the menu.
|
||||||
|
*/
|
||||||
|
function toggleMenu (location) {
|
||||||
|
// If the menu is already showing, hide it
|
||||||
|
if (state.devices.castMenu && state.devices.castMenu.location === location) {
|
||||||
|
state.devices.castMenu = null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Never cast to two devices at the same time
|
||||||
if (state.playing.location !== 'local') {
|
if (state.playing.location !== 'local') {
|
||||||
throw new Error('You can\'t connect to ' + location + ' when already connected to another device')
|
throw new Error('You can\'t connect to ' + location + ' when already connected to another device')
|
||||||
}
|
}
|
||||||
|
|
||||||
state.playing.location = location + '-pending'
|
// Find all cast devices of the given type
|
||||||
var device = getDevice(location)
|
var player = getPlayer(location)
|
||||||
if (device) {
|
var devices = player ? player.getDevices() : []
|
||||||
getDevice(location).open()
|
if (devices.length === 0) throw new Error('No ' + location + ' devices available')
|
||||||
startStatusInterval()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Show a menu
|
||||||
|
state.devices.castMenu = {location, devices}
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectDevice (index) {
|
||||||
|
var {location, devices} = state.devices.castMenu
|
||||||
|
|
||||||
|
// Start casting
|
||||||
|
var player = getPlayer(location)
|
||||||
|
player.device = devices[index]
|
||||||
|
player.open()
|
||||||
|
|
||||||
|
// Poll the casting device's status every few seconds
|
||||||
|
startStatusInterval()
|
||||||
|
|
||||||
|
// Show the Connecting... screen
|
||||||
|
state.devices.castMenu = null
|
||||||
|
state.playing.castName = devices[index].name
|
||||||
|
state.playing.location = location + '-pending'
|
||||||
update()
|
update()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stops casting, move video back to local screen
|
// Stops casting, move video back to local screen
|
||||||
function close () {
|
function stop () {
|
||||||
var device = getDevice()
|
var player = getPlayer()
|
||||||
if (device) {
|
if (player) {
|
||||||
device.stop(stoppedCasting)
|
player.stop(function () {
|
||||||
|
player.device = null
|
||||||
|
stoppedCasting()
|
||||||
|
})
|
||||||
clearInterval(statusInterval)
|
clearInterval(statusInterval)
|
||||||
} else {
|
} else {
|
||||||
stoppedCasting()
|
stoppedCasting()
|
||||||
@@ -340,8 +400,8 @@ function stoppedCasting () {
|
|||||||
update()
|
update()
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDevice (location) {
|
function getPlayer (location) {
|
||||||
if (location && state.devices[location]) {
|
if (location) {
|
||||||
return state.devices[location]
|
return state.devices[location]
|
||||||
} else if (state.playing.location === 'chromecast') {
|
} else if (state.playing.location === 'chromecast') {
|
||||||
return state.devices.chromecast
|
return state.devices.chromecast
|
||||||
@@ -355,29 +415,25 @@ function getDevice (location) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function play () {
|
function play () {
|
||||||
var device = getDevice()
|
var player = getPlayer()
|
||||||
if (device) {
|
if (player) player.play(castCallback)
|
||||||
device.play(castCallback)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function pause () {
|
function pause () {
|
||||||
var device = getDevice()
|
var player = getPlayer()
|
||||||
if (device) {
|
if (player) player.pause(castCallback)
|
||||||
device.pause(castCallback)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function setRate (rate) {
|
function setRate (rate) {
|
||||||
var device
|
var player
|
||||||
var result = true
|
var result = true
|
||||||
if (state.playing.location === 'chromecast') {
|
if (state.playing.location === 'chromecast') {
|
||||||
// TODO find how to control playback rate on chromecast
|
// TODO find how to control playback rate on chromecast
|
||||||
castCallback()
|
castCallback()
|
||||||
result = false
|
result = false
|
||||||
} else if (state.playing.location === 'airplay') {
|
} else if (state.playing.location === 'airplay') {
|
||||||
device = state.devices.airplay
|
player = state.devices.airplay
|
||||||
device.rate(rate, castCallback)
|
player.rate(rate, castCallback)
|
||||||
} else {
|
} else {
|
||||||
result = false
|
result = false
|
||||||
}
|
}
|
||||||
@@ -385,17 +441,13 @@ function setRate (rate) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function seek (time) {
|
function seek (time) {
|
||||||
var device = getDevice()
|
var player = getPlayer()
|
||||||
if (device) {
|
if (player) player.seek(time, castCallback)
|
||||||
device.seek(time, castCallback)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function setVolume (volume) {
|
function setVolume (volume) {
|
||||||
var device = getDevice()
|
var player = getPlayer()
|
||||||
if (device) {
|
if (player) player.volume(volume, castCallback)
|
||||||
device.volume(volume, castCallback)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function castCallback () {
|
function castCallback () {
|
||||||
|
|||||||
@@ -18,44 +18,41 @@ function run (state) {
|
|||||||
var version = state.saved.version
|
var version = state.saved.version
|
||||||
|
|
||||||
if (semver.lt(version, '0.7.0')) {
|
if (semver.lt(version, '0.7.0')) {
|
||||||
migrate_0_7_0(state)
|
migrate_0_7_0(state.saved)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Future migrations...
|
if (semver.lt(version, '0.7.2')) {
|
||||||
// if (semver.lt(version, '0.8.0')) {
|
migrate_0_7_2(state.saved)
|
||||||
// migrate_0_8_0(state)
|
}
|
||||||
// }
|
|
||||||
|
|
||||||
// Config is now on the new version
|
// Config is now on the new version
|
||||||
state.saved.version = config.APP_VERSION
|
state.saved.version = config.APP_VERSION
|
||||||
}
|
}
|
||||||
|
|
||||||
function migrate_0_7_0 (state) {
|
function migrate_0_7_0 (saved) {
|
||||||
console.log('migrate to 0.7.0')
|
console.log('migrate to 0.7.0')
|
||||||
|
|
||||||
var fs = require('fs-extra')
|
var fs = require('fs-extra')
|
||||||
var path = require('path')
|
var path = require('path')
|
||||||
|
|
||||||
state.saved.torrents.forEach(function (ts) {
|
saved.torrents.forEach(function (ts) {
|
||||||
var infoHash = ts.infoHash
|
var infoHash = ts.infoHash
|
||||||
|
|
||||||
// Replace torrentPath with torrentFileName
|
// Replace torrentPath with torrentFileName
|
||||||
|
// There are a number of cases to handle here:
|
||||||
|
// * Originally we used absolute paths
|
||||||
|
// * Then, relative paths for the default torrents, eg '../static/sintel.torrent'
|
||||||
|
// * Then, paths computed at runtime for default torrents, eg 'sintel.torrent'
|
||||||
|
// * Finally, now we're getting rid of torrentPath altogether
|
||||||
var src, dst
|
var src, dst
|
||||||
if (ts.torrentPath) {
|
if (ts.torrentPath) {
|
||||||
// There are a number of cases to handle here:
|
|
||||||
// * Originally we used absolute paths
|
|
||||||
// * Then, relative paths for the default torrents, eg '../static/sintel.torrent'
|
|
||||||
// * Then, paths computed at runtime for default torrents, eg 'sintel.torrent'
|
|
||||||
// * Finally, now we're getting rid of torrentPath altogether
|
|
||||||
console.log('replacing torrentPath %s', ts.torrentPath)
|
console.log('replacing torrentPath %s', ts.torrentPath)
|
||||||
if (path.isAbsolute(ts.torrentPath)) {
|
if (path.isAbsolute(ts.torrentPath) || ts.torrentPath.startsWith('..')) {
|
||||||
src = ts.torrentPath
|
|
||||||
} else if (ts.torrentPath.startsWith('..')) {
|
|
||||||
src = ts.torrentPath
|
src = ts.torrentPath
|
||||||
} else {
|
} else {
|
||||||
src = path.join(config.STATIC_PATH, ts.torrentPath)
|
src = path.join(config.STATIC_PATH, ts.torrentPath)
|
||||||
}
|
}
|
||||||
dst = path.join(config.CONFIG_TORRENT_PATH, infoHash + '.torrent')
|
dst = path.join(config.TORRENT_PATH, infoHash + '.torrent')
|
||||||
// Synchronous FS calls aren't ideal, but probably OK in a migration
|
// Synchronous FS calls aren't ideal, but probably OK in a migration
|
||||||
// that only runs once
|
// that only runs once
|
||||||
if (src !== dst) fs.copySync(src, dst)
|
if (src !== dst) fs.copySync(src, dst)
|
||||||
@@ -71,7 +68,7 @@ function migrate_0_7_0 (state) {
|
|||||||
src = path.isAbsolute(ts.posterURL)
|
src = path.isAbsolute(ts.posterURL)
|
||||||
? ts.posterURL
|
? ts.posterURL
|
||||||
: path.join(config.STATIC_PATH, ts.posterURL)
|
: path.join(config.STATIC_PATH, ts.posterURL)
|
||||||
dst = path.join(config.CONFIG_POSTER_PATH, infoHash + extension)
|
dst = path.join(config.POSTER_PATH, infoHash + extension)
|
||||||
// Synchronous FS calls aren't ideal, but probably OK in a migration
|
// Synchronous FS calls aren't ideal, but probably OK in a migration
|
||||||
// that only runs once
|
// that only runs once
|
||||||
if (src !== dst) fs.copySync(src, dst)
|
if (src !== dst) fs.copySync(src, dst)
|
||||||
@@ -88,3 +85,11 @@ function migrate_0_7_0 (state) {
|
|||||||
delete ts.fileModtimes
|
delete ts.fileModtimes
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function migrate_0_7_2 (saved) {
|
||||||
|
if (!saved.prefs) {
|
||||||
|
saved.prefs = {
|
||||||
|
downloadPath: config.DEFAULT_DOWNLOAD_PATH
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,18 +1,22 @@
|
|||||||
var electron = require('electron')
|
var appConfig = require('application-config')('WebTorrent')
|
||||||
var path = require('path')
|
var path = require('path')
|
||||||
|
var {EventEmitter} = require('events')
|
||||||
var remote = electron.remote
|
|
||||||
|
|
||||||
var config = require('../../config')
|
var config = require('../../config')
|
||||||
var LocationHistory = require('./location-history')
|
var migrations = require('./migrations')
|
||||||
|
|
||||||
module.exports = {
|
var State = module.exports = Object.assign(new EventEmitter(), {
|
||||||
getInitialState,
|
|
||||||
getDefaultPlayState,
|
getDefaultPlayState,
|
||||||
getDefaultSavedState
|
load,
|
||||||
}
|
save,
|
||||||
|
saveThrottled
|
||||||
|
})
|
||||||
|
|
||||||
|
appConfig.filePath = path.join(config.CONFIG_PATH, 'config.json')
|
||||||
|
|
||||||
|
function getDefaultState () {
|
||||||
|
var LocationHistory = require('./location-history')
|
||||||
|
|
||||||
function getInitialState () {
|
|
||||||
return {
|
return {
|
||||||
/*
|
/*
|
||||||
* Temporary state disappears once the program exits.
|
* Temporary state disappears once the program exits.
|
||||||
@@ -30,10 +34,7 @@ function getInitialState () {
|
|||||||
},
|
},
|
||||||
selectedInfoHash: null, /* the torrent we've selected to view details. see state.torrents */
|
selectedInfoHash: null, /* the torrent we've selected to view details. see state.torrents */
|
||||||
playing: getDefaultPlayState(), /* the media (audio or video) that we're currently playing */
|
playing: getDefaultPlayState(), /* the media (audio or video) that we're currently playing */
|
||||||
devices: { /* playback devices like Chromecast and AppleTV */
|
devices: {}, /* playback devices like Chromecast and AppleTV */
|
||||||
airplay: null, /* airplay client. finds and manages AppleTVs */
|
|
||||||
chromecast: null /* chromecast client. finds and manages Chromecasts */
|
|
||||||
},
|
|
||||||
dock: {
|
dock: {
|
||||||
badge: 0,
|
badge: 0,
|
||||||
progress: 0
|
progress: 0
|
||||||
@@ -91,185 +92,58 @@ function getDefaultPlayState () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* If the saved state file doesn't exist yet, here's what we use instead */
|
/* If the saved state file doesn't exist yet, here's what we use instead */
|
||||||
function getDefaultSavedState () {
|
function setupSavedState (cb) {
|
||||||
return {
|
var fs = require('fs-extra')
|
||||||
version: config.APP_VERSION, /* make sure we can upgrade gracefully later */
|
var parseTorrent = require('parse-torrent')
|
||||||
torrents: [
|
var parallel = require('run-parallel')
|
||||||
{
|
|
||||||
status: 'paused',
|
var saved = {
|
||||||
infoHash: '88594aaacbde40ef3e2510c47374ec0aa396c08e',
|
|
||||||
magnetURI: 'magnet:?xt=urn:btih:88594aaacbde40ef3e2510c47374ec0aa396c08e&dn=bbb_sunflower_1080p_30fps_normal.mp4&tr=udp%3A%2F%2Ftracker.openbittorrent.com%3A80%2Fannounce&tr=udp%3A%2F%2Ftracker.publicbt.com%3A80%2Fannounce&tr=wss%3A%2F%2Ftracker.btorrent.xyz&tr=wss%3A%2F%2Ftracker.fastcast.nz&tr=wss%3A%2F%2Ftracker.openwebtorrent.com&tr=wss%3A%2F%2Ftracker.webtorrent.io&ws=http%3A%2F%2Fdistribution.bbb3d.renderfarming.net%2Fvideo%2Fmp4%2Fbbb_sunflower_1080p_30fps_normal.mp4',
|
|
||||||
displayName: 'Big Buck Bunny',
|
|
||||||
posterURL: 'bigBuckBunny.jpg',
|
|
||||||
torrentPath: 'bigBuckBunny.torrent',
|
|
||||||
files: [
|
|
||||||
{
|
|
||||||
length: 276134947,
|
|
||||||
name: 'bbb_sunflower_1080p_30fps_normal.mp4'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
status: 'paused',
|
|
||||||
infoHash: '6a9759bffd5c0af65319979fb7832189f4f3c35d',
|
|
||||||
magnetURI: 'magnet:?xt=urn:btih:6a9759bffd5c0af65319979fb7832189f4f3c35d&dn=sintel.mp4&tr=udp%3A%2F%2Fexodus.desync.com%3A6969&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969&tr=udp%3A%2F%2Ftracker.internetwarriors.net%3A1337&tr=udp%3A%2F%2Ftracker.leechers-paradise.org%3A6969&tr=udp%3A%2F%2Ftracker.openbittorrent.com%3A80&tr=wss%3A%2F%2Ftracker.btorrent.xyz&tr=wss%3A%2F%2Ftracker.fastcast.nz&tr=wss%3A%2F%2Ftracker.openwebtorrent.com&tr=wss%3A%2F%2Ftracker.webtorrent.io&ws=https%3A%2F%2Fwebtorrent.io%2Ftorrents%2Fsintel-1024-surround.mp4',
|
|
||||||
displayName: 'Sintel',
|
|
||||||
posterURL: 'sintel.jpg',
|
|
||||||
torrentPath: 'sintel.torrent',
|
|
||||||
files: [
|
|
||||||
{
|
|
||||||
length: 129241752,
|
|
||||||
name: 'sintel.mp4'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
status: 'paused',
|
|
||||||
infoHash: '02767050e0be2fd4db9a2ad6c12416ac806ed6ed',
|
|
||||||
magnetURI: 'magnet:?xt=urn:btih:02767050e0be2fd4db9a2ad6c12416ac806ed6ed&dn=tears_of_steel_1080p.webm&tr=wss%3A%2F%2Ftracker.btorrent.xyz&tr=wss%3A%2F%2Ftracker.fastcast.nz&tr=wss%3A%2F%2Ftracker.openwebtorrent.com&tr=wss%3A%2F%2Ftracker.webtorrent.io',
|
|
||||||
displayName: 'Tears of Steel',
|
|
||||||
posterURL: 'tearsOfSteel.jpg',
|
|
||||||
torrentPath: 'tearsOfSteel.torrent',
|
|
||||||
files: [
|
|
||||||
{
|
|
||||||
length: 571346576,
|
|
||||||
name: 'tears_of_steel_1080p.webm'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
status: 'paused',
|
|
||||||
infoHash: '6a02592d2bbc069628cd5ed8a54f88ee06ac0ba5',
|
|
||||||
magnetURI: 'magnet:?xt=urn:btih:6a02592d2bbc069628cd5ed8a54f88ee06ac0ba5&dn=CosmosLaundromatFirstCycle&tr=http%3A%2F%2Fbt1.archive.org%3A6969%2Fannounce&tr=http%3A%2F%2Fbt2.archive.org%3A6969%2Fannounce&tr=wss%3A%2F%2Ftracker.btorrent.xyz&tr=wss%3A%2F%2Ftracker.fastcast.nz&tr=wss%3A%2F%2Ftracker.openwebtorrent.com&tr=wss%3A%2F%2Ftracker.webtorrent.io&ws=http%3A%2F%2Fia601508.us.archive.org%2F14%2Fitems%2F&ws=http%3A%2F%2Fia801508.us.archive.org%2F14%2Fitems%2F&ws=https%3A%2F%2Farchive.org%2Fdownload%2F',
|
|
||||||
displayName: 'Cosmos Laundromat (Preview)',
|
|
||||||
posterURL: 'cosmosLaundromat.jpg',
|
|
||||||
torrentPath: 'cosmosLaundromat.torrent',
|
|
||||||
files: [
|
|
||||||
{
|
|
||||||
length: 223580,
|
|
||||||
name: 'Cosmos Laundromat - First Cycle (1080p).gif'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
length: 220087570,
|
|
||||||
name: 'Cosmos Laundromat - First Cycle (1080p).mp4'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
length: 56832560,
|
|
||||||
name: 'Cosmos Laundromat - First Cycle (1080p).ogv'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
length: 3949,
|
|
||||||
name: 'CosmosLaundromat-FirstCycle1080p.en.srt'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
length: 3907,
|
|
||||||
name: 'CosmosLaundromat-FirstCycle1080p.es.srt'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
length: 4119,
|
|
||||||
name: 'CosmosLaundromat-FirstCycle1080p.fr.srt'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
length: 3941,
|
|
||||||
name: 'CosmosLaundromat-FirstCycle1080p.it.srt'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
length: 11264,
|
|
||||||
name: 'CosmosLaundromatFirstCycle_meta.sqlite'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
length: 1204,
|
|
||||||
name: 'CosmosLaundromatFirstCycle_meta.xml'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
status: 'paused',
|
|
||||||
infoHash: '3ba219a8634bf7bae3d848192b2da75ae995589d',
|
|
||||||
magnetURI: 'magnet:?xt=urn:btih:3ba219a8634bf7bae3d848192b2da75ae995589d&dn=The+WIRED+CD+-+Rip.+Sample.+Mash.+Share.&tr=udp%3A%2F%2Fexodus.desync.com%3A6969&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969&tr=udp%3A%2F%2Ftracker.internetwarriors.net%3A1337&tr=udp%3A%2F%2Ftracker.leechers-paradise.org%3A6969&tr=udp%3A%2F%2Ftracker.openbittorrent.com%3A80&tr=wss%3A%2F%2Ftracker.btorrent.xyz&tr=wss%3A%2F%2Ftracker.fastcast.nz&tr=wss%3A%2F%2Ftracker.openwebtorrent.com&tr=wss%3A%2F%2Ftracker.webtorrent.io&ws=https%3A%2F%2Fwebtorrent.io%2Ftorrents%2F',
|
|
||||||
displayName: 'The WIRED CD - Rip. Sample. Mash. Share.',
|
|
||||||
posterURL: 'wired-cd.jpg',
|
|
||||||
torrentPath: 'wired-cd.torrent',
|
|
||||||
files: [
|
|
||||||
{
|
|
||||||
length: 1964275,
|
|
||||||
name: '01 - Beastie Boys - Now Get Busy.mp3'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
length: 3610523,
|
|
||||||
name: '02 - David Byrne - My Fair Lady.mp3'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
length: 2759377,
|
|
||||||
name: '03 - Zap Mama - Wadidyusay.mp3'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
length: 5816537,
|
|
||||||
name: '04 - My Morning Jacket - One Big Holiday.mp3'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
length: 2106421,
|
|
||||||
name: '05 - Spoon - Revenge!.mp3'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
length: 3347550,
|
|
||||||
name: '06 - Gilberto Gil - Oslodum.mp3'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
length: 2107577,
|
|
||||||
name: '07 - Dan The Automator - Relaxation Spa Treatment.mp3'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
length: 3108130,
|
|
||||||
name: '08 - Thievery Corporation - Dc 3000.mp3'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
length: 3051528,
|
|
||||||
name: '09 - Le Tigre - Fake French.mp3'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
length: 3270259,
|
|
||||||
name: '10 - Paul Westerberg - Looking Up In Heaven.mp3'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
length: 3263528,
|
|
||||||
name: '11 - Chuck D - No Meaning No (feat. Fine Arts Militia).mp3'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
length: 6380952,
|
|
||||||
name: '12 - The Rapture - Sister Saviour (Blackstrobe Remix).mp3'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
length: 6550396,
|
|
||||||
name: '13 - Cornelius - Wataridori 2.mp3'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
length: 3034692,
|
|
||||||
name: '14 - DJ Danger Mouse - What U Sittin\' On (feat. Jemini, Cee Lo And Tha Alkaholiks).mp3'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
length: 3854611,
|
|
||||||
name: '15 - DJ Dolores - Oslodum 2004.mp3'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
length: 1762120,
|
|
||||||
name: '16 - Matmos - Action At A Distance.mp3'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
length: 4071,
|
|
||||||
name: 'README.md'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
length: 78163,
|
|
||||||
name: 'poster.jpg'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
prefs: {
|
prefs: {
|
||||||
downloadPath: config.IS_PORTABLE
|
downloadPath: config.DEFAULT_DOWNLOAD_PATH
|
||||||
? path.join(config.CONFIG_PATH, 'Downloads')
|
},
|
||||||
: remote.app.getPath('downloads')
|
torrents: config.DEFAULT_TORRENTS.map(createTorrentObject),
|
||||||
|
version: config.APP_VERSION /* make sure we can upgrade gracefully later */
|
||||||
|
}
|
||||||
|
|
||||||
|
var tasks = []
|
||||||
|
|
||||||
|
config.DEFAULT_TORRENTS.map(function (t, i) {
|
||||||
|
var infoHash = saved.torrents[i].infoHash
|
||||||
|
tasks.push(function (cb) {
|
||||||
|
fs.copy(
|
||||||
|
path.join(config.STATIC_PATH, t.posterFileName),
|
||||||
|
path.join(config.POSTER_PATH, infoHash + path.extname(t.posterFileName)),
|
||||||
|
cb
|
||||||
|
)
|
||||||
|
})
|
||||||
|
tasks.push(function (cb) {
|
||||||
|
fs.copy(
|
||||||
|
path.join(config.STATIC_PATH, t.torrentFileName),
|
||||||
|
path.join(config.TORRENT_PATH, infoHash + '.torrent'),
|
||||||
|
cb
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
parallel(tasks, function (err) {
|
||||||
|
if (err) return cb(err)
|
||||||
|
cb(null, saved)
|
||||||
|
})
|
||||||
|
|
||||||
|
function createTorrentObject (t) {
|
||||||
|
var torrent = fs.readFileSync(path.join(config.STATIC_PATH, t.torrentFileName))
|
||||||
|
var parsedTorrent = parseTorrent(torrent)
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: 'paused',
|
||||||
|
infoHash: parsedTorrent.infoHash,
|
||||||
|
name: t.name,
|
||||||
|
displayName: t.name,
|
||||||
|
posterFileName: parsedTorrent.infoHash + path.extname(t.posterFileName),
|
||||||
|
torrentFileName: parsedTorrent.infoHash + '.torrent',
|
||||||
|
magnetURI: parseTorrent.toMagnetURI(parsedTorrent),
|
||||||
|
files: parsedTorrent.files,
|
||||||
|
selections: parsedTorrent.files.map((x) => true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -284,3 +158,63 @@ function getPlayingFileSummary () {
|
|||||||
if (!torrentSummary) return null
|
if (!torrentSummary) return null
|
||||||
return torrentSummary.files[this.playing.fileIndex]
|
return torrentSummary.files[this.playing.fileIndex]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function load (cb) {
|
||||||
|
var state = getDefaultState()
|
||||||
|
|
||||||
|
appConfig.read(function (err, saved) {
|
||||||
|
if (err || !saved.version) {
|
||||||
|
console.log('Missing config file: Creating new one')
|
||||||
|
setupSavedState(onSaved)
|
||||||
|
} else {
|
||||||
|
onSaved(null, saved)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function onSaved (err, saved) {
|
||||||
|
if (err) return cb(err)
|
||||||
|
state.saved = saved
|
||||||
|
migrations.run(state)
|
||||||
|
cb(null, state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write state.saved to the JSON state file
|
||||||
|
function save (state, cb) {
|
||||||
|
console.log('Saving state to ' + appConfig.filePath)
|
||||||
|
delete state.saveStateTimeout
|
||||||
|
|
||||||
|
// Clean up, so that we're not saving any pending state
|
||||||
|
var copy = Object.assign({}, state.saved)
|
||||||
|
// Remove torrents pending addition to the list, where we haven't finished
|
||||||
|
// reading the torrent file or file(s) to seed & don't have an infohash
|
||||||
|
copy.torrents = copy.torrents
|
||||||
|
.filter((x) => x.infoHash)
|
||||||
|
.map(function (x) {
|
||||||
|
var torrent = {}
|
||||||
|
for (var key in x) {
|
||||||
|
if (key === 'progress' || key === 'torrentKey') {
|
||||||
|
continue // Don't save progress info or key for the webtorrent process
|
||||||
|
}
|
||||||
|
if (key === 'playStatus') {
|
||||||
|
continue // Don't save whether a torrent is playing / pending
|
||||||
|
}
|
||||||
|
torrent[key] = x[key]
|
||||||
|
}
|
||||||
|
return torrent
|
||||||
|
})
|
||||||
|
|
||||||
|
appConfig.write(copy, (err) => {
|
||||||
|
if (err) console.error(err)
|
||||||
|
else State.emit('savedState')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write, but no more than once a second
|
||||||
|
function saveThrottled (state) {
|
||||||
|
if (state.saveStateTimeout) return
|
||||||
|
state.saveStateTimeout = setTimeout(function () {
|
||||||
|
if (!state.saveStateTimeout) return
|
||||||
|
save(state)
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
|||||||
152
renderer/lib/telemetry.js
Normal file
152
renderer/lib/telemetry.js
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
// Collects anonymous usage stats and uncaught errors
|
||||||
|
// Reports back so that we can improve WebTorrent Desktop
|
||||||
|
module.exports = {
|
||||||
|
init,
|
||||||
|
logUncaughtError,
|
||||||
|
logPlayAttempt
|
||||||
|
}
|
||||||
|
|
||||||
|
const crypto = require('crypto')
|
||||||
|
const electron = require('electron')
|
||||||
|
const https = require('https')
|
||||||
|
const os = require('os')
|
||||||
|
const url = require('url')
|
||||||
|
|
||||||
|
const config = require('../../config')
|
||||||
|
|
||||||
|
var telemetry
|
||||||
|
|
||||||
|
function init (state) {
|
||||||
|
telemetry = state.saved.telemetry
|
||||||
|
if (!telemetry) {
|
||||||
|
telemetry = state.saved.telemetry = createSummary()
|
||||||
|
reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
var now = new Date()
|
||||||
|
telemetry.timestamp = now.toISOString()
|
||||||
|
telemetry.localTime = now.toTimeString()
|
||||||
|
telemetry.screens = getScreenInfo()
|
||||||
|
telemetry.system = getSystemInfo()
|
||||||
|
telemetry.approxNumTorrents = getApproxNumTorrents(state)
|
||||||
|
|
||||||
|
if (config.IS_PRODUCTION) {
|
||||||
|
postToServer()
|
||||||
|
} else {
|
||||||
|
// Development: telemetry used only for local debugging
|
||||||
|
// Empty uncaught errors, etc at the start of every run
|
||||||
|
reset()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function reset () {
|
||||||
|
telemetry.uncaughtErrors = []
|
||||||
|
telemetry.playAttempts = {
|
||||||
|
total: 0,
|
||||||
|
success: 0,
|
||||||
|
timeout: 0,
|
||||||
|
error: 0,
|
||||||
|
abandoned: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function postToServer () {
|
||||||
|
// Serialize the telemetry summary
|
||||||
|
var payload = new Buffer(JSON.stringify(telemetry), 'utf8')
|
||||||
|
|
||||||
|
// POST to our server
|
||||||
|
var options = url.parse(config.TELEMETRY_URL)
|
||||||
|
options.method = 'POST'
|
||||||
|
options.headers = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Content-Length': payload.length
|
||||||
|
}
|
||||||
|
|
||||||
|
var req = https.request(options, function (res) {
|
||||||
|
if (res.statusCode === 200) {
|
||||||
|
console.log('Successfully posted telemetry summary')
|
||||||
|
reset()
|
||||||
|
} else {
|
||||||
|
console.error('Couldn\'t post telemetry summary, got HTTP ' + res.statusCode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
req.on('error', function (e) {
|
||||||
|
console.error('Couldn\'t post telemetry summary', e)
|
||||||
|
})
|
||||||
|
req.write(payload)
|
||||||
|
req.end()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creates a new telemetry summary. Gives the user a unique ID,
|
||||||
|
// collects screen resolution, etc
|
||||||
|
function createSummary () {
|
||||||
|
// Make a 256-bit random unique ID
|
||||||
|
var userID = crypto.randomBytes(32).toString('hex')
|
||||||
|
return { userID }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track screen resolution
|
||||||
|
function getScreenInfo () {
|
||||||
|
return electron.screen.getAllDisplays().map((screen) => ({
|
||||||
|
width: screen.size.width,
|
||||||
|
height: screen.size.height,
|
||||||
|
scaleFactor: screen.scaleFactor
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track basic system info like OS version and amount of RAM
|
||||||
|
function getSystemInfo () {
|
||||||
|
return {
|
||||||
|
osPlatform: process.platform,
|
||||||
|
osRelease: os.type() + ' ' + os.release(),
|
||||||
|
architecture: os.arch(),
|
||||||
|
totalMemoryMB: os.totalmem() / (1 << 20),
|
||||||
|
numCores: os.cpus().length
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the number of torrents, rounded to the nearest power of two
|
||||||
|
function getApproxNumTorrents (state) {
|
||||||
|
var exactNum = state.saved.torrents.length
|
||||||
|
if (exactNum === 0) return 0
|
||||||
|
// Otherwise, return 1, 2, 4, 8, etc by rounding in log space
|
||||||
|
var log2 = Math.log(exactNum) / Math.log(2)
|
||||||
|
return 1 << Math.round(log2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// An uncaught error happened in the main process or in one of the windows
|
||||||
|
function logUncaughtError (procName, err) {
|
||||||
|
console.error('uncaught error', procName, err)
|
||||||
|
|
||||||
|
// Not initialized yet? Ignore.
|
||||||
|
// Hopefully uncaught errors immediately on startup are fixed in dev
|
||||||
|
if (!telemetry) return
|
||||||
|
|
||||||
|
var message, stack
|
||||||
|
if (err instanceof Error) {
|
||||||
|
message = err.message
|
||||||
|
stack = err.stack
|
||||||
|
} else {
|
||||||
|
message = String(err)
|
||||||
|
stack = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// We need to POST the telemetry object, make sure it stays < 100kb
|
||||||
|
if (telemetry.uncaughtErrors.length > 20) return
|
||||||
|
if (message.length > 1000) message = message.substring(0, 1000)
|
||||||
|
if (stack.length > 1000) stack = stack.substring(0, 1000)
|
||||||
|
|
||||||
|
telemetry.uncaughtErrors.push({process: procName, message, stack})
|
||||||
|
}
|
||||||
|
|
||||||
|
// The user pressed play. It either worked, timed out, or showed the
|
||||||
|
// "Play in VLC" codec error
|
||||||
|
function logPlayAttempt (result) {
|
||||||
|
if (!['success', 'timeout', 'error', 'abandoned'].includes(result)) {
|
||||||
|
return console.error('Unknown play attempt result', result)
|
||||||
|
}
|
||||||
|
|
||||||
|
var attempts = telemetry.playAttempts
|
||||||
|
attempts.total = (attempts.total || 0) + 1
|
||||||
|
attempts[result] = (attempts[result] || 0) + 1
|
||||||
|
}
|
||||||
@@ -2,20 +2,21 @@ module.exports = {
|
|||||||
isPlayable,
|
isPlayable,
|
||||||
isVideo,
|
isVideo,
|
||||||
isAudio,
|
isAudio,
|
||||||
isPlayableTorrent
|
isTorrent,
|
||||||
|
isPlayableTorrentSummary,
|
||||||
|
pickFileToPlay
|
||||||
}
|
}
|
||||||
|
|
||||||
var path = require('path')
|
var path = require('path')
|
||||||
|
|
||||||
/**
|
// Checks whether a fileSummary or file path is audio/video that we can play,
|
||||||
* Determines whether a file in a torrent is audio/video we can play
|
// based on the file extension
|
||||||
*/
|
|
||||||
function isPlayable (file) {
|
function isPlayable (file) {
|
||||||
return isVideo(file) || isAudio(file)
|
return isVideo(file) || isAudio(file)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Checks whether a fileSummary or file path is playable video
|
||||||
function isVideo (file) {
|
function isVideo (file) {
|
||||||
var ext = path.extname(file.name).toLowerCase()
|
|
||||||
return [
|
return [
|
||||||
'.avi',
|
'.avi',
|
||||||
'.m4v',
|
'.m4v',
|
||||||
@@ -24,21 +25,59 @@ function isVideo (file) {
|
|||||||
'.mp4',
|
'.mp4',
|
||||||
'.mpg',
|
'.mpg',
|
||||||
'.ogv',
|
'.ogv',
|
||||||
'.webm'
|
'.webm',
|
||||||
].includes(ext)
|
'.wmv'
|
||||||
|
].includes(getFileExtension(file))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Checks whether a fileSummary or file path is playable audio
|
||||||
function isAudio (file) {
|
function isAudio (file) {
|
||||||
var ext = path.extname(file.name).toLowerCase()
|
|
||||||
return [
|
return [
|
||||||
'.aac',
|
'.aac',
|
||||||
'.ac3',
|
'.ac3',
|
||||||
'.mp3',
|
'.mp3',
|
||||||
'.ogg',
|
'.ogg',
|
||||||
'.wav'
|
'.wav'
|
||||||
].includes(ext)
|
].includes(getFileExtension(file))
|
||||||
}
|
}
|
||||||
|
|
||||||
function isPlayableTorrent (torrentSummary) {
|
// Checks if the argument is either:
|
||||||
|
// - a string that's a valid filename ending in .torrent
|
||||||
|
// - a file object where obj.name is ends in .torrent
|
||||||
|
// - a string that's a magnet link (magnet://...)
|
||||||
|
function isTorrent (file) {
|
||||||
|
var isTorrentFile = getFileExtension(file) === '.torrent'
|
||||||
|
var isMagnet = typeof file === 'string' && /^(stream-)?magnet:/.test(file)
|
||||||
|
return isTorrentFile || isMagnet
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFileExtension (file) {
|
||||||
|
var name = typeof file === 'string' ? file : file.name
|
||||||
|
return path.extname(name).toLowerCase()
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPlayableTorrentSummary (torrentSummary) {
|
||||||
return torrentSummary.files && torrentSummary.files.some(isPlayable)
|
return torrentSummary.files && torrentSummary.files.some(isPlayable)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Picks the default file to play from a list of torrent or torrentSummary files
|
||||||
|
// Returns an index or undefined, if no files are playable
|
||||||
|
function pickFileToPlay (files) {
|
||||||
|
// first, try to find the biggest video file
|
||||||
|
var videoFiles = files.filter(isVideo)
|
||||||
|
if (videoFiles.length > 0) {
|
||||||
|
var largestVideoFile = videoFiles.reduce(function (a, b) {
|
||||||
|
return a.length > b.length ? a : b
|
||||||
|
})
|
||||||
|
return files.indexOf(largestVideoFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
// if there are no videos, play the first audio file
|
||||||
|
var audioFiles = files.filter(isAudio)
|
||||||
|
if (audioFiles.length > 0) {
|
||||||
|
return files.indexOf(audioFiles[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
// no video or audio means nothing is playable
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ function torrentPoster (torrent, cb) {
|
|||||||
if (videoFile) return torrentPosterFromVideo(videoFile, torrent, cb)
|
if (videoFile) return torrentPosterFromVideo(videoFile, torrent, cb)
|
||||||
|
|
||||||
// Third, try to use the largest image file
|
// Third, try to use the largest image file
|
||||||
var imgFile = getLargestFileByExtension(torrent, ['.gif', '.jpg', '.png'])
|
var imgFile = getLargestFileByExtension(torrent, ['.gif', '.jpg', '.jpeg', '.png'])
|
||||||
if (imgFile) return torrentPosterFromImage(imgFile, torrent, cb)
|
if (imgFile) return torrentPosterFromImage(imgFile, torrent, cb)
|
||||||
|
|
||||||
// TODO: generate a waveform from the largest sound file
|
// TODO: generate a waveform from the largest sound file
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
getPosterPath,
|
getPosterPath,
|
||||||
getTorrentPath
|
getTorrentPath,
|
||||||
|
getByKey,
|
||||||
|
getTorrentID,
|
||||||
|
getFileOrFolder
|
||||||
}
|
}
|
||||||
|
|
||||||
var path = require('path')
|
var path = require('path')
|
||||||
@@ -10,15 +13,44 @@ var config = require('../../config')
|
|||||||
// Returns an absolute path to the torrent file, or null if unavailable
|
// Returns an absolute path to the torrent file, or null if unavailable
|
||||||
function getTorrentPath (torrentSummary) {
|
function getTorrentPath (torrentSummary) {
|
||||||
if (!torrentSummary || !torrentSummary.torrentFileName) return null
|
if (!torrentSummary || !torrentSummary.torrentFileName) return null
|
||||||
return path.join(config.CONFIG_TORRENT_PATH, torrentSummary.torrentFileName)
|
return path.join(config.TORRENT_PATH, torrentSummary.torrentFileName)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Expects a torrentSummary
|
// Expects a torrentSummary
|
||||||
// Returns an absolute path to the poster image, or null if unavailable
|
// Returns an absolute path to the poster image, or null if unavailable
|
||||||
function getPosterPath (torrentSummary) {
|
function getPosterPath (torrentSummary) {
|
||||||
if (!torrentSummary || !torrentSummary.posterFileName) return null
|
if (!torrentSummary || !torrentSummary.posterFileName) return null
|
||||||
var posterPath = path.join(config.CONFIG_POSTER_PATH, torrentSummary.posterFileName)
|
var posterPath = path.join(config.POSTER_PATH, torrentSummary.posterFileName)
|
||||||
// Work around a Chrome bug (reproduced in vanilla Chrome, not just Electron):
|
// Work around a Chrome bug (reproduced in vanilla Chrome, not just Electron):
|
||||||
// Backslashes in URLS in CSS cause bizarre string encoding issues
|
// Backslashes in URLS in CSS cause bizarre string encoding issues
|
||||||
return posterPath.replace(/\\/g, '/')
|
return posterPath.replace(/\\/g, '/')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Expects a torrentSummary
|
||||||
|
// Returns a torrentID: filename, magnet URI, or infohash
|
||||||
|
function getTorrentID (torrentSummary) {
|
||||||
|
var s = torrentSummary
|
||||||
|
if (s.torrentFileName) { // Load torrent file from disk
|
||||||
|
return getTorrentPath(s)
|
||||||
|
} else { // Load torrent from DHT
|
||||||
|
return s.magnetURI || s.infoHash
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expects a torrentKey or infoHash
|
||||||
|
// Returns the corresponding torrentSummary, or undefined
|
||||||
|
function getByKey (state, torrentKey) {
|
||||||
|
if (!torrentKey) return undefined
|
||||||
|
return state.saved.torrents.find((x) =>
|
||||||
|
x.torrentKey === torrentKey || x.infoHash === torrentKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the path to either the file (in a single-file torrent) or the root
|
||||||
|
// folder (in multi-file torrent)
|
||||||
|
// WARNING: assumes that multi-file torrents consist of a SINGLE folder.
|
||||||
|
// TODO: make this assumption explicit, enforce it in the `create-torrent`
|
||||||
|
// module. Store root folder explicitly to avoid hacky path processing below.
|
||||||
|
function getFileOrFolder (torrentSummary) {
|
||||||
|
var ts = torrentSummary
|
||||||
|
return path.join(ts.path, ts.files[0].path.split('/')[0])
|
||||||
|
}
|
||||||
|
|||||||
@@ -260,7 +260,7 @@ table {
|
|||||||
|
|
||||||
.modal .modal-content {
|
.modal .modal-content {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 45px;
|
top: 38px;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
@@ -326,7 +326,6 @@ table {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.create-torrent input.torrent-is-private {
|
.create-torrent input.torrent-is-private {
|
||||||
width: initial;
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -404,7 +403,7 @@ button.button-raised:active {
|
|||||||
* OTHER FORM ELEMENT DEFAULTS
|
* OTHER FORM ELEMENT DEFAULTS
|
||||||
*/
|
*/
|
||||||
|
|
||||||
input {
|
input[type='text'] {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
width: 300px;
|
width: 300px;
|
||||||
padding: 6px;
|
padding: 6px;
|
||||||
@@ -848,7 +847,7 @@ body.drag .app::after {
|
|||||||
height: 14px;
|
height: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.player .controls .subtitles-list {
|
.player .controls .options-list {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
background: rgba(40, 40, 40, 0.8);
|
background: rgba(40, 40, 40, 0.8);
|
||||||
min-width: 100px;
|
min-width: 100px;
|
||||||
@@ -862,7 +861,7 @@ body.drag .app::after {
|
|||||||
color: rgba(255, 255, 255, 0.8);
|
color: rgba(255, 255, 255, 0.8);
|
||||||
}
|
}
|
||||||
|
|
||||||
.player .controls .subtitles-list .icon {
|
.player .controls .options-list .icon {
|
||||||
display: inline;
|
display: inline;
|
||||||
font-size: 17px;
|
font-size: 17px;
|
||||||
vertical-align: bottom;
|
vertical-align: bottom;
|
||||||
|
|||||||
1320
renderer/main.js
1320
renderer/main.js
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,7 @@ var hx = require('../lib/hx')
|
|||||||
var Header = require('./header')
|
var Header = require('./header')
|
||||||
|
|
||||||
var Views = {
|
var Views = {
|
||||||
'home': require('./home'),
|
'home': require('./torrent-list'),
|
||||||
'player': require('./player'),
|
'player': require('./player'),
|
||||||
'create-torrent': require('./create-torrent'),
|
'create-torrent': require('./create-torrent'),
|
||||||
'preferences': require('./preferences')
|
'preferences': require('./preferences')
|
||||||
@@ -12,6 +12,7 @@ var Views = {
|
|||||||
|
|
||||||
var Modals = {
|
var Modals = {
|
||||||
'open-torrent-address-modal': require('./open-torrent-address-modal'),
|
'open-torrent-address-modal': require('./open-torrent-address-modal'),
|
||||||
|
'remove-torrent-modal': require('./remove-torrent-modal'),
|
||||||
'update-available-modal': require('./update-available-modal'),
|
'update-available-modal': require('./update-available-modal'),
|
||||||
'unsupported-media-modal': require('./unsupported-media-modal')
|
'unsupported-media-modal': require('./unsupported-media-modal')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,7 +65,8 @@ function CreateTorrentPage (state) {
|
|||||||
<label>Path:</label>
|
<label>Path:</label>
|
||||||
<div class='torrent-attribute'>${pathPrefix}</div>
|
<div class='torrent-attribute'>${pathPrefix}</div>
|
||||||
</p>
|
</p>
|
||||||
<div class='expand-collapse ${collapsedClass}' onclick=${handleToggleShowAdvanced}>
|
<div class='expand-collapse ${collapsedClass}'
|
||||||
|
onclick=${dispatcher('toggleCreateTorrentAdvanced')}>
|
||||||
${info.showAdvanced ? 'Basic' : 'Advanced'}
|
${info.showAdvanced ? 'Basic' : 'Advanced'}
|
||||||
</div>
|
</div>
|
||||||
<div class="create-torrent-advanced ${collapsedClass}">
|
<div class="create-torrent-advanced ${collapsedClass}">
|
||||||
@@ -87,7 +88,7 @@ function CreateTorrentPage (state) {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<p class="float-right">
|
<p class="float-right">
|
||||||
<button class='button-flat light' onclick=${handleCancel}>Cancel</button>
|
<button class='button-flat light' onclick=${dispatcher('back')}>Cancel</button>
|
||||||
<button class='button-raised' onclick=${handleOK}>Create Torrent</button>
|
<button class='button-raised' onclick=${handleOK}>Create Torrent</button>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -114,17 +115,6 @@ function CreateTorrentPage (state) {
|
|||||||
}
|
}
|
||||||
dispatch('createTorrent', options)
|
dispatch('createTorrent', options)
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleCancel () {
|
|
||||||
dispatch('back')
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleToggleShowAdvanced () {
|
|
||||||
// TODO: what's the clean way to handle this?
|
|
||||||
// Should every button on every screen have its own dispatch()?
|
|
||||||
info.showAdvanced = !info.showAdvanced
|
|
||||||
dispatch('update')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function CreateTorrentErrorPage () {
|
function CreateTorrentErrorPage () {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
module.exports = OpenTorrentAddressModal
|
module.exports = OpenTorrentAddressModal
|
||||||
|
|
||||||
var {dispatch} = require('../lib/dispatcher')
|
var {dispatch, dispatcher} = require('../lib/dispatcher')
|
||||||
var hx = require('../lib/hx')
|
var hx = require('../lib/hx')
|
||||||
|
|
||||||
function OpenTorrentAddressModal (state) {
|
function OpenTorrentAddressModal (state) {
|
||||||
@@ -11,8 +11,8 @@ function OpenTorrentAddressModal (state) {
|
|||||||
<input id='add-torrent-url' type='text' onkeypress=${handleKeyPress} />
|
<input id='add-torrent-url' type='text' onkeypress=${handleKeyPress} />
|
||||||
</p>
|
</p>
|
||||||
<p class='float-right'>
|
<p class='float-right'>
|
||||||
<button class='button button-flat' onclick=${handleCancel}>CANCEL</button>
|
<button class='button button-flat' onclick=${dispatcher('exitModal')}>Cancel</button>
|
||||||
<button class='button button-raised' onclick=${handleOK}>OK</button>
|
<button class='button button-raised' onclick=${handleOK}>OK</button>
|
||||||
</p>
|
</p>
|
||||||
<script>document.querySelector('#add-torrent-url').focus()</script>
|
<script>document.querySelector('#add-torrent-url').focus()</script>
|
||||||
</div>
|
</div>
|
||||||
@@ -27,7 +27,3 @@ function handleOK () {
|
|||||||
dispatch('exitModal')
|
dispatch('exitModal')
|
||||||
dispatch('addTorrent', document.querySelector('#add-torrent-url').value)
|
dispatch('addTorrent', document.querySelector('#add-torrent-url').value)
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleCancel () {
|
|
||||||
dispatch('exitModal')
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ function renderMedia (state) {
|
|||||||
mediaElement.play()
|
mediaElement.play()
|
||||||
}
|
}
|
||||||
// When the user clicks or drags on the progress bar, jump to that position
|
// When the user clicks or drags on the progress bar, jump to that position
|
||||||
if (state.playing.jumpToTime) {
|
if (state.playing.jumpToTime != null) {
|
||||||
mediaElement.currentTime = state.playing.jumpToTime
|
mediaElement.currentTime = state.playing.jumpToTime
|
||||||
state.playing.jumpToTime = null
|
state.playing.jumpToTime = null
|
||||||
}
|
}
|
||||||
@@ -73,6 +73,15 @@ function renderMedia (state) {
|
|||||||
var file = state.getPlayingFileSummary()
|
var file = state.getPlayingFileSummary()
|
||||||
file.currentTime = state.playing.currentTime = mediaElement.currentTime
|
file.currentTime = state.playing.currentTime = mediaElement.currentTime
|
||||||
file.duration = state.playing.duration = mediaElement.duration
|
file.duration = state.playing.duration = mediaElement.duration
|
||||||
|
|
||||||
|
// Save selected subtitle
|
||||||
|
if (state.playing.subtitles.selectedIndex !== -1) {
|
||||||
|
var index = state.playing.subtitles.selectedIndex
|
||||||
|
file.selectedSubtitle = state.playing.subtitles.tracks[index].filePath
|
||||||
|
} else if (file.selectedSubtitle != null) {
|
||||||
|
delete file.selectedSubtitle
|
||||||
|
}
|
||||||
|
|
||||||
state.playing.volume = mediaElement.volume
|
state.playing.volume = mediaElement.volume
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,6 +152,7 @@ function renderMedia (state) {
|
|||||||
} else if (elem.webkitAudioDecodedByteCount === 0) {
|
} else if (elem.webkitAudioDecodedByteCount === 0) {
|
||||||
dispatch('mediaError', 'Audio codec unsupported')
|
dispatch('mediaError', 'Audio codec unsupported')
|
||||||
} else {
|
} else {
|
||||||
|
dispatch('mediaSuccess')
|
||||||
elem.play()
|
elem.play()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -279,8 +289,10 @@ function renderCastScreen (state) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var isStarting = state.playing.location.endsWith('-pending')
|
var isStarting = state.playing.location.endsWith('-pending')
|
||||||
|
var castName = state.playing.castName
|
||||||
var castStatus
|
var castStatus
|
||||||
if (isCast) castStatus = isStarting ? 'Connecting...' : 'Connected'
|
if (isCast && isStarting) castStatus = 'Connecting to ' + castName + '...'
|
||||||
|
else if (isCast && !isStarting) castStatus = 'Connected to ' + castName
|
||||||
else castStatus = ''
|
else castStatus = ''
|
||||||
|
|
||||||
// Show a nice title image, if possible
|
// Show a nice title image, if possible
|
||||||
@@ -299,6 +311,30 @@ function renderCastScreen (state) {
|
|||||||
`
|
`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderCastOptions (state) {
|
||||||
|
if (!state.devices.castMenu) return
|
||||||
|
|
||||||
|
var {location, devices} = state.devices.castMenu
|
||||||
|
var player = state.devices[location]
|
||||||
|
|
||||||
|
var items = devices.map(function (device, ix) {
|
||||||
|
var isSelected = player.device === device
|
||||||
|
var name = device.name
|
||||||
|
return hx`
|
||||||
|
<li onclick=${dispatcher('selectCastDevice', ix)}>
|
||||||
|
<i.icon>${isSelected ? 'radio_button_checked' : 'radio_button_unchecked'}</i>
|
||||||
|
${name}
|
||||||
|
</li>
|
||||||
|
`
|
||||||
|
})
|
||||||
|
|
||||||
|
return hx`
|
||||||
|
<ul.options-list>
|
||||||
|
${items}
|
||||||
|
</ul>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
function renderSubtitlesOptions (state) {
|
function renderSubtitlesOptions (state) {
|
||||||
var subtitles = state.playing.subtitles
|
var subtitles = state.playing.subtitles
|
||||||
if (!subtitles.tracks.length || !subtitles.showMenu) return
|
if (!subtitles.tracks.length || !subtitles.showMenu) return
|
||||||
@@ -316,7 +352,7 @@ function renderSubtitlesOptions (state) {
|
|||||||
var noneSelected = state.playing.subtitles.selectedIndex === -1
|
var noneSelected = state.playing.subtitles.selectedIndex === -1
|
||||||
var noneClass = 'radio_button_' + (noneSelected ? 'checked' : 'unchecked')
|
var noneClass = 'radio_button_' + (noneSelected ? 'checked' : 'unchecked')
|
||||||
return hx`
|
return hx`
|
||||||
<ul.subtitles-list>
|
<ul.options-list>
|
||||||
${items}
|
${items}
|
||||||
<li onclick=${dispatcher('selectSubtitle', -1)}>
|
<li onclick=${dispatcher('selectSubtitle', -1)}>
|
||||||
<i.icon>${noneClass}</i>
|
<i.icon>${noneClass}</i>
|
||||||
@@ -378,72 +414,49 @@ function renderPlayerControls (state) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If we've detected a Chromecast or AppleTV, the user can play video there
|
// If we've detected a Chromecast or AppleTV, the user can play video there
|
||||||
var isOnChromecast = state.playing.location.startsWith('chromecast')
|
var castTypes = ['chromecast', 'airplay', 'dlna']
|
||||||
var isOnAirplay = state.playing.location.startsWith('airplay')
|
var isCastingAnywhere = castTypes.some(
|
||||||
var isOnDlna = state.playing.location.startsWith('dlna')
|
(castType) => state.playing.location.startsWith(castType))
|
||||||
var chromecastClass, chromecastHandler
|
|
||||||
var airplayClass, airplayHandler
|
|
||||||
var dlnaClass, dlnaHandler
|
|
||||||
if (isOnChromecast) {
|
|
||||||
chromecastClass = 'active'
|
|
||||||
dlnaClass = 'disabled'
|
|
||||||
airplayClass = 'disabled'
|
|
||||||
chromecastHandler = dispatcher('closeDevice')
|
|
||||||
airplayHandler = undefined
|
|
||||||
dlnaHandler = undefined
|
|
||||||
} else if (isOnAirplay) {
|
|
||||||
chromecastClass = 'disabled'
|
|
||||||
dlnaClass = 'disabled'
|
|
||||||
airplayClass = 'active'
|
|
||||||
chromecastHandler = undefined
|
|
||||||
airplayHandler = dispatcher('closeDevice')
|
|
||||||
dlnaHandler = undefined
|
|
||||||
} else if (isOnDlna) {
|
|
||||||
chromecastClass = 'disabled'
|
|
||||||
dlnaClass = 'active'
|
|
||||||
airplayClass = 'disabled'
|
|
||||||
chromecastHandler = undefined
|
|
||||||
airplayHandler = undefined
|
|
||||||
dlnaHandler = dispatcher('closeDevice')
|
|
||||||
} else {
|
|
||||||
chromecastClass = ''
|
|
||||||
airplayClass = ''
|
|
||||||
dlnaClass = ''
|
|
||||||
chromecastHandler = dispatcher('openDevice', 'chromecast')
|
|
||||||
airplayHandler = dispatcher('openDevice', 'airplay')
|
|
||||||
dlnaHandler = dispatcher('openDevice', 'dlna')
|
|
||||||
}
|
|
||||||
if (state.devices.chromecast || isOnChromecast) {
|
|
||||||
var castIcon = isOnChromecast ? 'cast_connected' : 'cast'
|
|
||||||
elements.push(hx`
|
|
||||||
<i.icon.device.float-right
|
|
||||||
class=${chromecastClass}
|
|
||||||
onclick=${chromecastHandler}>
|
|
||||||
${castIcon}
|
|
||||||
</i>
|
|
||||||
`)
|
|
||||||
}
|
|
||||||
if (state.devices.airplay || isOnAirplay) {
|
|
||||||
elements.push(hx`
|
|
||||||
<i.icon.device.float-right
|
|
||||||
class=${airplayClass}
|
|
||||||
onclick=${airplayHandler}>
|
|
||||||
airplay
|
|
||||||
</i>
|
|
||||||
`)
|
|
||||||
}
|
|
||||||
if (state.devices.dlna || isOnDlna) {
|
|
||||||
elements.push(hx`
|
|
||||||
<i
|
|
||||||
class='icon device float-right'
|
|
||||||
class=${dlnaClass}
|
|
||||||
onclick=${dlnaHandler}>
|
|
||||||
tv
|
|
||||||
</i>
|
|
||||||
`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// render volume
|
// Add the cast buttons. Icons for each cast type, connected/disconnected:
|
||||||
|
var buttonIcons = {
|
||||||
|
'chromecast': {true: 'cast_connected', false: 'cast'},
|
||||||
|
'airplay': {true: 'airplay', false: 'airplay'},
|
||||||
|
'dlna': {true: 'tv', false: 'tv'}
|
||||||
|
}
|
||||||
|
castTypes.forEach(function (castType) {
|
||||||
|
// Do we show this button (eg. the Chromecast button) at all?
|
||||||
|
var isCasting = state.playing.location.startsWith(castType)
|
||||||
|
var player = state.devices[castType]
|
||||||
|
if ((!player || player.getDevices().length === 0) && !isCasting) return
|
||||||
|
|
||||||
|
// Show the button. Three options for eg the Chromecast button:
|
||||||
|
var buttonClass, buttonHandler
|
||||||
|
if (isCasting) {
|
||||||
|
// Option 1: we are currently connected to Chromecast. Button stops the cast.
|
||||||
|
buttonClass = 'active'
|
||||||
|
buttonHandler = dispatcher('stopCasting')
|
||||||
|
} else if (isCastingAnywhere) {
|
||||||
|
// Option 2: we are currently connected somewhere else. Button disabled.
|
||||||
|
buttonClass = 'disabled'
|
||||||
|
buttonHandler = undefined
|
||||||
|
} else {
|
||||||
|
// Option 3: we are not connected anywhere. Button opens Chromecast menu.
|
||||||
|
buttonClass = ''
|
||||||
|
buttonHandler = dispatcher('toggleCastMenu', castType)
|
||||||
|
}
|
||||||
|
var buttonIcon = buttonIcons[castType][isCasting]
|
||||||
|
|
||||||
|
elements.push(hx`
|
||||||
|
<i.icon.device.float-right
|
||||||
|
class=${buttonClass}
|
||||||
|
onclick=${buttonHandler}>
|
||||||
|
${buttonIcon}
|
||||||
|
</i>
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Render volume slider
|
||||||
var volume = state.playing.volume
|
var volume = state.playing.volume
|
||||||
var volumeIcon = 'volume_' + (
|
var volumeIcon = 'volume_' + (
|
||||||
volume === 0 ? 'off'
|
volume === 0 ? 'off'
|
||||||
@@ -496,6 +509,7 @@ function renderPlayerControls (state) {
|
|||||||
return hx`
|
return hx`
|
||||||
<div class='controls'>
|
<div class='controls'>
|
||||||
${elements}
|
${elements}
|
||||||
|
${renderCastOptions(state)}
|
||||||
${renderSubtitlesOptions(state)}
|
${renderSubtitlesOptions(state)}
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
@@ -510,11 +524,12 @@ function renderPlayerControls (state) {
|
|||||||
|
|
||||||
// Handles a click or drag to scrub (jump to another position in the video)
|
// Handles a click or drag to scrub (jump to another position in the video)
|
||||||
function handleScrub (e) {
|
function handleScrub (e) {
|
||||||
|
if (!e.clientX) return
|
||||||
dispatch('mediaMouseMoved')
|
dispatch('mediaMouseMoved')
|
||||||
var windowWidth = document.querySelector('body').clientWidth
|
var windowWidth = document.querySelector('body').clientWidth
|
||||||
var fraction = e.clientX / windowWidth
|
var fraction = e.clientX / windowWidth
|
||||||
var position = fraction * state.playing.duration /* seconds */
|
var position = fraction * state.playing.duration /* seconds */
|
||||||
dispatch('playbackJump', position)
|
dispatch('skipTo', position)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handles volume muting and Unmuting
|
// Handles volume muting and Unmuting
|
||||||
|
|||||||
26
renderer/views/remove-torrent-modal.js
Normal file
26
renderer/views/remove-torrent-modal.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
module.exports = RemoveTorrentModal
|
||||||
|
|
||||||
|
var {dispatch, dispatcher} = require('../lib/dispatcher')
|
||||||
|
var hx = require('../lib/hx')
|
||||||
|
|
||||||
|
function RemoveTorrentModal (state) {
|
||||||
|
var message = state.modal.deleteData
|
||||||
|
? 'Are you sure you want to remove this torrent from the list and delete the data file?'
|
||||||
|
: 'Are you sure you want to remove this torrent from the list?'
|
||||||
|
var buttonText = state.modal.deleteData ? 'Remove Data' : 'Remove'
|
||||||
|
|
||||||
|
return hx`
|
||||||
|
<div>
|
||||||
|
<p><strong>${message}</strong></p>
|
||||||
|
<p class='float-right'>
|
||||||
|
<button class='button button-flat' onclick=${dispatcher('exitModal')}>Cancel</button>
|
||||||
|
<button class='button button-raised' onclick=${handleRemove}>${buttonText}</button>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
|
||||||
|
function handleRemove () {
|
||||||
|
dispatch('deleteTorrent', state.modal.infoHash, state.modal.deleteData)
|
||||||
|
dispatch('exitModal')
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -148,12 +148,12 @@ function TorrentList (state) {
|
|||||||
|
|
||||||
// Only show the play button for torrents that contain playable media
|
// Only show the play button for torrents that contain playable media
|
||||||
var playButton
|
var playButton
|
||||||
if (TorrentPlayer.isPlayableTorrent(torrentSummary)) {
|
if (TorrentPlayer.isPlayableTorrentSummary(torrentSummary)) {
|
||||||
playButton = hx`
|
playButton = hx`
|
||||||
<i.button-round.icon.play
|
<i.button-round.icon.play
|
||||||
title=${playTooltip}
|
title=${playTooltip}
|
||||||
class=${playClass}
|
class=${playClass}
|
||||||
onclick=${dispatcher('play', infoHash)}>
|
onclick=${dispatcher('playFile', infoHash)}>
|
||||||
${playIcon}
|
${playIcon}
|
||||||
</i>
|
</i>
|
||||||
`
|
`
|
||||||
@@ -172,7 +172,7 @@ function TorrentList (state) {
|
|||||||
<i
|
<i
|
||||||
class='icon delete'
|
class='icon delete'
|
||||||
title='Remove torrent'
|
title='Remove torrent'
|
||||||
onclick=${dispatcher('deleteTorrent', infoHash)}>
|
onclick=${dispatcher('confirmDeleteTorrent', infoHash, false)}>
|
||||||
close
|
close
|
||||||
</i>
|
</i>
|
||||||
</div>
|
</div>
|
||||||
@@ -218,7 +218,8 @@ function TorrentList (state) {
|
|||||||
// Show a single torrentSummary file in the details view for a single torrent
|
// Show a single torrentSummary file in the details view for a single torrent
|
||||||
function renderFileRow (torrentSummary, file, index) {
|
function renderFileRow (torrentSummary, file, index) {
|
||||||
// First, find out how much of the file we've downloaded
|
// First, find out how much of the file we've downloaded
|
||||||
var isSelected = torrentSummary.selections[index] // Are we even torrenting it?
|
// Are we even torrenting it?
|
||||||
|
var isSelected = torrentSummary.selections && torrentSummary.selections[index]
|
||||||
var isDone = false // Are we finished torrenting it?
|
var isDone = false // Are we finished torrenting it?
|
||||||
var progress = ''
|
var progress = ''
|
||||||
if (torrentSummary.progress && torrentSummary.progress.files) {
|
if (torrentSummary.progress && torrentSummary.progress.files) {
|
||||||
@@ -241,7 +242,7 @@ function TorrentList (state) {
|
|||||||
var handleClick
|
var handleClick
|
||||||
if (isPlayable) {
|
if (isPlayable) {
|
||||||
icon = 'play_arrow' /* playable? add option to play */
|
icon = 'play_arrow' /* playable? add option to play */
|
||||||
handleClick = dispatcher('play', infoHash, index)
|
handleClick = dispatcher('playFile', infoHash, index)
|
||||||
} else {
|
} else {
|
||||||
icon = 'description' /* file icon, opens in OS default app */
|
icon = 'description' /* file icon, opens in OS default app */
|
||||||
handleClick = dispatcher('openItem', infoHash, index)
|
handleClick = dispatcher('openItem', infoHash, index)
|
||||||
@@ -10,9 +10,9 @@ function UpdateAvailableModal (state) {
|
|||||||
<div class='update-available-modal'>
|
<div class='update-available-modal'>
|
||||||
<p><strong>A new version of WebTorrent is available: v${state.modal.version}</strong></p>
|
<p><strong>A new version of WebTorrent is available: v${state.modal.version}</strong></p>
|
||||||
<p>We have an auto-updater for Windows and Mac. We don't have one for Linux yet, so you'll have to download the new version manually.</p>
|
<p>We have an auto-updater for Windows and Mac. We don't have one for Linux yet, so you'll have to download the new version manually.</p>
|
||||||
<p>
|
<p class='float-right'>
|
||||||
<button class='primary' onclick=${handleOK}>Show Download Page</button>
|
<button class='button button-flat' onclick=${handleCancel}>Skip This Release</button>
|
||||||
<button class='cancel' onclick=${handleCancel}>Skip This Release</button>
|
<button class='button button-raised' onclick=${handleOK}>Show Download Page</button>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
|
|||||||
@@ -63,6 +63,10 @@ function init () {
|
|||||||
|
|
||||||
ipc.send('ipcReadyWebTorrent')
|
ipc.send('ipcReadyWebTorrent')
|
||||||
|
|
||||||
|
window.addEventListener('error', (e) =>
|
||||||
|
ipc.send('wt-uncaught-error', {message: e.error.message, stack: e.error.stack}),
|
||||||
|
true)
|
||||||
|
|
||||||
setInterval(updateTorrentProgress, 1000)
|
setInterval(updateTorrentProgress, 1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,7 +177,7 @@ function saveTorrentFile (torrentKey) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise, save the .torrent file, under the app config folder
|
// Otherwise, save the .torrent file, under the app config folder
|
||||||
fs.mkdir(config.CONFIG_TORRENT_PATH, function (_) {
|
fs.mkdir(config.TORRENT_PATH, function (_) {
|
||||||
fs.writeFile(torrentPath, torrent.torrentFile, function (err) {
|
fs.writeFile(torrentPath, torrent.torrentFile, function (err) {
|
||||||
if (err) return console.log('error saving torrent file %s: %o', torrentPath, err)
|
if (err) return console.log('error saving torrent file %s: %o', torrentPath, err)
|
||||||
console.log('saved torrent file %s', torrentPath)
|
console.log('saved torrent file %s', torrentPath)
|
||||||
@@ -186,7 +190,7 @@ function saveTorrentFile (torrentKey) {
|
|||||||
// Checks whether we've already resolved a given infohash to a torrent file
|
// Checks whether we've already resolved a given infohash to a torrent file
|
||||||
// Calls back with (torrentPath, exists). Logs, does not call back on error
|
// Calls back with (torrentPath, exists). Logs, does not call back on error
|
||||||
function checkIfTorrentFileExists (infoHash, cb) {
|
function checkIfTorrentFileExists (infoHash, cb) {
|
||||||
var torrentPath = path.join(config.CONFIG_TORRENT_PATH, infoHash + '.torrent')
|
var torrentPath = path.join(config.TORRENT_PATH, infoHash + '.torrent')
|
||||||
fs.exists(torrentPath, function (exists) {
|
fs.exists(torrentPath, function (exists) {
|
||||||
cb(torrentPath, exists)
|
cb(torrentPath, exists)
|
||||||
})
|
})
|
||||||
@@ -199,10 +203,10 @@ function generateTorrentPoster (torrentKey) {
|
|||||||
torrentPoster(torrent, function (err, buf, extension) {
|
torrentPoster(torrent, function (err, buf, extension) {
|
||||||
if (err) return console.log('error generating poster: %o', err)
|
if (err) return console.log('error generating poster: %o', err)
|
||||||
// save it for next time
|
// save it for next time
|
||||||
fs.mkdirp(config.CONFIG_POSTER_PATH, function (err) {
|
fs.mkdirp(config.POSTER_PATH, function (err) {
|
||||||
if (err) return console.log('error creating poster dir: %o', err)
|
if (err) return console.log('error creating poster dir: %o', err)
|
||||||
var posterFileName = torrent.infoHash + extension
|
var posterFileName = torrent.infoHash + extension
|
||||||
var posterFilePath = path.join(config.CONFIG_POSTER_PATH, posterFileName)
|
var posterFilePath = path.join(config.POSTER_PATH, posterFileName)
|
||||||
fs.writeFile(posterFilePath, buf, function (err) {
|
fs.writeFile(posterFilePath, buf, function (err) {
|
||||||
if (err) return console.log('error saving poster: %o', err)
|
if (err) return console.log('error saving poster: %o', err)
|
||||||
// show the poster
|
// show the poster
|
||||||
|
|||||||
BIN
static/PauseThumbnailBarButton.png
Normal file
BIN
static/PauseThumbnailBarButton.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 101 B |
BIN
static/PlayThumbnailBarButton.png
Normal file
BIN
static/PlayThumbnailBarButton.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 208 B |
@@ -13,7 +13,7 @@ Exec=$EXEC_PATH %U
|
|||||||
TryExec=$TRY_EXEC_PATH
|
TryExec=$TRY_EXEC_PATH
|
||||||
StartupNotify=false
|
StartupNotify=false
|
||||||
Categories=Network;FileTransfer;P2P;
|
Categories=Network;FileTransfer;P2P;
|
||||||
MimeType=application/x-bittorrent;x-scheme-handler/magnet;
|
MimeType=application/x-bittorrent;x-scheme-handler/magnet;x-scheme-handler/stream-magnet;
|
||||||
|
|
||||||
Actions=CreateNewTorrent;OpenTorrentFile;OpenTorrentAddress;
|
Actions=CreateNewTorrent;OpenTorrentFile;OpenTorrentAddress;
|
||||||
|
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 76 KiB |
Reference in New Issue
Block a user