Compare commits

..

44 Commits

Author SHA1 Message Date
DC
e34223fc94 0.8.0 2016-06-23 07:31:18 -07:00
Gediminas Petrikas
15f733f11c Windows Thumbnail Bar
* While in the player view, show a play/pause toggle in the thumbnail
2016-06-23 07:12:32 -07:00
DC
7526b18507 Show which cast device you're connected to 2016-06-23 07:09:49 -07:00
DC
0af6007632 Refactor cast menu 2016-06-23 07:09:49 -07:00
DC
1bc3cd1d51 Make check-deps handle older verions of node 2016-06-23 07:09:49 -07:00
DC
92bafd695d Listen to events on new cast devices 2016-06-23 07:09:49 -07:00
DC
78a2ee4e85 Cast menu
Fixes #301
2016-06-23 07:09:49 -07:00
Feross Aboukhadijeh
8b9346d767 Prevent playback continues after minimize (#662)
Fixes #649.
2016-06-23 06:59:55 -07:00
DC
06d3bd3f93 Seeding: sort files by path (#663)
Fixes a bug where you could create duplicate torrents by adding the same folder multiple times, because the file order & therefore the infohash was nondeterministic
2016-06-23 02:14:23 -07:00
Mathias Rasmussen
1af7e4ef19 Remove torrent data support (#641)
* add moveItemToTrash to shell

* delete torrent/data + context menu items
2016-06-22 18:58:16 -07:00
DC
8e64e4120b Telemetry: add Privacy section to README 2016-06-21 21:58:15 -07:00
DC
b983559763 Telemetry: address PR comments 2016-06-21 21:58:15 -07:00
DC
e62527de23 Telemetry: limit POST to 100kb 2016-06-21 04:20:12 -07:00
DC
1f51f35f8e Telemetry: report uncaught errors 2016-06-21 03:45:34 -07:00
DC
c3686417e3 Telemetry 2016-06-20 22:33:17 -07:00
Feross Aboukhadijeh
746e10c025 author email 2016-06-15 17:41:04 -07:00
Feross Aboukhadijeh
98389fc07c Merge pull request #644 from feross/dc/ux
Support .wmv video via VLC
2016-06-15 16:35:12 -07:00
DC
aaebf93db4 Support .wmv video via VLC. Fixes #625 2016-06-15 16:22:16 -07:00
Feross Aboukhadijeh
12500dfb64 modals should stick to title bar 2016-06-13 16:15:45 -07:00
Feross Aboukhadijeh
acc8e7923a Update modal: improve buttons 2016-06-13 16:15:01 -07:00
Feross Aboukhadijeh
9aa5775528 Merge pull request #636 from mathiasvr/modals
fix modal inconsistencies
2016-06-13 16:08:05 -07:00
Mathias Rasmussen
2a2d71289a fix modal inconsistencies 2016-06-13 16:18:43 +02:00
Feross Aboukhadijeh
ae28e34fd5 Merge pull request #634 from feross/dc/fix
Make posters from jpeg files
2016-06-11 23:31:20 -07:00
DC
6b175e7d40 Make posters from jpeg files 2016-06-11 23:10:48 -07:00
Feross Aboukhadijeh
2c6d74e8ef Merge pull request #632 from mathiasvr/patch
handle play/pause when window is hidden
2016-06-10 20:23:58 -07:00
Mathias Rasmussen
3b832595fe handle play/pause when window is hidden
using `webkitvisibilitychange` event
2016-06-10 04:56:33 +02:00
Feross Aboukhadijeh
bf372029fb changelog 2016-06-03 15:10:11 -07:00
Feross Aboukhadijeh
17ce7e519c Merge pull request #623 from feross/tray
Fix Windows tray state
2016-06-02 23:24:33 -07:00
Feross Aboukhadijeh
1f6a112df7 changelog 2016-06-02 23:14:37 -07:00
Feross Aboukhadijeh
9d3e26f15a 0.7.2 2016-06-02 22:46:10 -07:00
Feross Aboukhadijeh
8a95895254 Ensure state.saved.prefs exists
Fixes a bug I introduced in the migrations
2016-06-02 22:33:22 -07:00
Feross Aboukhadijeh
5d71f9e9c6 code cleanup 2016-06-02 22:32:01 -07:00
Feross Aboukhadijeh
0ec6fb5a93 Fix Windows tray state
After this PR, the Windows tray state will be correct. "Show
WebTorrent" vs. "Hide WebTorrent"
2016-06-02 21:16:10 -07:00
Feross Aboukhadijeh
5d410457ce Merge pull request #622 from feross/about
Fix title in About Window
2016-06-02 21:12:58 -07:00
Feross Aboukhadijeh
c6cd21b8ff Fix title in About Window
Shows up as 'Electron' on Windows
2016-06-02 20:50:04 -07:00
Feross Aboukhadijeh
2235b2fa82 changelog 2016-06-02 20:10:40 -07:00
Feross Aboukhadijeh
65e0b5d6e7 0.7.1 2016-06-02 19:58:43 -07:00
Feross Aboukhadijeh
ea64411570 Merge pull request #621 from feross/f/fix
Fix v0.7 bug - refactor state
2016-06-02 19:53:53 -07:00
Feross Aboukhadijeh
9348c61a84 stray console.log 2016-06-02 19:53:37 -07:00
Feross Aboukhadijeh
d9aa3822ee Set selections by default
In case the user tries to change a file selection state before enabling
the torrent.
2016-06-02 19:52:35 -07:00
Feross Aboukhadijeh
e86bd26800 Refactor state save/load
- Fix bug where new install was relying on the migration to run on
startup to fix up the default config
- Moved save/load functions into state.js
- Removed exported getInitialState, getDefaultSavedState since that's
leaky. The state module should take care of that.
2016-06-02 19:46:29 -07:00
Feross Aboukhadijeh
6d8cec17de npm run open-config -- open folder 2016-06-02 19:40:12 -07:00
Feross Aboukhadijeh
572f084570 Merge pull request #620 from feross/skip-keys
Change step Forward/Backward shortcuts to match VLC's
2016-06-02 17:23:37 -07:00
Feross Aboukhadijeh
4a3ca5459d Change step Forward/Backward shortcuts to match VLC's
We got this feature idea from VLC, so let's align with the keyboard
shortcuts that they use :)

Closes #618.
2016-06-02 16:41:39 -07:00
31 changed files with 854 additions and 515 deletions

View File

@@ -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

View File

@@ -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
[![js-standard-style](https://cdn.rawgit.com/feross/standard/master/badge.svg)](https://github.com/feross/standard)

View File

@@ -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
}

View File

@@ -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)

View File

@@ -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)

View File

@@ -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 () {

View File

@@ -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

View File

@@ -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
},

View File

@@ -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
View 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)
}

View File

@@ -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()
})

View File

@@ -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'

View File

@@ -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",

View File

@@ -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 () {

View File

@@ -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
}
}
}

View File

@@ -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
View 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
}

View File

@@ -24,7 +24,8 @@ function isVideo (file) {
'.mp4',
'.mpg',
'.ogv',
'.webm'
'.webm',
'.wmv'
].includes(ext)
}

View File

@@ -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

View 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, '/')

View File

@@ -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;

View File

@@ -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
}

View File

@@ -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) {

View File

@@ -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')
}

View File

@@ -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>
`

View File

@@ -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>
`

View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 B

View File

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 76 KiB