Compare commits
44 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e34223fc94 | ||
|
|
15f733f11c | ||
|
|
7526b18507 | ||
|
|
0af6007632 | ||
|
|
1bc3cd1d51 | ||
|
|
92bafd695d | ||
|
|
78a2ee4e85 | ||
|
|
8b9346d767 | ||
|
|
06d3bd3f93 | ||
|
|
1af7e4ef19 | ||
|
|
8e64e4120b | ||
|
|
b983559763 | ||
|
|
e62527de23 | ||
|
|
1f51f35f8e | ||
|
|
c3686417e3 | ||
|
|
746e10c025 | ||
|
|
98389fc07c | ||
|
|
aaebf93db4 | ||
|
|
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 |
19
CHANGELOG.md
19
CHANGELOG.md
@@ -1,5 +1,24 @@
|
||||
# WebTorrent Desktop Version History
|
||||
|
||||
## 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
|
||||
|
||||
### Added
|
||||
|
||||
@@ -81,6 +81,10 @@ brew install wine
|
||||
|
||||
(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 what OSs are users are on, and how well the play button works (how often does it succeed? time out? show a missing codec error?). The app never sends personally identifying or other private info.
|
||||
|
||||
### Code Style
|
||||
|
||||
[](https://github.com/feross/standard)
|
||||
|
||||
@@ -3,7 +3,48 @@
|
||||
var fs = require('fs')
|
||||
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']
|
||||
|
||||
main()
|
||||
@@ -19,10 +60,10 @@ function main () {
|
||||
var packageDeps = findPackageDeps()
|
||||
|
||||
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(
|
||||
(dep) => !usedDeps.includes(dep) && !EXECUTABLE_DEPS.includes(dep)
|
||||
(dep) => !includes(usedDeps, dep) && !includes(EXECUTABLE_DEPS, dep)
|
||||
)
|
||||
|
||||
if (missingDeps.length > 0) {
|
||||
@@ -52,3 +93,7 @@ function findUsedDeps () {
|
||||
var stdout = cp.execSync('./bin/list-deps.sh')
|
||||
return stdout.toString().trim().split('\n')
|
||||
}
|
||||
|
||||
function includes (arr, elem) {
|
||||
return arr.indexOf(elem) >= 0
|
||||
}
|
||||
|
||||
@@ -2,7 +2,5 @@
|
||||
|
||||
var config = require('../config')
|
||||
var open = require('open')
|
||||
var path = require('path')
|
||||
|
||||
var configPath = path.join(config.CONFIG_PATH, 'config.json')
|
||||
open(configPath)
|
||||
open(config.CONFIG_PATH)
|
||||
|
||||
57
config.js
57
config.js
@@ -10,6 +10,9 @@ var PORTABLE_PATH = path.join(path.dirname(process.execPath), 'Portable Settings
|
||||
|
||||
module.exports = {
|
||||
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_FILE_ICON: path.join(__dirname, 'static', 'WebTorrentFile'),
|
||||
@@ -19,16 +22,40 @@ module.exports = {
|
||||
APP_VERSION: APP_VERSION,
|
||||
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_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 */,
|
||||
|
||||
DEFAULT_DOWNLOAD_PATH: getDefaultDownloadPath(),
|
||||
|
||||
GITHUB_URL: 'https://github.com/feross/webtorrent-desktop',
|
||||
GITHUB_URL_ISSUES: 'https://github.com/feross/webtorrent-desktop/issues',
|
||||
GITHUB_URL_RAW: 'https://raw.githubusercontent.com/feross/webtorrent-desktop/master',
|
||||
@@ -38,8 +65,10 @@ module.exports = {
|
||||
IS_PORTABLE: isPortable(),
|
||||
IS_PRODUCTION: isProduction(),
|
||||
|
||||
POSTER_PATH: path.join(getConfigPath(), 'Posters'),
|
||||
ROOT_PATH: __dirname,
|
||||
STATIC_PATH: path.join(__dirname, 'static'),
|
||||
TORRENT_PATH: path.join(getConfigPath(), 'Torrents'),
|
||||
|
||||
WINDOW_ABOUT: 'file://' + path.join(__dirname, 'renderer', 'about.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 () {
|
||||
try {
|
||||
return process.platform === 'win32' && isProduction() && !!fs.statSync(PORTABLE_PATH)
|
||||
|
||||
@@ -68,6 +68,13 @@ function init () {
|
||||
|
||||
// To keep app startup fast, some code is delayed.
|
||||
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 () {
|
||||
|
||||
@@ -15,6 +15,7 @@ var shell = require('./shell')
|
||||
var shortcuts = require('./shortcuts')
|
||||
var vlc = require('./vlc')
|
||||
var windows = require('./windows')
|
||||
var thumbnail = require('./thumbnail')
|
||||
|
||||
// Messages from the main process, to be sent once the WebTorrent process starts
|
||||
var messageQueueMainToWebTorrent = []
|
||||
@@ -68,6 +69,10 @@ function init () {
|
||||
shortcuts.onPlayerOpen()
|
||||
})
|
||||
|
||||
ipc.on('updateThumbnailBar', function (e, isPaused) {
|
||||
thumbnail.updateThumbarButtons(isPaused)
|
||||
})
|
||||
|
||||
/**
|
||||
* Power Save Blocker
|
||||
*/
|
||||
@@ -81,6 +86,7 @@ function init () {
|
||||
|
||||
ipc.on('openItem', (e, ...args) => shell.openItem(...args))
|
||||
ipc.on('showItemInFolder', (e, ...args) => shell.showItemInFolder(...args))
|
||||
ipc.on('moveItemToTrash', (e, ...args) => shell.moveItemToTrash(...args))
|
||||
|
||||
/**
|
||||
* Windows: Main
|
||||
|
||||
13
main/menu.js
13
main/menu.js
@@ -16,6 +16,7 @@ var config = require('../config')
|
||||
var dialog = require('./dialog')
|
||||
var shell = require('./shell')
|
||||
var windows = require('./windows')
|
||||
var thumbnail = require('./thumbnail')
|
||||
|
||||
var menu
|
||||
|
||||
@@ -33,6 +34,8 @@ function onPlayerClose () {
|
||||
getMenuItem('Increase Speed').enabled = false
|
||||
getMenuItem('Decrease Speed').enabled = false
|
||||
getMenuItem('Add Subtitles File...').enabled = false
|
||||
|
||||
thumbnail.showPlayerThumbnailBar()
|
||||
}
|
||||
|
||||
function onPlayerOpen () {
|
||||
@@ -44,6 +47,8 @@ function onPlayerOpen () {
|
||||
getMenuItem('Increase Speed').enabled = true
|
||||
getMenuItem('Decrease Speed').enabled = true
|
||||
getMenuItem('Add Subtitles File...').enabled = true
|
||||
|
||||
thumbnail.hidePlayerThumbnailBar()
|
||||
}
|
||||
|
||||
function onToggleAlwaysOnTop (flag) {
|
||||
@@ -217,13 +222,17 @@ function getMenuTemplate () {
|
||||
},
|
||||
{
|
||||
label: 'Step Forward',
|
||||
accelerator: 'CmdOrCtrl+Alt+Right',
|
||||
accelerator: process.platform === 'darwin'
|
||||
? 'CmdOrCtrl+Alt+Right'
|
||||
: 'Alt+Right',
|
||||
click: () => windows.main.dispatch('skip', 1),
|
||||
enabled: false
|
||||
},
|
||||
{
|
||||
label: 'Step Backward',
|
||||
accelerator: 'CmdOrCtrl+Alt+Left',
|
||||
accelerator: process.platform === 'darwin'
|
||||
? 'CmdOrCtrl+Alt+Left'
|
||||
: 'Alt+Left',
|
||||
click: () => windows.main.dispatch('skip', -1),
|
||||
enabled: false
|
||||
},
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
module.exports = {
|
||||
openExternal,
|
||||
openItem,
|
||||
showItemInFolder
|
||||
showItemInFolder,
|
||||
moveItemToTrash
|
||||
}
|
||||
|
||||
var electron = require('electron')
|
||||
@@ -30,3 +31,11 @@ function showItemInFolder (path) {
|
||||
log(`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)
|
||||
}
|
||||
|
||||
35
main/thumbnail.js
Normal file
35
main/thumbnail.js
Normal file
@@ -0,0 +1,35 @@
|
||||
module.exports = {
|
||||
showPlayerThumbnailBar,
|
||||
hidePlayerThumbnailBar,
|
||||
updateThumbarButtons
|
||||
}
|
||||
|
||||
var path = require('path')
|
||||
var config = require('../config')
|
||||
|
||||
var windows = require('./windows')
|
||||
|
||||
// gets called on player open
|
||||
function showPlayerThumbnailBar () {
|
||||
updateThumbarButtons(false)
|
||||
}
|
||||
|
||||
// gets called on player close
|
||||
function hidePlayerThumbnailBar () {
|
||||
windows.main.win.setThumbarButtons([])
|
||||
}
|
||||
|
||||
function updateThumbarButtons (isPaused) {
|
||||
var icon = isPaused ? 'PlayThumbnailBarButton.png' : 'PauseThumbnailBarButton.png'
|
||||
var tooltip = isPaused ? 'Play' : 'Pause'
|
||||
var buttons = [
|
||||
{
|
||||
tooltip: tooltip,
|
||||
icon: path.join(config.STATIC_PATH, icon),
|
||||
click: function () {
|
||||
windows.main.send('dispatch', 'playPause')
|
||||
}
|
||||
}
|
||||
]
|
||||
windows.main.win.setThumbarButtons(buttons)
|
||||
}
|
||||
@@ -22,6 +22,7 @@ function init () {
|
||||
resizable: false,
|
||||
show: false,
|
||||
skipTaskbar: true,
|
||||
title: 'About ' + config.APP_WINDOW_TITLE,
|
||||
useContentSize: true,
|
||||
width: 300
|
||||
})
|
||||
@@ -31,7 +32,7 @@ function init () {
|
||||
// No menu on the About window
|
||||
win.setMenu(null)
|
||||
|
||||
win.webContents.on('did-finish-load', function () {
|
||||
win.webContents.once('did-finish-load', function () {
|
||||
win.show()
|
||||
})
|
||||
|
||||
|
||||
@@ -51,15 +51,11 @@ function init () {
|
||||
menu.onToggleFullScreen(main.win.isFullScreen())
|
||||
})
|
||||
|
||||
win.on('blur', function () {
|
||||
menu.onWindowBlur()
|
||||
tray.onWindowBlur()
|
||||
})
|
||||
win.on('blur', onWindowBlur)
|
||||
win.on('focus', onWindowFocus)
|
||||
|
||||
win.on('focus', function () {
|
||||
menu.onWindowFocus()
|
||||
tray.onWindowFocus()
|
||||
})
|
||||
win.on('hide', onWindowBlur)
|
||||
win.on('show', onWindowFocus)
|
||||
|
||||
win.on('enter-full-screen', function () {
|
||||
menu.onToggleFullScreen(true)
|
||||
@@ -78,7 +74,7 @@ function init () {
|
||||
app.quit()
|
||||
} else if (!app.isQuitting) {
|
||||
e.preventDefault()
|
||||
win.hide()
|
||||
hide()
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -207,6 +203,16 @@ function toggleFullScreen (flag) {
|
||||
main.win.setFullScreen(flag)
|
||||
}
|
||||
|
||||
function onWindowBlur () {
|
||||
menu.onWindowBlur()
|
||||
tray.onWindowBlur()
|
||||
}
|
||||
|
||||
function onWindowFocus () {
|
||||
menu.onWindowFocus()
|
||||
tray.onWindowFocus()
|
||||
}
|
||||
|
||||
function getIconPath () {
|
||||
return process.platform === 'win32'
|
||||
? config.APP_ICON + '.ico'
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"name": "webtorrent-desktop",
|
||||
"description": "WebTorrent, the streaming torrent client. For OS X, Windows, and Linux.",
|
||||
"version": "0.7.0",
|
||||
"version": "0.8.0",
|
||||
"author": {
|
||||
"name": "WebTorrent, LLC",
|
||||
"email": "feross@feross.org",
|
||||
"email": "feross@webtorrent.io",
|
||||
"url": "https://webtorrent.io"
|
||||
},
|
||||
"bin": {
|
||||
@@ -30,6 +30,7 @@
|
||||
"main-loop": "^3.2.0",
|
||||
"musicmetadata": "^2.0.2",
|
||||
"network-address": "^1.1.0",
|
||||
"parse-torrent": "^5.7.3",
|
||||
"prettier-bytes": "^1.0.1",
|
||||
"run-parallel": "^1.1.6",
|
||||
"semver": "^5.1.0",
|
||||
|
||||
@@ -3,8 +3,9 @@
|
||||
// * Starts and stops casting, provides remote video controls
|
||||
module.exports = {
|
||||
init,
|
||||
open,
|
||||
close,
|
||||
toggleMenu,
|
||||
selectDevice,
|
||||
stop,
|
||||
play,
|
||||
pause,
|
||||
seek,
|
||||
@@ -32,24 +33,49 @@ function init (appState, callback) {
|
||||
state = appState
|
||||
update = callback
|
||||
|
||||
state.devices.chromecast = chromecastPlayer()
|
||||
state.devices.dlna = dlnaPlayer()
|
||||
state.devices.airplay = airplayPlayer()
|
||||
|
||||
// Listen for devices: Chromecast, DLNA and Airplay
|
||||
chromecasts.on('update', function (player) {
|
||||
state.devices.chromecast = chromecastPlayer(player)
|
||||
chromecasts.on('update', function (device) {
|
||||
// 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) {
|
||||
state.devices.dlna = dlnaPlayer(player)
|
||||
dlnacasts.on('update', function (device) {
|
||||
state.devices.dlna.addDevice(device)
|
||||
})
|
||||
|
||||
airplayer.on('update', function (player) {
|
||||
state.devices.airplay = airplayPlayer(player)
|
||||
airplayer.on('update', function (device) {
|
||||
state.devices.airplay.addDevice(device)
|
||||
})
|
||||
}
|
||||
|
||||
// chromecast player implementation
|
||||
function chromecastPlayer (player) {
|
||||
function addEvents () {
|
||||
player.on('error', function (err) {
|
||||
function chromecastPlayer () {
|
||||
var ret = {
|
||||
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.errors.push({
|
||||
time: new Date().getTime(),
|
||||
@@ -57,7 +83,8 @@ function chromecastPlayer (player) {
|
||||
})
|
||||
update()
|
||||
})
|
||||
player.on('disconnect', function () {
|
||||
device.on('disconnect', function () {
|
||||
if (device !== ret.device) return
|
||||
state.playing.location = 'local'
|
||||
update()
|
||||
})
|
||||
@@ -65,7 +92,7 @@ function chromecastPlayer (player) {
|
||||
|
||||
function open () {
|
||||
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',
|
||||
title: config.APP_NAME + ' - ' + torrentSummary.name
|
||||
}, function (err) {
|
||||
@@ -83,19 +110,19 @@ function chromecastPlayer (player) {
|
||||
}
|
||||
|
||||
function play (callback) {
|
||||
player.play(null, null, callback)
|
||||
ret.device.play(null, null, callback)
|
||||
}
|
||||
|
||||
function pause (callback) {
|
||||
player.pause(callback)
|
||||
ret.device.pause(callback)
|
||||
}
|
||||
|
||||
function stop (callback) {
|
||||
player.stop(callback)
|
||||
ret.device.stop(callback)
|
||||
}
|
||||
|
||||
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)
|
||||
state.playing.isPaused = status.playerState === 'PAUSED'
|
||||
state.playing.currentTime = status.currentTime
|
||||
@@ -105,30 +132,31 @@ function chromecastPlayer (player) {
|
||||
}
|
||||
|
||||
function seek (time, callback) {
|
||||
player.seek(time, callback)
|
||||
ret.device.seek(time, callback)
|
||||
}
|
||||
|
||||
function volume (volume, callback) {
|
||||
player.volume(volume, callback)
|
||||
}
|
||||
|
||||
addEvents()
|
||||
|
||||
return {
|
||||
player: player,
|
||||
open: open,
|
||||
play: play,
|
||||
pause: pause,
|
||||
stop: stop,
|
||||
status: status,
|
||||
seek: seek,
|
||||
volume: volume
|
||||
ret.device.volume(volume, callback)
|
||||
}
|
||||
}
|
||||
|
||||
// airplay player implementation
|
||||
function airplayPlayer (player) {
|
||||
function addEvents () {
|
||||
function airplayPlayer () {
|
||||
var ret = {
|
||||
device: null,
|
||||
addDevice,
|
||||
getDevices,
|
||||
open,
|
||||
play,
|
||||
pause,
|
||||
stop,
|
||||
status,
|
||||
seek,
|
||||
volume
|
||||
}
|
||||
return ret
|
||||
|
||||
function addDevice (player) {
|
||||
player.on('event', function (event) {
|
||||
switch (event.state) {
|
||||
case 'loading':
|
||||
@@ -146,8 +174,12 @@ function airplayPlayer (player) {
|
||||
})
|
||||
}
|
||||
|
||||
function getDevices () {
|
||||
return airplayer.players
|
||||
}
|
||||
|
||||
function open () {
|
||||
player.play(state.server.networkURL, function (err, res) {
|
||||
ret.device.play(state.server.networkURL, function (err, res) {
|
||||
if (err) {
|
||||
state.playing.location = 'local'
|
||||
state.errors.push({
|
||||
@@ -162,19 +194,19 @@ function airplayPlayer (player) {
|
||||
}
|
||||
|
||||
function play (callback) {
|
||||
player.resume(callback)
|
||||
ret.device.resume(callback)
|
||||
}
|
||||
|
||||
function pause (callback) {
|
||||
player.pause(callback)
|
||||
ret.device.pause(callback)
|
||||
}
|
||||
|
||||
function stop (callback) {
|
||||
player.stop(callback)
|
||||
ret.device.stop(callback)
|
||||
}
|
||||
|
||||
function status () {
|
||||
player.playbackInfo(function (err, res, status) {
|
||||
ret.device.playbackInfo(function (err, res, status) {
|
||||
if (err) {
|
||||
state.playing.location = 'local'
|
||||
state.errors.push({
|
||||
@@ -190,7 +222,7 @@ function airplayPlayer (player) {
|
||||
}
|
||||
|
||||
function seek (time, callback) {
|
||||
player.scrub(time, callback)
|
||||
ret.device.scrub(time, callback)
|
||||
}
|
||||
|
||||
function volume (volume, callback) {
|
||||
@@ -198,25 +230,31 @@ function airplayPlayer (player) {
|
||||
// TODO: We should just disable the volume slider
|
||||
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
|
||||
function dlnaPlayer (player) {
|
||||
function addEvents () {
|
||||
player.on('error', function (err) {
|
||||
var ret = {
|
||||
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.errors.push({
|
||||
time: new Date().getTime(),
|
||||
@@ -224,7 +262,8 @@ function dlnaPlayer (player) {
|
||||
})
|
||||
update()
|
||||
})
|
||||
player.on('disconnect', function () {
|
||||
device.on('disconnect', function () {
|
||||
if (device !== ret.device) return
|
||||
state.playing.location = 'local'
|
||||
update()
|
||||
})
|
||||
@@ -232,7 +271,7 @@ function dlnaPlayer (player) {
|
||||
|
||||
function open () {
|
||||
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',
|
||||
title: config.APP_NAME + ' - ' + torrentSummary.name,
|
||||
seek: state.playing.currentTime > 10 ? state.playing.currentTime : 0
|
||||
@@ -251,19 +290,19 @@ function dlnaPlayer (player) {
|
||||
}
|
||||
|
||||
function play (callback) {
|
||||
player.play(null, null, callback)
|
||||
ret.device.play(null, null, callback)
|
||||
}
|
||||
|
||||
function pause (callback) {
|
||||
player.pause(callback)
|
||||
ret.device.pause(callback)
|
||||
}
|
||||
|
||||
function stop (callback) {
|
||||
player.stop(callback)
|
||||
ret.device.stop(callback)
|
||||
}
|
||||
|
||||
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)
|
||||
state.playing.isPaused = status.playerState === 'PAUSED'
|
||||
state.playing.currentTime = status.currentTime
|
||||
@@ -273,61 +312,78 @@ function dlnaPlayer (player) {
|
||||
}
|
||||
|
||||
function seek (time, callback) {
|
||||
player.seek(time, callback)
|
||||
ret.device.seek(time, callback)
|
||||
}
|
||||
|
||||
function volume (volume, callback) {
|
||||
player.volume(volume, function (err) {
|
||||
ret.device.volume(volume, function (err) {
|
||||
// quick volume update
|
||||
state.playing.volume = volume
|
||||
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
|
||||
function startStatusInterval () {
|
||||
statusInterval = setInterval(function () {
|
||||
var device = getDevice()
|
||||
if (device) {
|
||||
device.status()
|
||||
}
|
||||
var player = getPlayer()
|
||||
if (player) player.status()
|
||||
}, 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') {
|
||||
throw new Error('You can\'t connect to ' + location + ' when already connected to another device')
|
||||
}
|
||||
|
||||
state.playing.location = location + '-pending'
|
||||
var device = getDevice(location)
|
||||
if (device) {
|
||||
getDevice(location).open()
|
||||
startStatusInterval()
|
||||
}
|
||||
// Find all cast devices of the given type
|
||||
var player = getPlayer(location)
|
||||
var devices = player ? player.getDevices() : []
|
||||
if (devices.length === 0) throw new Error('No ' + location + ' devices available')
|
||||
|
||||
// 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()
|
||||
}
|
||||
|
||||
// Stops casting, move video back to local screen
|
||||
function close () {
|
||||
var device = getDevice()
|
||||
if (device) {
|
||||
device.stop(stoppedCasting)
|
||||
function stop () {
|
||||
var player = getPlayer()
|
||||
if (player) {
|
||||
player.stop(function () {
|
||||
player.device = null
|
||||
stoppedCasting()
|
||||
})
|
||||
clearInterval(statusInterval)
|
||||
} else {
|
||||
stoppedCasting()
|
||||
@@ -340,8 +396,8 @@ function stoppedCasting () {
|
||||
update()
|
||||
}
|
||||
|
||||
function getDevice (location) {
|
||||
if (location && state.devices[location]) {
|
||||
function getPlayer (location) {
|
||||
if (location) {
|
||||
return state.devices[location]
|
||||
} else if (state.playing.location === 'chromecast') {
|
||||
return state.devices.chromecast
|
||||
@@ -355,29 +411,25 @@ function getDevice (location) {
|
||||
}
|
||||
|
||||
function play () {
|
||||
var device = getDevice()
|
||||
if (device) {
|
||||
device.play(castCallback)
|
||||
}
|
||||
var player = getPlayer()
|
||||
if (player) player.play(castCallback)
|
||||
}
|
||||
|
||||
function pause () {
|
||||
var device = getDevice()
|
||||
if (device) {
|
||||
device.pause(castCallback)
|
||||
}
|
||||
var player = getPlayer()
|
||||
if (player) player.pause(castCallback)
|
||||
}
|
||||
|
||||
function setRate (rate) {
|
||||
var device
|
||||
var player
|
||||
var result = true
|
||||
if (state.playing.location === 'chromecast') {
|
||||
// TODO find how to control playback rate on chromecast
|
||||
castCallback()
|
||||
result = false
|
||||
} else if (state.playing.location === 'airplay') {
|
||||
device = state.devices.airplay
|
||||
device.rate(rate, castCallback)
|
||||
player = state.devices.airplay
|
||||
player.rate(rate, castCallback)
|
||||
} else {
|
||||
result = false
|
||||
}
|
||||
@@ -385,17 +437,13 @@ function setRate (rate) {
|
||||
}
|
||||
|
||||
function seek (time) {
|
||||
var device = getDevice()
|
||||
if (device) {
|
||||
device.seek(time, castCallback)
|
||||
}
|
||||
var player = getPlayer()
|
||||
if (player) player.seek(time, castCallback)
|
||||
}
|
||||
|
||||
function setVolume (volume) {
|
||||
var device = getDevice()
|
||||
if (device) {
|
||||
device.volume(volume, castCallback)
|
||||
}
|
||||
var player = getPlayer()
|
||||
if (player) player.volume(volume, castCallback)
|
||||
}
|
||||
|
||||
function castCallback () {
|
||||
|
||||
@@ -18,44 +18,41 @@ function run (state) {
|
||||
var version = state.saved.version
|
||||
|
||||
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.8.0')) {
|
||||
// migrate_0_8_0(state)
|
||||
// }
|
||||
if (semver.lt(version, '0.7.2')) {
|
||||
migrate_0_7_2(state.saved)
|
||||
}
|
||||
|
||||
// Config is now on the new 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')
|
||||
|
||||
var fs = require('fs-extra')
|
||||
var path = require('path')
|
||||
|
||||
state.saved.torrents.forEach(function (ts) {
|
||||
saved.torrents.forEach(function (ts) {
|
||||
var infoHash = ts.infoHash
|
||||
|
||||
// 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
|
||||
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)
|
||||
if (path.isAbsolute(ts.torrentPath)) {
|
||||
src = ts.torrentPath
|
||||
} else if (ts.torrentPath.startsWith('..')) {
|
||||
if (path.isAbsolute(ts.torrentPath) || ts.torrentPath.startsWith('..')) {
|
||||
src = ts.torrentPath
|
||||
} else {
|
||||
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
|
||||
// that only runs once
|
||||
if (src !== dst) fs.copySync(src, dst)
|
||||
@@ -71,7 +68,7 @@ function migrate_0_7_0 (state) {
|
||||
src = path.isAbsolute(ts.posterURL)
|
||||
? 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
|
||||
// that only runs once
|
||||
if (src !== dst) fs.copySync(src, dst)
|
||||
@@ -88,3 +85,11 @@ function migrate_0_7_0 (state) {
|
||||
delete ts.fileModtimes
|
||||
})
|
||||
}
|
||||
|
||||
function migrate_0_7_2 (saved) {
|
||||
if (!saved.prefs) {
|
||||
saved.prefs = {
|
||||
downloadPath: config.DEFAULT_DOWNLOAD_PATH
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
var electron = require('electron')
|
||||
var path = require('path')
|
||||
|
||||
var remote = electron.remote
|
||||
|
||||
var config = require('../../config')
|
||||
var LocationHistory = require('./location-history')
|
||||
|
||||
module.exports = {
|
||||
getInitialState,
|
||||
getDefaultPlayState,
|
||||
getDefaultSavedState
|
||||
load,
|
||||
save
|
||||
}
|
||||
|
||||
function getInitialState () {
|
||||
var appConfig = require('application-config')('WebTorrent')
|
||||
var path = require('path')
|
||||
|
||||
var config = require('../../config')
|
||||
var migrations = require('./migrations')
|
||||
|
||||
appConfig.filePath = path.join(config.CONFIG_PATH, 'config.json')
|
||||
|
||||
function getDefaultState () {
|
||||
var LocationHistory = require('./location-history')
|
||||
|
||||
return {
|
||||
/*
|
||||
* Temporary state disappears once the program exits.
|
||||
@@ -30,10 +32,7 @@ function getInitialState () {
|
||||
},
|
||||
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 */
|
||||
devices: { /* playback devices like Chromecast and AppleTV */
|
||||
airplay: null, /* airplay client. finds and manages AppleTVs */
|
||||
chromecast: null /* chromecast client. finds and manages Chromecasts */
|
||||
},
|
||||
devices: {}, /* playback devices like Chromecast and AppleTV */
|
||||
dock: {
|
||||
badge: 0,
|
||||
progress: 0
|
||||
@@ -91,185 +90,58 @@ function getDefaultPlayState () {
|
||||
}
|
||||
|
||||
/* If the saved state file doesn't exist yet, here's what we use instead */
|
||||
function getDefaultSavedState () {
|
||||
return {
|
||||
version: config.APP_VERSION, /* make sure we can upgrade gracefully later */
|
||||
torrents: [
|
||||
{
|
||||
status: 'paused',
|
||||
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'
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
function setupSavedState (cb) {
|
||||
var fs = require('fs-extra')
|
||||
var parseTorrent = require('parse-torrent')
|
||||
var parallel = require('run-parallel')
|
||||
|
||||
var saved = {
|
||||
prefs: {
|
||||
downloadPath: config.IS_PORTABLE
|
||||
? path.join(config.CONFIG_PATH, 'Downloads')
|
||||
: remote.app.getPath('downloads')
|
||||
downloadPath: config.DEFAULT_DOWNLOAD_PATH
|
||||
},
|
||||
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 +156,57 @@ function getPlayingFileSummary () {
|
||||
if (!torrentSummary) return null
|
||||
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)
|
||||
|
||||
var electron = require('electron')
|
||||
|
||||
// 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, function (err) {
|
||||
if (err) console.error(err)
|
||||
|
||||
// TODO: this doesn't belong here
|
||||
electron.ipcRenderer.send('savedState')
|
||||
})
|
||||
}
|
||||
|
||||
140
renderer/lib/telemetry.js
Normal file
140
renderer/lib/telemetry.js
Normal file
@@ -0,0 +1,140 @@
|
||||
// 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)
|
||||
|
||||
postToServer(telemetry)
|
||||
}
|
||||
|
||||
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) {
|
||||
var message, stack
|
||||
if (typeof err === 'string') {
|
||||
message = err
|
||||
stack = ''
|
||||
} else {
|
||||
message = err.message
|
||||
stack = 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
|
||||
}
|
||||
@@ -24,7 +24,8 @@ function isVideo (file) {
|
||||
'.mp4',
|
||||
'.mpg',
|
||||
'.ogv',
|
||||
'.webm'
|
||||
'.webm',
|
||||
'.wmv'
|
||||
].includes(ext)
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ function torrentPoster (torrent, cb) {
|
||||
if (videoFile) return torrentPosterFromVideo(videoFile, torrent, cb)
|
||||
|
||||
// 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)
|
||||
|
||||
// TODO: generate a waveform from the largest sound file
|
||||
|
||||
@@ -10,14 +10,14 @@ var config = require('../../config')
|
||||
// Returns an absolute path to the torrent file, or null if unavailable
|
||||
function getTorrentPath (torrentSummary) {
|
||||
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
|
||||
// Returns an absolute path to the poster image, or null if unavailable
|
||||
function getPosterPath (torrentSummary) {
|
||||
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):
|
||||
// Backslashes in URLS in CSS cause bizarre string encoding issues
|
||||
return posterPath.replace(/\\/g, '/')
|
||||
|
||||
@@ -260,7 +260,7 @@ table {
|
||||
|
||||
.modal .modal-content {
|
||||
position: fixed;
|
||||
top: 45px;
|
||||
top: 38px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin: 0 auto;
|
||||
@@ -848,7 +848,7 @@ body.drag .app::after {
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
.player .controls .subtitles-list {
|
||||
.player .controls .options-list {
|
||||
position: fixed;
|
||||
background: rgba(40, 40, 40, 0.8);
|
||||
min-width: 100px;
|
||||
@@ -862,7 +862,7 @@ body.drag .app::after {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.player .controls .subtitles-list .icon {
|
||||
.player .controls .options-list .icon {
|
||||
display: inline;
|
||||
font-size: 17px;
|
||||
vertical-align: bottom;
|
||||
|
||||
182
renderer/main.js
182
renderer/main.js
@@ -3,7 +3,6 @@ console.time('init')
|
||||
var crashReporter = require('../crash-reporter')
|
||||
crashReporter.init()
|
||||
|
||||
var appConfig = require('application-config')('WebTorrent')
|
||||
var dragDrop = require('drag-drop')
|
||||
var electron = require('electron')
|
||||
var fs = require('fs-extra')
|
||||
@@ -15,49 +14,34 @@ var createElement = require('virtual-dom/create-element')
|
||||
var diff = require('virtual-dom/diff')
|
||||
var patch = require('virtual-dom/patch')
|
||||
|
||||
var App = require('./views/app')
|
||||
var config = require('../config')
|
||||
var App = require('./views/app')
|
||||
var telemetry = require('./lib/telemetry')
|
||||
var errors = require('./lib/errors')
|
||||
var migrations = require('./lib/migrations')
|
||||
var sound = require('./lib/sound')
|
||||
var State = require('./lib/state')
|
||||
var TorrentPlayer = require('./lib/torrent-player')
|
||||
var TorrentSummary = require('./lib/torrent-summary')
|
||||
|
||||
var {setDispatch} = require('./lib/dispatcher')
|
||||
setDispatch(dispatch)
|
||||
|
||||
appConfig.filePath = path.join(config.CONFIG_PATH, 'config.json')
|
||||
|
||||
// This dependency is the slowest-loading, so we lazy load it
|
||||
var Cast = null
|
||||
|
||||
var vdomLoop
|
||||
|
||||
var state = State.getInitialState()
|
||||
state.location.go({ url: 'home' }) // Add first page to location history
|
||||
|
||||
// Electron apps have two processes: a main process (node) runs first and starts
|
||||
// a renderer process (essentially a Chrome window). We're in the renderer process,
|
||||
// and this IPC channel receives from and sends messages to the main process
|
||||
var ipcRenderer = electron.ipcRenderer
|
||||
|
||||
// All state lives in state.js. `state.saved` is read from and written to a file.
|
||||
// All other state is ephemeral. First we load state.saved then initialize the app.
|
||||
loadState(init)
|
||||
var state, vdomLoop
|
||||
|
||||
function loadState (cb) {
|
||||
appConfig.read(function (err, data) {
|
||||
if (err) console.error(err)
|
||||
// This dependency is the slowest-loading, so we lazy load it
|
||||
var Cast = null
|
||||
|
||||
// populate defaults if they're not there
|
||||
state.saved = Object.assign({}, State.getDefaultSavedState(), data)
|
||||
state.saved.torrents.forEach(function (torrentSummary) {
|
||||
if (torrentSummary.displayName) torrentSummary.name = torrentSummary.displayName
|
||||
})
|
||||
init()
|
||||
|
||||
if (cb) cb()
|
||||
})
|
||||
function init () {
|
||||
// All state lives in state.js. `state.saved` is read from and written to a file.
|
||||
// All other state is ephemeral. First we load state.saved then initialize the app.
|
||||
State.load(onState)
|
||||
|
||||
setDispatch(dispatch)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -65,9 +49,12 @@ function loadState (cb) {
|
||||
* Connects to the torrent networks, sets up the UI and OS integrations like
|
||||
* the dock icon and drag+drop.
|
||||
*/
|
||||
function init () {
|
||||
// Clean up the freshly-loaded config file, which may be from an older version
|
||||
migrations.run(state)
|
||||
function onState (err, _state) {
|
||||
if (err) return onError(err)
|
||||
state = _state
|
||||
|
||||
// Add first page to location history
|
||||
state.location.go({ url: 'home' })
|
||||
|
||||
// Restart everything we were torrenting last time the app ran
|
||||
resumeTorrents()
|
||||
@@ -105,15 +92,22 @@ function init () {
|
||||
window.addEventListener('focus', onFocus)
|
||||
window.addEventListener('blur', onBlur)
|
||||
|
||||
sound.play('STARTUP')
|
||||
// ...window visibility state.
|
||||
document.addEventListener('webkitvisibilitychange', onVisibilityChange)
|
||||
|
||||
// Log uncaught JS errors
|
||||
window.addEventListener('error',
|
||||
(e) => telemetry.logUncaughtError('window', e.error), true)
|
||||
|
||||
// Done! Ideally we want to get here < 500ms after the user clicks the app
|
||||
sound.play('STARTUP')
|
||||
console.timeEnd('init')
|
||||
}
|
||||
|
||||
function delayedInit () {
|
||||
lazyLoadCast()
|
||||
sound.preload()
|
||||
telemetry.init(state)
|
||||
}
|
||||
|
||||
// Lazily loads Chromecast and Airplay support
|
||||
@@ -181,7 +175,6 @@ function dispatch (action, ...args) {
|
||||
}
|
||||
if (action === 'openTorrentAddress') {
|
||||
state.modal = { id: 'open-torrent-address-modal' }
|
||||
update()
|
||||
}
|
||||
if (action === 'createTorrent') {
|
||||
createTorrent(args[0] /* options */)
|
||||
@@ -204,11 +197,14 @@ function dispatch (action, ...args) {
|
||||
if (action === 'openTorrentContextMenu') {
|
||||
openTorrentContextMenu(args[0] /* infoHash */)
|
||||
}
|
||||
if (action === 'openDevice') {
|
||||
lazyLoadCast().open(args[0] /* deviceType */)
|
||||
if (action === 'toggleCastMenu') {
|
||||
lazyLoadCast().toggleMenu(args[0] /* deviceType */)
|
||||
}
|
||||
if (action === 'closeDevice') {
|
||||
lazyLoadCast().close()
|
||||
if (action === 'selectCastDevice') {
|
||||
lazyLoadCast().selectDevice(args[0] /* index */)
|
||||
}
|
||||
if (action === 'stopCasting') {
|
||||
lazyLoadCast().stop()
|
||||
}
|
||||
if (action === 'setDimensions') {
|
||||
setDimensions(args[0] /* dimensions */)
|
||||
@@ -266,6 +262,7 @@ function dispatch (action, ...args) {
|
||||
}
|
||||
if (action === 'mediaError') {
|
||||
if (state.location.url() === 'player') {
|
||||
state.playing.result = 'error'
|
||||
state.playing.location = 'error'
|
||||
ipcRenderer.send('checkForVLC')
|
||||
ipcRenderer.once('checkForVLC', function (e, isInstalled) {
|
||||
@@ -277,6 +274,9 @@ function dispatch (action, ...args) {
|
||||
})
|
||||
}
|
||||
}
|
||||
if (action === 'mediaSuccess') {
|
||||
state.playing.result = 'success'
|
||||
}
|
||||
if (action === 'mediaTimeUpdate') {
|
||||
state.playing.lastTimeUpdate = new Date().getTime()
|
||||
state.playing.isStalled = false
|
||||
@@ -328,11 +328,14 @@ function dispatch (action, ...args) {
|
||||
saveStateThrottled()
|
||||
}
|
||||
if (action === 'saveState') {
|
||||
saveState()
|
||||
State.save(state)
|
||||
}
|
||||
if (action === 'setTitle') {
|
||||
state.window.title = args[0] /* title */
|
||||
}
|
||||
if (action === 'uncaughtError') {
|
||||
telemetry.logUncaughtError(args[0] /* process */, args[1] /* error */)
|
||||
}
|
||||
|
||||
// Update the virtual-dom, unless it's just a mouse move event
|
||||
if (action !== 'mediaMouseMoved' || showOrHidePlayerControls()) {
|
||||
@@ -369,11 +372,18 @@ function pause () {
|
||||
|
||||
function playPause () {
|
||||
if (state.location.url() !== 'player') return
|
||||
|
||||
if (state.playing.isPaused) {
|
||||
play()
|
||||
} else {
|
||||
pause()
|
||||
}
|
||||
|
||||
// force rerendering if window is hidden,
|
||||
// in order to bypass `raf` and play/pause media immediately
|
||||
if (!state.window.isVisible) render(state)
|
||||
|
||||
ipcRenderer.send('updateThumbnailBar', state.playing.isPaused)
|
||||
}
|
||||
|
||||
function jumpToTime (time) {
|
||||
@@ -484,6 +494,8 @@ function setupIpc () {
|
||||
ipcRenderer.on('wt-audio-metadata', (e, ...args) => torrentAudioMetadata(...args))
|
||||
ipcRenderer.on('wt-server-running', (e, ...args) => torrentServerRunning(...args))
|
||||
|
||||
ipcRenderer.on('wt-uncaught-error', (e, err) => telemetry.logUncaughtError('webtorrent', err))
|
||||
|
||||
ipcRenderer.send('ipcReady')
|
||||
}
|
||||
|
||||
@@ -512,7 +524,8 @@ function updatePreferences (property, value) {
|
||||
// All unsaved prefs take effect atomically, and are saved to config.json
|
||||
function savePreferences () {
|
||||
state.saved.prefs = Object.assign(state.saved.prefs || {}, state.unsaved.prefs)
|
||||
saveState()
|
||||
State.save(state)
|
||||
update()
|
||||
}
|
||||
|
||||
// Don't write state.saved to file more than once a second
|
||||
@@ -520,43 +533,11 @@ function saveStateThrottled () {
|
||||
if (state.saveStateTimeout) return
|
||||
state.saveStateTimeout = setTimeout(function () {
|
||||
delete state.saveStateTimeout
|
||||
saveState()
|
||||
State.save(state)
|
||||
update()
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
// Write state.saved to the JSON state file
|
||||
function saveState () {
|
||||
console.log('saving state to ' + appConfig.filePath)
|
||||
|
||||
// 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, function (err) {
|
||||
if (err) console.error(err)
|
||||
ipcRenderer.send('savedState')
|
||||
})
|
||||
|
||||
// Update right away, don't wait for the state to save
|
||||
update()
|
||||
}
|
||||
|
||||
// Called when the user adds files (.torrent, files to seed, subtitles) to the app
|
||||
// via any method (drag-drop, drag to app icon, command line)
|
||||
function onOpen (files) {
|
||||
@@ -782,6 +763,7 @@ function findFilesRecursive (paths, cb) {
|
||||
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)
|
||||
}
|
||||
})
|
||||
@@ -1018,10 +1000,13 @@ function openPlayer (infoHash, index, cb) {
|
||||
|
||||
// update UI to show pending playback
|
||||
if (torrentSummary.progress !== 1) sound.play('PLAY')
|
||||
// TODO: remove torrentSummary.playStatus
|
||||
torrentSummary.playStatus = 'requested'
|
||||
update()
|
||||
|
||||
var timeout = setTimeout(function () {
|
||||
telemetry.logPlayAttempt('timeout')
|
||||
// TODO: remove torrentSummary.playStatus
|
||||
torrentSummary.playStatus = 'timeout' /* no seeders available? */
|
||||
sound.play('ERROR')
|
||||
cb(new Error('Playback timed out. Try again.'))
|
||||
@@ -1087,25 +1072,41 @@ function openPlayerFromActiveTorrent (torrentSummary, index, timeout, cb) {
|
||||
}
|
||||
|
||||
function closePlayer (cb) {
|
||||
console.log('closePlayer')
|
||||
|
||||
// Quit any external players, like Chromecast/Airplay/etc or VLC
|
||||
if (isCasting()) {
|
||||
Cast.close()
|
||||
Cast.stop()
|
||||
}
|
||||
if (state.playing.location === 'vlc') {
|
||||
ipcRenderer.send('vlcQuit')
|
||||
}
|
||||
state.window.title = config.APP_WINDOW_TITLE
|
||||
// Lets save volume for later
|
||||
|
||||
// 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()
|
||||
|
||||
// Tell the WebTorrent process to kill the torrent-to-HTTP server
|
||||
ipcRenderer.send('wt-stop-server')
|
||||
|
||||
// Tell the OS we're no longer playing media, laptops allowed to sleep again
|
||||
ipcRenderer.send('unblockPowerSave')
|
||||
ipcRenderer.send('onPlayerClose')
|
||||
|
||||
@@ -1136,9 +1137,14 @@ function toggleTorrent (infoHash) {
|
||||
}
|
||||
|
||||
// TODO: use torrentKey, not infoHash
|
||||
function deleteTorrent (infoHash) {
|
||||
function deleteTorrent (infoHash, deleteData) {
|
||||
ipcRenderer.send('wt-stop-torrenting', infoHash)
|
||||
|
||||
if (deleteData) {
|
||||
var torrentSummary = getTorrentSummary(infoHash)
|
||||
moveItemToTrash(torrentSummary)
|
||||
}
|
||||
|
||||
var index = state.saved.torrents.findIndex((x) => x.infoHash === infoHash)
|
||||
if (index > -1) state.saved.torrents.splice(index, 1)
|
||||
saveStateThrottled()
|
||||
@@ -1164,6 +1170,20 @@ function openTorrentContextMenu (infoHash) {
|
||||
var torrentSummary = getTorrentSummary(infoHash)
|
||||
var menu = new electron.remote.Menu()
|
||||
|
||||
menu.append(new electron.remote.MenuItem({
|
||||
label: 'Remove From List',
|
||||
click: () => deleteTorrent(torrentSummary.infoHash, false)
|
||||
}))
|
||||
|
||||
menu.append(new electron.remote.MenuItem({
|
||||
label: 'Remove Data File',
|
||||
click: () => deleteTorrent(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',
|
||||
@@ -1204,6 +1224,10 @@ function showItemInFolder (torrentSummary) {
|
||||
ipcRenderer.send('showItemInFolder', getTorrentPath(torrentSummary))
|
||||
}
|
||||
|
||||
function moveItemToTrash (torrentSummary) {
|
||||
ipcRenderer.send('moveItemToTrash', getTorrentPath(torrentSummary))
|
||||
}
|
||||
|
||||
function saveTorrentFileAs (torrentSummary) {
|
||||
var newFileName = `${path.parse(torrentSummary.name).name}.torrent`
|
||||
var opts = {
|
||||
@@ -1341,3 +1365,7 @@ function onBlur () {
|
||||
state.window.isFocused = false
|
||||
update()
|
||||
}
|
||||
|
||||
function onVisibilityChange () {
|
||||
state.window.isVisible = !document.webkitHidden
|
||||
}
|
||||
|
||||
@@ -218,7 +218,8 @@ function TorrentList (state) {
|
||||
// Show a single torrentSummary file in the details view for a single torrent
|
||||
function renderFileRow (torrentSummary, file, index) {
|
||||
// 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 progress = ''
|
||||
if (torrentSummary.progress && torrentSummary.progress.files) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
module.exports = OpenTorrentAddressModal
|
||||
|
||||
var {dispatch} = require('../lib/dispatcher')
|
||||
var {dispatch, dispatcher} = require('../lib/dispatcher')
|
||||
var hx = require('../lib/hx')
|
||||
|
||||
function OpenTorrentAddressModal (state) {
|
||||
@@ -11,8 +11,8 @@ function OpenTorrentAddressModal (state) {
|
||||
<input id='add-torrent-url' type='text' onkeypress=${handleKeyPress} />
|
||||
</p>
|
||||
<p class='float-right'>
|
||||
<button class='button button-flat' onclick=${handleCancel}>CANCEL</button>
|
||||
<button class='button button-raised' onclick=${handleOK}>OK</button>
|
||||
<button class='button button-flat' onclick=${dispatcher('exitModal')}>Cancel</button>
|
||||
<button class='button button-raised' onclick=${handleOK}>OK</button>
|
||||
</p>
|
||||
<script>document.querySelector('#add-torrent-url').focus()</script>
|
||||
</div>
|
||||
@@ -27,7 +27,3 @@ function handleOK () {
|
||||
dispatch('exitModal')
|
||||
dispatch('addTorrent', document.querySelector('#add-torrent-url').value)
|
||||
}
|
||||
|
||||
function handleCancel () {
|
||||
dispatch('exitModal')
|
||||
}
|
||||
|
||||
@@ -143,6 +143,7 @@ function renderMedia (state) {
|
||||
} else if (elem.webkitAudioDecodedByteCount === 0) {
|
||||
dispatch('mediaError', 'Audio codec unsupported')
|
||||
} else {
|
||||
dispatch('mediaSuccess')
|
||||
elem.play()
|
||||
}
|
||||
}
|
||||
@@ -279,8 +280,10 @@ function renderCastScreen (state) {
|
||||
}
|
||||
|
||||
var isStarting = state.playing.location.endsWith('-pending')
|
||||
var castName = state.playing.castName
|
||||
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 = ''
|
||||
|
||||
// Show a nice title image, if possible
|
||||
@@ -299,6 +302,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) {
|
||||
var subtitles = state.playing.subtitles
|
||||
if (!subtitles.tracks.length || !subtitles.showMenu) return
|
||||
@@ -316,7 +343,7 @@ function renderSubtitlesOptions (state) {
|
||||
var noneSelected = state.playing.subtitles.selectedIndex === -1
|
||||
var noneClass = 'radio_button_' + (noneSelected ? 'checked' : 'unchecked')
|
||||
return hx`
|
||||
<ul.subtitles-list>
|
||||
<ul.options-list>
|
||||
${items}
|
||||
<li onclick=${dispatcher('selectSubtitle', -1)}>
|
||||
<i.icon>${noneClass}</i>
|
||||
@@ -378,72 +405,49 @@ function renderPlayerControls (state) {
|
||||
}
|
||||
|
||||
// If we've detected a Chromecast or AppleTV, the user can play video there
|
||||
var isOnChromecast = state.playing.location.startsWith('chromecast')
|
||||
var isOnAirplay = state.playing.location.startsWith('airplay')
|
||||
var isOnDlna = state.playing.location.startsWith('dlna')
|
||||
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>
|
||||
`)
|
||||
}
|
||||
var castTypes = ['chromecast', 'airplay', 'dlna']
|
||||
var isCastingAnywhere = castTypes.some(
|
||||
(castType) => state.playing.location.startsWith(castType))
|
||||
|
||||
// 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'},
|
||||
'dnla': {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 volumeIcon = 'volume_' + (
|
||||
volume === 0 ? 'off'
|
||||
@@ -496,6 +500,7 @@ function renderPlayerControls (state) {
|
||||
return hx`
|
||||
<div class='controls'>
|
||||
${elements}
|
||||
${renderCastOptions(state)}
|
||||
${renderSubtitlesOptions(state)}
|
||||
</div>
|
||||
`
|
||||
|
||||
@@ -10,9 +10,9 @@ function UpdateAvailableModal (state) {
|
||||
<div class='update-available-modal'>
|
||||
<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>
|
||||
<button class='primary' onclick=${handleOK}>Show Download Page</button>
|
||||
<button class='cancel' onclick=${handleCancel}>Skip This Release</button>
|
||||
<p class='float-right'>
|
||||
<button class='button button-flat' onclick=${handleCancel}>Skip This Release</button>
|
||||
<button class='button button-raised' onclick=${handleOK}>Show Download Page</button>
|
||||
</p>
|
||||
</div>
|
||||
`
|
||||
|
||||
@@ -63,6 +63,10 @@ function init () {
|
||||
|
||||
ipc.send('ipcReadyWebTorrent')
|
||||
|
||||
window.addEventListener('error', (e) =>
|
||||
ipc.send('wt-uncaught-error', {message: e.error.message, stack: e.error.stack}),
|
||||
true)
|
||||
|
||||
setInterval(updateTorrentProgress, 1000)
|
||||
}
|
||||
|
||||
@@ -173,7 +177,7 @@ function saveTorrentFile (torrentKey) {
|
||||
}
|
||||
|
||||
// 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) {
|
||||
if (err) return console.log('error saving torrent file %s: %o', torrentPath, err)
|
||||
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
|
||||
// Calls back with (torrentPath, exists). Logs, does not call back on error
|
||||
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) {
|
||||
cb(torrentPath, exists)
|
||||
})
|
||||
@@ -199,10 +203,10 @@ function generateTorrentPoster (torrentKey) {
|
||||
torrentPoster(torrent, function (err, buf, extension) {
|
||||
if (err) return console.log('error generating poster: %o', err)
|
||||
// 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)
|
||||
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) {
|
||||
if (err) return console.log('error saving poster: %o', err)
|
||||
// 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 |
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 76 KiB |
Reference in New Issue
Block a user