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
|
# 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
|
## v0.7.0 - 2016-06-02
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -81,6 +81,10 @@ brew install wine
|
|||||||
|
|
||||||
(Requires the [Homebrew](http://brew.sh/) package manager.)
|
(Requires the [Homebrew](http://brew.sh/) package manager.)
|
||||||
|
|
||||||
|
### Privacy
|
||||||
|
|
||||||
|
WebTorrent Desktop collects some basic usage stats to help us make the app better. For example, we track 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
|
### Code Style
|
||||||
|
|
||||||
[](https://github.com/feross/standard)
|
[](https://github.com/feross/standard)
|
||||||
|
|||||||
@@ -3,7 +3,48 @@
|
|||||||
var fs = require('fs')
|
var fs = require('fs')
|
||||||
var cp = require('child_process')
|
var cp = require('child_process')
|
||||||
|
|
||||||
var BUILT_IN_DEPS = ['child_process', 'electron', 'fs', 'os', 'path']
|
// We can't use `builtin-modules` here since our TravisCI
|
||||||
|
// setup expects this file to run with no dependencies
|
||||||
|
var BUILT_IN_NODE_MODULES = [
|
||||||
|
'assert',
|
||||||
|
'buffer',
|
||||||
|
'child_process',
|
||||||
|
'cluster',
|
||||||
|
'console',
|
||||||
|
'constants',
|
||||||
|
'crypto',
|
||||||
|
'dgram',
|
||||||
|
'dns',
|
||||||
|
'domain',
|
||||||
|
'events',
|
||||||
|
'fs',
|
||||||
|
'http',
|
||||||
|
'https',
|
||||||
|
'module',
|
||||||
|
'net',
|
||||||
|
'os',
|
||||||
|
'path',
|
||||||
|
'process',
|
||||||
|
'punycode',
|
||||||
|
'querystring',
|
||||||
|
'readline',
|
||||||
|
'repl',
|
||||||
|
'stream',
|
||||||
|
'string_decoder',
|
||||||
|
'timers',
|
||||||
|
'tls',
|
||||||
|
'tty',
|
||||||
|
'url',
|
||||||
|
'util',
|
||||||
|
'v8',
|
||||||
|
'vm',
|
||||||
|
'zlib'
|
||||||
|
]
|
||||||
|
|
||||||
|
var BUILT_IN_ELECTRON_MODULES = [ 'electron' ]
|
||||||
|
|
||||||
|
var BUILT_IN_DEPS = [].concat(BUILT_IN_NODE_MODULES, BUILT_IN_ELECTRON_MODULES)
|
||||||
|
|
||||||
var EXECUTABLE_DEPS = ['gh-release', 'standard']
|
var EXECUTABLE_DEPS = ['gh-release', 'standard']
|
||||||
|
|
||||||
main()
|
main()
|
||||||
@@ -19,10 +60,10 @@ function main () {
|
|||||||
var packageDeps = findPackageDeps()
|
var packageDeps = findPackageDeps()
|
||||||
|
|
||||||
var missingDeps = usedDeps.filter(
|
var missingDeps = usedDeps.filter(
|
||||||
(dep) => !packageDeps.includes(dep) && !BUILT_IN_DEPS.includes(dep)
|
(dep) => !includes(packageDeps, dep) && !includes(BUILT_IN_DEPS, dep)
|
||||||
)
|
)
|
||||||
var unusedDeps = packageDeps.filter(
|
var unusedDeps = packageDeps.filter(
|
||||||
(dep) => !usedDeps.includes(dep) && !EXECUTABLE_DEPS.includes(dep)
|
(dep) => !includes(usedDeps, dep) && !includes(EXECUTABLE_DEPS, dep)
|
||||||
)
|
)
|
||||||
|
|
||||||
if (missingDeps.length > 0) {
|
if (missingDeps.length > 0) {
|
||||||
@@ -52,3 +93,7 @@ function findUsedDeps () {
|
|||||||
var stdout = cp.execSync('./bin/list-deps.sh')
|
var stdout = cp.execSync('./bin/list-deps.sh')
|
||||||
return stdout.toString().trim().split('\n')
|
return stdout.toString().trim().split('\n')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function includes (arr, elem) {
|
||||||
|
return arr.indexOf(elem) >= 0
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,7 +2,5 @@
|
|||||||
|
|
||||||
var config = require('../config')
|
var config = require('../config')
|
||||||
var open = require('open')
|
var open = require('open')
|
||||||
var path = require('path')
|
|
||||||
|
|
||||||
var configPath = path.join(config.CONFIG_PATH, 'config.json')
|
open(config.CONFIG_PATH)
|
||||||
open(configPath)
|
|
||||||
|
|||||||
57
config.js
57
config.js
@@ -10,6 +10,9 @@ var PORTABLE_PATH = path.join(path.dirname(process.execPath), 'Portable Settings
|
|||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
ANNOUNCEMENT_URL: 'https://webtorrent.io/desktop/announcement',
|
ANNOUNCEMENT_URL: 'https://webtorrent.io/desktop/announcement',
|
||||||
|
AUTO_UPDATE_URL: 'https://webtorrent.io/desktop/update',
|
||||||
|
CRASH_REPORT_URL: 'https://webtorrent.io/desktop/crash-report',
|
||||||
|
TELEMETRY_URL: 'https://webtorrent.io/desktop/telemetry',
|
||||||
|
|
||||||
APP_COPYRIGHT: 'Copyright © 2014-2016 ' + APP_TEAM,
|
APP_COPYRIGHT: 'Copyright © 2014-2016 ' + APP_TEAM,
|
||||||
APP_FILE_ICON: path.join(__dirname, 'static', 'WebTorrentFile'),
|
APP_FILE_ICON: path.join(__dirname, 'static', 'WebTorrentFile'),
|
||||||
@@ -19,16 +22,40 @@ module.exports = {
|
|||||||
APP_VERSION: APP_VERSION,
|
APP_VERSION: APP_VERSION,
|
||||||
APP_WINDOW_TITLE: APP_NAME + ' (BETA)',
|
APP_WINDOW_TITLE: APP_NAME + ' (BETA)',
|
||||||
|
|
||||||
AUTO_UPDATE_URL: 'https://webtorrent.io/desktop/update',
|
|
||||||
|
|
||||||
CRASH_REPORT_URL: 'https://webtorrent.io/desktop/crash-report',
|
|
||||||
|
|
||||||
CONFIG_PATH: getConfigPath(),
|
CONFIG_PATH: getConfigPath(),
|
||||||
CONFIG_POSTER_PATH: path.join(getConfigPath(), 'Posters'),
|
|
||||||
CONFIG_TORRENT_PATH: path.join(getConfigPath(), 'Torrents'),
|
DEFAULT_TORRENTS: [
|
||||||
|
{
|
||||||
|
name: 'Big Buck Bunny',
|
||||||
|
posterFileName: 'bigBuckBunny.jpg',
|
||||||
|
torrentFileName: 'bigBuckBunny.torrent'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Cosmos Laundromat (Preview)',
|
||||||
|
posterFileName: 'cosmosLaundromat.jpg',
|
||||||
|
torrentFileName: 'cosmosLaundromat.torrent'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Sintel',
|
||||||
|
posterFileName: 'sintel.jpg',
|
||||||
|
torrentFileName: 'sintel.torrent'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Tears of Steel',
|
||||||
|
posterFileName: 'tearsOfSteel.jpg',
|
||||||
|
torrentFileName: 'tearsOfSteel.torrent'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'The WIRED CD - Rip. Sample. Mash. Share.',
|
||||||
|
posterFileName: 'wiredCd.jpg',
|
||||||
|
torrentFileName: 'wiredCd.torrent'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
DELAYED_INIT: 3000 /* 3 seconds */,
|
DELAYED_INIT: 3000 /* 3 seconds */,
|
||||||
|
|
||||||
|
DEFAULT_DOWNLOAD_PATH: getDefaultDownloadPath(),
|
||||||
|
|
||||||
GITHUB_URL: 'https://github.com/feross/webtorrent-desktop',
|
GITHUB_URL: 'https://github.com/feross/webtorrent-desktop',
|
||||||
GITHUB_URL_ISSUES: 'https://github.com/feross/webtorrent-desktop/issues',
|
GITHUB_URL_ISSUES: 'https://github.com/feross/webtorrent-desktop/issues',
|
||||||
GITHUB_URL_RAW: 'https://raw.githubusercontent.com/feross/webtorrent-desktop/master',
|
GITHUB_URL_RAW: 'https://raw.githubusercontent.com/feross/webtorrent-desktop/master',
|
||||||
@@ -38,8 +65,10 @@ module.exports = {
|
|||||||
IS_PORTABLE: isPortable(),
|
IS_PORTABLE: isPortable(),
|
||||||
IS_PRODUCTION: isProduction(),
|
IS_PRODUCTION: isProduction(),
|
||||||
|
|
||||||
|
POSTER_PATH: path.join(getConfigPath(), 'Posters'),
|
||||||
ROOT_PATH: __dirname,
|
ROOT_PATH: __dirname,
|
||||||
STATIC_PATH: path.join(__dirname, 'static'),
|
STATIC_PATH: path.join(__dirname, 'static'),
|
||||||
|
TORRENT_PATH: path.join(getConfigPath(), 'Torrents'),
|
||||||
|
|
||||||
WINDOW_ABOUT: 'file://' + path.join(__dirname, 'renderer', 'about.html'),
|
WINDOW_ABOUT: 'file://' + path.join(__dirname, 'renderer', 'about.html'),
|
||||||
WINDOW_MAIN: 'file://' + path.join(__dirname, 'renderer', 'main.html'),
|
WINDOW_MAIN: 'file://' + path.join(__dirname, 'renderer', 'main.html'),
|
||||||
@@ -57,6 +86,22 @@ function getConfigPath () {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getDefaultDownloadPath () {
|
||||||
|
if (!process || !process.type) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPortable()) {
|
||||||
|
return path.join(getConfigPath(), 'Downloads')
|
||||||
|
}
|
||||||
|
|
||||||
|
var electron = require('electron')
|
||||||
|
|
||||||
|
return process.type === 'renderer'
|
||||||
|
? electron.remote.app.getPath('downloads')
|
||||||
|
: electron.app.getPath('downloads')
|
||||||
|
}
|
||||||
|
|
||||||
function isPortable () {
|
function isPortable () {
|
||||||
try {
|
try {
|
||||||
return process.platform === 'win32' && isProduction() && !!fs.statSync(PORTABLE_PATH)
|
return process.platform === 'win32' && isProduction() && !!fs.statSync(PORTABLE_PATH)
|
||||||
|
|||||||
@@ -68,6 +68,13 @@ function init () {
|
|||||||
|
|
||||||
// To keep app startup fast, some code is delayed.
|
// To keep app startup fast, some code is delayed.
|
||||||
setTimeout(delayedInit, config.DELAYED_INIT)
|
setTimeout(delayedInit, config.DELAYED_INIT)
|
||||||
|
|
||||||
|
// Report uncaught exceptions
|
||||||
|
process.on('uncaughtException', (err) => {
|
||||||
|
console.error(err)
|
||||||
|
var errJSON = {message: err.message, stack: err.stack}
|
||||||
|
windows.main.dispatch('uncaughtError', 'main', errJSON)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
app.once('ipcReady', function () {
|
app.once('ipcReady', function () {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ var shell = require('./shell')
|
|||||||
var shortcuts = require('./shortcuts')
|
var shortcuts = require('./shortcuts')
|
||||||
var vlc = require('./vlc')
|
var vlc = require('./vlc')
|
||||||
var windows = require('./windows')
|
var windows = require('./windows')
|
||||||
|
var thumbnail = require('./thumbnail')
|
||||||
|
|
||||||
// Messages from the main process, to be sent once the WebTorrent process starts
|
// Messages from the main process, to be sent once the WebTorrent process starts
|
||||||
var messageQueueMainToWebTorrent = []
|
var messageQueueMainToWebTorrent = []
|
||||||
@@ -68,6 +69,10 @@ function init () {
|
|||||||
shortcuts.onPlayerOpen()
|
shortcuts.onPlayerOpen()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
ipc.on('updateThumbnailBar', function (e, isPaused) {
|
||||||
|
thumbnail.updateThumbarButtons(isPaused)
|
||||||
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Power Save Blocker
|
* Power Save Blocker
|
||||||
*/
|
*/
|
||||||
@@ -81,6 +86,7 @@ function init () {
|
|||||||
|
|
||||||
ipc.on('openItem', (e, ...args) => shell.openItem(...args))
|
ipc.on('openItem', (e, ...args) => shell.openItem(...args))
|
||||||
ipc.on('showItemInFolder', (e, ...args) => shell.showItemInFolder(...args))
|
ipc.on('showItemInFolder', (e, ...args) => shell.showItemInFolder(...args))
|
||||||
|
ipc.on('moveItemToTrash', (e, ...args) => shell.moveItemToTrash(...args))
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Windows: Main
|
* Windows: Main
|
||||||
|
|||||||
13
main/menu.js
13
main/menu.js
@@ -16,6 +16,7 @@ var config = require('../config')
|
|||||||
var dialog = require('./dialog')
|
var dialog = require('./dialog')
|
||||||
var shell = require('./shell')
|
var shell = require('./shell')
|
||||||
var windows = require('./windows')
|
var windows = require('./windows')
|
||||||
|
var thumbnail = require('./thumbnail')
|
||||||
|
|
||||||
var menu
|
var menu
|
||||||
|
|
||||||
@@ -33,6 +34,8 @@ function onPlayerClose () {
|
|||||||
getMenuItem('Increase Speed').enabled = false
|
getMenuItem('Increase Speed').enabled = false
|
||||||
getMenuItem('Decrease Speed').enabled = false
|
getMenuItem('Decrease Speed').enabled = false
|
||||||
getMenuItem('Add Subtitles File...').enabled = false
|
getMenuItem('Add Subtitles File...').enabled = false
|
||||||
|
|
||||||
|
thumbnail.showPlayerThumbnailBar()
|
||||||
}
|
}
|
||||||
|
|
||||||
function onPlayerOpen () {
|
function onPlayerOpen () {
|
||||||
@@ -44,6 +47,8 @@ function onPlayerOpen () {
|
|||||||
getMenuItem('Increase Speed').enabled = true
|
getMenuItem('Increase Speed').enabled = true
|
||||||
getMenuItem('Decrease Speed').enabled = true
|
getMenuItem('Decrease Speed').enabled = true
|
||||||
getMenuItem('Add Subtitles File...').enabled = true
|
getMenuItem('Add Subtitles File...').enabled = true
|
||||||
|
|
||||||
|
thumbnail.hidePlayerThumbnailBar()
|
||||||
}
|
}
|
||||||
|
|
||||||
function onToggleAlwaysOnTop (flag) {
|
function onToggleAlwaysOnTop (flag) {
|
||||||
@@ -217,13 +222,17 @@ function getMenuTemplate () {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Step Forward',
|
label: 'Step Forward',
|
||||||
accelerator: 'CmdOrCtrl+Alt+Right',
|
accelerator: process.platform === 'darwin'
|
||||||
|
? 'CmdOrCtrl+Alt+Right'
|
||||||
|
: 'Alt+Right',
|
||||||
click: () => windows.main.dispatch('skip', 1),
|
click: () => windows.main.dispatch('skip', 1),
|
||||||
enabled: false
|
enabled: false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Step Backward',
|
label: 'Step Backward',
|
||||||
accelerator: 'CmdOrCtrl+Alt+Left',
|
accelerator: process.platform === 'darwin'
|
||||||
|
? 'CmdOrCtrl+Alt+Left'
|
||||||
|
: 'Alt+Left',
|
||||||
click: () => windows.main.dispatch('skip', -1),
|
click: () => windows.main.dispatch('skip', -1),
|
||||||
enabled: false
|
enabled: false
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
openExternal,
|
openExternal,
|
||||||
openItem,
|
openItem,
|
||||||
showItemInFolder
|
showItemInFolder,
|
||||||
|
moveItemToTrash
|
||||||
}
|
}
|
||||||
|
|
||||||
var electron = require('electron')
|
var electron = require('electron')
|
||||||
@@ -30,3 +31,11 @@ function showItemInFolder (path) {
|
|||||||
log(`showItemInFolder: ${path}`)
|
log(`showItemInFolder: ${path}`)
|
||||||
electron.shell.showItemInFolder(path)
|
electron.shell.showItemInFolder(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move the given file to trash and returns a boolean status for the operation.
|
||||||
|
*/
|
||||||
|
function moveItemToTrash (path) {
|
||||||
|
log(`moveItemToTrash: ${path}`)
|
||||||
|
electron.shell.moveItemToTrash(path)
|
||||||
|
}
|
||||||
|
|||||||
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,
|
resizable: false,
|
||||||
show: false,
|
show: false,
|
||||||
skipTaskbar: true,
|
skipTaskbar: true,
|
||||||
|
title: 'About ' + config.APP_WINDOW_TITLE,
|
||||||
useContentSize: true,
|
useContentSize: true,
|
||||||
width: 300
|
width: 300
|
||||||
})
|
})
|
||||||
@@ -31,7 +32,7 @@ function init () {
|
|||||||
// No menu on the About window
|
// No menu on the About window
|
||||||
win.setMenu(null)
|
win.setMenu(null)
|
||||||
|
|
||||||
win.webContents.on('did-finish-load', function () {
|
win.webContents.once('did-finish-load', function () {
|
||||||
win.show()
|
win.show()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -51,15 +51,11 @@ function init () {
|
|||||||
menu.onToggleFullScreen(main.win.isFullScreen())
|
menu.onToggleFullScreen(main.win.isFullScreen())
|
||||||
})
|
})
|
||||||
|
|
||||||
win.on('blur', function () {
|
win.on('blur', onWindowBlur)
|
||||||
menu.onWindowBlur()
|
win.on('focus', onWindowFocus)
|
||||||
tray.onWindowBlur()
|
|
||||||
})
|
|
||||||
|
|
||||||
win.on('focus', function () {
|
win.on('hide', onWindowBlur)
|
||||||
menu.onWindowFocus()
|
win.on('show', onWindowFocus)
|
||||||
tray.onWindowFocus()
|
|
||||||
})
|
|
||||||
|
|
||||||
win.on('enter-full-screen', function () {
|
win.on('enter-full-screen', function () {
|
||||||
menu.onToggleFullScreen(true)
|
menu.onToggleFullScreen(true)
|
||||||
@@ -78,7 +74,7 @@ function init () {
|
|||||||
app.quit()
|
app.quit()
|
||||||
} else if (!app.isQuitting) {
|
} else if (!app.isQuitting) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
win.hide()
|
hide()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -207,6 +203,16 @@ function toggleFullScreen (flag) {
|
|||||||
main.win.setFullScreen(flag)
|
main.win.setFullScreen(flag)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onWindowBlur () {
|
||||||
|
menu.onWindowBlur()
|
||||||
|
tray.onWindowBlur()
|
||||||
|
}
|
||||||
|
|
||||||
|
function onWindowFocus () {
|
||||||
|
menu.onWindowFocus()
|
||||||
|
tray.onWindowFocus()
|
||||||
|
}
|
||||||
|
|
||||||
function getIconPath () {
|
function getIconPath () {
|
||||||
return process.platform === 'win32'
|
return process.platform === 'win32'
|
||||||
? config.APP_ICON + '.ico'
|
? config.APP_ICON + '.ico'
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
{
|
{
|
||||||
"name": "webtorrent-desktop",
|
"name": "webtorrent-desktop",
|
||||||
"description": "WebTorrent, the streaming torrent client. For OS X, Windows, and Linux.",
|
"description": "WebTorrent, the streaming torrent client. For OS X, Windows, and Linux.",
|
||||||
"version": "0.7.0",
|
"version": "0.8.0",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "WebTorrent, LLC",
|
"name": "WebTorrent, LLC",
|
||||||
"email": "feross@feross.org",
|
"email": "feross@webtorrent.io",
|
||||||
"url": "https://webtorrent.io"
|
"url": "https://webtorrent.io"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -30,6 +30,7 @@
|
|||||||
"main-loop": "^3.2.0",
|
"main-loop": "^3.2.0",
|
||||||
"musicmetadata": "^2.0.2",
|
"musicmetadata": "^2.0.2",
|
||||||
"network-address": "^1.1.0",
|
"network-address": "^1.1.0",
|
||||||
|
"parse-torrent": "^5.7.3",
|
||||||
"prettier-bytes": "^1.0.1",
|
"prettier-bytes": "^1.0.1",
|
||||||
"run-parallel": "^1.1.6",
|
"run-parallel": "^1.1.6",
|
||||||
"semver": "^5.1.0",
|
"semver": "^5.1.0",
|
||||||
|
|||||||
@@ -3,8 +3,9 @@
|
|||||||
// * Starts and stops casting, provides remote video controls
|
// * Starts and stops casting, provides remote video controls
|
||||||
module.exports = {
|
module.exports = {
|
||||||
init,
|
init,
|
||||||
open,
|
toggleMenu,
|
||||||
close,
|
selectDevice,
|
||||||
|
stop,
|
||||||
play,
|
play,
|
||||||
pause,
|
pause,
|
||||||
seek,
|
seek,
|
||||||
@@ -32,24 +33,49 @@ function init (appState, callback) {
|
|||||||
state = appState
|
state = appState
|
||||||
update = callback
|
update = callback
|
||||||
|
|
||||||
|
state.devices.chromecast = chromecastPlayer()
|
||||||
|
state.devices.dlna = dlnaPlayer()
|
||||||
|
state.devices.airplay = airplayPlayer()
|
||||||
|
|
||||||
// Listen for devices: Chromecast, DLNA and Airplay
|
// Listen for devices: Chromecast, DLNA and Airplay
|
||||||
chromecasts.on('update', function (player) {
|
chromecasts.on('update', function (device) {
|
||||||
state.devices.chromecast = chromecastPlayer(player)
|
// TODO: how do we tell if there are *no longer* any Chromecasts available?
|
||||||
|
// From looking at the code, chromecasts.players only grows, never shrinks
|
||||||
|
state.devices.chromecast.addDevice(device)
|
||||||
})
|
})
|
||||||
|
|
||||||
dlnacasts.on('update', function (player) {
|
dlnacasts.on('update', function (device) {
|
||||||
state.devices.dlna = dlnaPlayer(player)
|
state.devices.dlna.addDevice(device)
|
||||||
})
|
})
|
||||||
|
|
||||||
airplayer.on('update', function (player) {
|
airplayer.on('update', function (device) {
|
||||||
state.devices.airplay = airplayPlayer(player)
|
state.devices.airplay.addDevice(device)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// chromecast player implementation
|
// chromecast player implementation
|
||||||
function chromecastPlayer (player) {
|
function chromecastPlayer () {
|
||||||
function addEvents () {
|
var ret = {
|
||||||
player.on('error', function (err) {
|
device: null,
|
||||||
|
addDevice,
|
||||||
|
getDevices,
|
||||||
|
open,
|
||||||
|
play,
|
||||||
|
pause,
|
||||||
|
stop,
|
||||||
|
status,
|
||||||
|
seek,
|
||||||
|
volume
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
|
||||||
|
function getDevices () {
|
||||||
|
return chromecasts.players
|
||||||
|
}
|
||||||
|
|
||||||
|
function addDevice (device) {
|
||||||
|
device.on('error', function (err) {
|
||||||
|
if (device !== ret.device) return
|
||||||
state.playing.location = 'local'
|
state.playing.location = 'local'
|
||||||
state.errors.push({
|
state.errors.push({
|
||||||
time: new Date().getTime(),
|
time: new Date().getTime(),
|
||||||
@@ -57,7 +83,8 @@ function chromecastPlayer (player) {
|
|||||||
})
|
})
|
||||||
update()
|
update()
|
||||||
})
|
})
|
||||||
player.on('disconnect', function () {
|
device.on('disconnect', function () {
|
||||||
|
if (device !== ret.device) return
|
||||||
state.playing.location = 'local'
|
state.playing.location = 'local'
|
||||||
update()
|
update()
|
||||||
})
|
})
|
||||||
@@ -65,7 +92,7 @@ function chromecastPlayer (player) {
|
|||||||
|
|
||||||
function open () {
|
function open () {
|
||||||
var torrentSummary = state.saved.torrents.find((x) => x.infoHash === state.playing.infoHash)
|
var torrentSummary = state.saved.torrents.find((x) => x.infoHash === state.playing.infoHash)
|
||||||
player.play(state.server.networkURL, {
|
ret.device.play(state.server.networkURL, {
|
||||||
type: 'video/mp4',
|
type: 'video/mp4',
|
||||||
title: config.APP_NAME + ' - ' + torrentSummary.name
|
title: config.APP_NAME + ' - ' + torrentSummary.name
|
||||||
}, function (err) {
|
}, function (err) {
|
||||||
@@ -83,19 +110,19 @@ function chromecastPlayer (player) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function play (callback) {
|
function play (callback) {
|
||||||
player.play(null, null, callback)
|
ret.device.play(null, null, callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
function pause (callback) {
|
function pause (callback) {
|
||||||
player.pause(callback)
|
ret.device.pause(callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
function stop (callback) {
|
function stop (callback) {
|
||||||
player.stop(callback)
|
ret.device.stop(callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
function status () {
|
function status () {
|
||||||
player.status(function (err, status) {
|
ret.device.status(function (err, status) {
|
||||||
if (err) return console.log('error getting %s status: %o', state.playing.location, err)
|
if (err) return console.log('error getting %s status: %o', state.playing.location, err)
|
||||||
state.playing.isPaused = status.playerState === 'PAUSED'
|
state.playing.isPaused = status.playerState === 'PAUSED'
|
||||||
state.playing.currentTime = status.currentTime
|
state.playing.currentTime = status.currentTime
|
||||||
@@ -105,30 +132,31 @@ function chromecastPlayer (player) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function seek (time, callback) {
|
function seek (time, callback) {
|
||||||
player.seek(time, callback)
|
ret.device.seek(time, callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
function volume (volume, callback) {
|
function volume (volume, callback) {
|
||||||
player.volume(volume, callback)
|
ret.device.volume(volume, callback)
|
||||||
}
|
|
||||||
|
|
||||||
addEvents()
|
|
||||||
|
|
||||||
return {
|
|
||||||
player: player,
|
|
||||||
open: open,
|
|
||||||
play: play,
|
|
||||||
pause: pause,
|
|
||||||
stop: stop,
|
|
||||||
status: status,
|
|
||||||
seek: seek,
|
|
||||||
volume: volume
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// airplay player implementation
|
// airplay player implementation
|
||||||
function airplayPlayer (player) {
|
function airplayPlayer () {
|
||||||
function addEvents () {
|
var ret = {
|
||||||
|
device: null,
|
||||||
|
addDevice,
|
||||||
|
getDevices,
|
||||||
|
open,
|
||||||
|
play,
|
||||||
|
pause,
|
||||||
|
stop,
|
||||||
|
status,
|
||||||
|
seek,
|
||||||
|
volume
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
|
||||||
|
function addDevice (player) {
|
||||||
player.on('event', function (event) {
|
player.on('event', function (event) {
|
||||||
switch (event.state) {
|
switch (event.state) {
|
||||||
case 'loading':
|
case 'loading':
|
||||||
@@ -146,8 +174,12 @@ function airplayPlayer (player) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getDevices () {
|
||||||
|
return airplayer.players
|
||||||
|
}
|
||||||
|
|
||||||
function open () {
|
function open () {
|
||||||
player.play(state.server.networkURL, function (err, res) {
|
ret.device.play(state.server.networkURL, function (err, res) {
|
||||||
if (err) {
|
if (err) {
|
||||||
state.playing.location = 'local'
|
state.playing.location = 'local'
|
||||||
state.errors.push({
|
state.errors.push({
|
||||||
@@ -162,19 +194,19 @@ function airplayPlayer (player) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function play (callback) {
|
function play (callback) {
|
||||||
player.resume(callback)
|
ret.device.resume(callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
function pause (callback) {
|
function pause (callback) {
|
||||||
player.pause(callback)
|
ret.device.pause(callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
function stop (callback) {
|
function stop (callback) {
|
||||||
player.stop(callback)
|
ret.device.stop(callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
function status () {
|
function status () {
|
||||||
player.playbackInfo(function (err, res, status) {
|
ret.device.playbackInfo(function (err, res, status) {
|
||||||
if (err) {
|
if (err) {
|
||||||
state.playing.location = 'local'
|
state.playing.location = 'local'
|
||||||
state.errors.push({
|
state.errors.push({
|
||||||
@@ -190,7 +222,7 @@ function airplayPlayer (player) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function seek (time, callback) {
|
function seek (time, callback) {
|
||||||
player.scrub(time, callback)
|
ret.device.scrub(time, callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
function volume (volume, callback) {
|
function volume (volume, callback) {
|
||||||
@@ -198,25 +230,31 @@ function airplayPlayer (player) {
|
|||||||
// TODO: We should just disable the volume slider
|
// TODO: We should just disable the volume slider
|
||||||
state.playing.volume = volume
|
state.playing.volume = volume
|
||||||
}
|
}
|
||||||
|
|
||||||
addEvents()
|
|
||||||
|
|
||||||
return {
|
|
||||||
player: player,
|
|
||||||
open: open,
|
|
||||||
play: play,
|
|
||||||
pause: pause,
|
|
||||||
stop: stop,
|
|
||||||
status: status,
|
|
||||||
seek: seek,
|
|
||||||
volume: volume
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DLNA player implementation
|
// DLNA player implementation
|
||||||
function dlnaPlayer (player) {
|
function dlnaPlayer (player) {
|
||||||
function addEvents () {
|
var ret = {
|
||||||
player.on('error', function (err) {
|
device: null,
|
||||||
|
addDevice,
|
||||||
|
getDevices,
|
||||||
|
open,
|
||||||
|
play,
|
||||||
|
pause,
|
||||||
|
stop,
|
||||||
|
status,
|
||||||
|
seek,
|
||||||
|
volume
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
|
||||||
|
function getDevices () {
|
||||||
|
return dlnacasts.players
|
||||||
|
}
|
||||||
|
|
||||||
|
function addDevice (device) {
|
||||||
|
device.on('error', function (err) {
|
||||||
|
if (device !== ret.device) return
|
||||||
state.playing.location = 'local'
|
state.playing.location = 'local'
|
||||||
state.errors.push({
|
state.errors.push({
|
||||||
time: new Date().getTime(),
|
time: new Date().getTime(),
|
||||||
@@ -224,7 +262,8 @@ function dlnaPlayer (player) {
|
|||||||
})
|
})
|
||||||
update()
|
update()
|
||||||
})
|
})
|
||||||
player.on('disconnect', function () {
|
device.on('disconnect', function () {
|
||||||
|
if (device !== ret.device) return
|
||||||
state.playing.location = 'local'
|
state.playing.location = 'local'
|
||||||
update()
|
update()
|
||||||
})
|
})
|
||||||
@@ -232,7 +271,7 @@ function dlnaPlayer (player) {
|
|||||||
|
|
||||||
function open () {
|
function open () {
|
||||||
var torrentSummary = state.saved.torrents.find((x) => x.infoHash === state.playing.infoHash)
|
var torrentSummary = state.saved.torrents.find((x) => x.infoHash === state.playing.infoHash)
|
||||||
player.play(state.server.networkURL, {
|
ret.device.play(state.server.networkURL, {
|
||||||
type: 'video/mp4',
|
type: 'video/mp4',
|
||||||
title: config.APP_NAME + ' - ' + torrentSummary.name,
|
title: config.APP_NAME + ' - ' + torrentSummary.name,
|
||||||
seek: state.playing.currentTime > 10 ? state.playing.currentTime : 0
|
seek: state.playing.currentTime > 10 ? state.playing.currentTime : 0
|
||||||
@@ -251,19 +290,19 @@ function dlnaPlayer (player) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function play (callback) {
|
function play (callback) {
|
||||||
player.play(null, null, callback)
|
ret.device.play(null, null, callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
function pause (callback) {
|
function pause (callback) {
|
||||||
player.pause(callback)
|
ret.device.pause(callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
function stop (callback) {
|
function stop (callback) {
|
||||||
player.stop(callback)
|
ret.device.stop(callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
function status () {
|
function status () {
|
||||||
player.status(function (err, status) {
|
ret.device.status(function (err, status) {
|
||||||
if (err) return console.log('error getting %s status: %o', state.playing.location, err)
|
if (err) return console.log('error getting %s status: %o', state.playing.location, err)
|
||||||
state.playing.isPaused = status.playerState === 'PAUSED'
|
state.playing.isPaused = status.playerState === 'PAUSED'
|
||||||
state.playing.currentTime = status.currentTime
|
state.playing.currentTime = status.currentTime
|
||||||
@@ -273,61 +312,78 @@ function dlnaPlayer (player) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function seek (time, callback) {
|
function seek (time, callback) {
|
||||||
player.seek(time, callback)
|
ret.device.seek(time, callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
function volume (volume, callback) {
|
function volume (volume, callback) {
|
||||||
player.volume(volume, function (err) {
|
ret.device.volume(volume, function (err) {
|
||||||
// quick volume update
|
// quick volume update
|
||||||
state.playing.volume = volume
|
state.playing.volume = volume
|
||||||
callback(err)
|
callback(err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
addEvents()
|
|
||||||
|
|
||||||
return {
|
|
||||||
player: player,
|
|
||||||
open: open,
|
|
||||||
play: play,
|
|
||||||
pause: pause,
|
|
||||||
stop: stop,
|
|
||||||
status: status,
|
|
||||||
seek: seek,
|
|
||||||
volume: volume
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start polling cast device state, whenever we're connected
|
// Start polling cast device state, whenever we're connected
|
||||||
function startStatusInterval () {
|
function startStatusInterval () {
|
||||||
statusInterval = setInterval(function () {
|
statusInterval = setInterval(function () {
|
||||||
var device = getDevice()
|
var player = getPlayer()
|
||||||
if (device) {
|
if (player) player.status()
|
||||||
device.status()
|
|
||||||
}
|
|
||||||
}, 1000)
|
}, 1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
function open (location) {
|
/*
|
||||||
|
* Shows the device menu for a given cast type ('chromecast', 'airplay', etc)
|
||||||
|
* The menu lists eg. all Chromecasts detected; the user can click one to cast.
|
||||||
|
* If the menu was already showing for that type, hides the menu.
|
||||||
|
*/
|
||||||
|
function toggleMenu (location) {
|
||||||
|
// If the menu is already showing, hide it
|
||||||
|
if (state.devices.castMenu && state.devices.castMenu.location === location) {
|
||||||
|
state.devices.castMenu = null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Never cast to two devices at the same time
|
||||||
if (state.playing.location !== 'local') {
|
if (state.playing.location !== 'local') {
|
||||||
throw new Error('You can\'t connect to ' + location + ' when already connected to another device')
|
throw new Error('You can\'t connect to ' + location + ' when already connected to another device')
|
||||||
}
|
}
|
||||||
|
|
||||||
state.playing.location = location + '-pending'
|
// Find all cast devices of the given type
|
||||||
var device = getDevice(location)
|
var player = getPlayer(location)
|
||||||
if (device) {
|
var devices = player ? player.getDevices() : []
|
||||||
getDevice(location).open()
|
if (devices.length === 0) throw new Error('No ' + location + ' devices available')
|
||||||
startStatusInterval()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Show a menu
|
||||||
|
state.devices.castMenu = {location, devices}
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectDevice (index) {
|
||||||
|
var {location, devices} = state.devices.castMenu
|
||||||
|
|
||||||
|
// Start casting
|
||||||
|
var player = getPlayer(location)
|
||||||
|
player.device = devices[index]
|
||||||
|
player.open()
|
||||||
|
|
||||||
|
// Poll the casting device's status every few seconds
|
||||||
|
startStatusInterval()
|
||||||
|
|
||||||
|
// Show the Connecting... screen
|
||||||
|
state.devices.castMenu = null
|
||||||
|
state.playing.castName = devices[index].name
|
||||||
|
state.playing.location = location + '-pending'
|
||||||
update()
|
update()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stops casting, move video back to local screen
|
// Stops casting, move video back to local screen
|
||||||
function close () {
|
function stop () {
|
||||||
var device = getDevice()
|
var player = getPlayer()
|
||||||
if (device) {
|
if (player) {
|
||||||
device.stop(stoppedCasting)
|
player.stop(function () {
|
||||||
|
player.device = null
|
||||||
|
stoppedCasting()
|
||||||
|
})
|
||||||
clearInterval(statusInterval)
|
clearInterval(statusInterval)
|
||||||
} else {
|
} else {
|
||||||
stoppedCasting()
|
stoppedCasting()
|
||||||
@@ -340,8 +396,8 @@ function stoppedCasting () {
|
|||||||
update()
|
update()
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDevice (location) {
|
function getPlayer (location) {
|
||||||
if (location && state.devices[location]) {
|
if (location) {
|
||||||
return state.devices[location]
|
return state.devices[location]
|
||||||
} else if (state.playing.location === 'chromecast') {
|
} else if (state.playing.location === 'chromecast') {
|
||||||
return state.devices.chromecast
|
return state.devices.chromecast
|
||||||
@@ -355,29 +411,25 @@ function getDevice (location) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function play () {
|
function play () {
|
||||||
var device = getDevice()
|
var player = getPlayer()
|
||||||
if (device) {
|
if (player) player.play(castCallback)
|
||||||
device.play(castCallback)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function pause () {
|
function pause () {
|
||||||
var device = getDevice()
|
var player = getPlayer()
|
||||||
if (device) {
|
if (player) player.pause(castCallback)
|
||||||
device.pause(castCallback)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function setRate (rate) {
|
function setRate (rate) {
|
||||||
var device
|
var player
|
||||||
var result = true
|
var result = true
|
||||||
if (state.playing.location === 'chromecast') {
|
if (state.playing.location === 'chromecast') {
|
||||||
// TODO find how to control playback rate on chromecast
|
// TODO find how to control playback rate on chromecast
|
||||||
castCallback()
|
castCallback()
|
||||||
result = false
|
result = false
|
||||||
} else if (state.playing.location === 'airplay') {
|
} else if (state.playing.location === 'airplay') {
|
||||||
device = state.devices.airplay
|
player = state.devices.airplay
|
||||||
device.rate(rate, castCallback)
|
player.rate(rate, castCallback)
|
||||||
} else {
|
} else {
|
||||||
result = false
|
result = false
|
||||||
}
|
}
|
||||||
@@ -385,17 +437,13 @@ function setRate (rate) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function seek (time) {
|
function seek (time) {
|
||||||
var device = getDevice()
|
var player = getPlayer()
|
||||||
if (device) {
|
if (player) player.seek(time, castCallback)
|
||||||
device.seek(time, castCallback)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function setVolume (volume) {
|
function setVolume (volume) {
|
||||||
var device = getDevice()
|
var player = getPlayer()
|
||||||
if (device) {
|
if (player) player.volume(volume, castCallback)
|
||||||
device.volume(volume, castCallback)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function castCallback () {
|
function castCallback () {
|
||||||
|
|||||||
@@ -18,44 +18,41 @@ function run (state) {
|
|||||||
var version = state.saved.version
|
var version = state.saved.version
|
||||||
|
|
||||||
if (semver.lt(version, '0.7.0')) {
|
if (semver.lt(version, '0.7.0')) {
|
||||||
migrate_0_7_0(state)
|
migrate_0_7_0(state.saved)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Future migrations...
|
if (semver.lt(version, '0.7.2')) {
|
||||||
// if (semver.lt(version, '0.8.0')) {
|
migrate_0_7_2(state.saved)
|
||||||
// migrate_0_8_0(state)
|
}
|
||||||
// }
|
|
||||||
|
|
||||||
// Config is now on the new version
|
// Config is now on the new version
|
||||||
state.saved.version = config.APP_VERSION
|
state.saved.version = config.APP_VERSION
|
||||||
}
|
}
|
||||||
|
|
||||||
function migrate_0_7_0 (state) {
|
function migrate_0_7_0 (saved) {
|
||||||
console.log('migrate to 0.7.0')
|
console.log('migrate to 0.7.0')
|
||||||
|
|
||||||
var fs = require('fs-extra')
|
var fs = require('fs-extra')
|
||||||
var path = require('path')
|
var path = require('path')
|
||||||
|
|
||||||
state.saved.torrents.forEach(function (ts) {
|
saved.torrents.forEach(function (ts) {
|
||||||
var infoHash = ts.infoHash
|
var infoHash = ts.infoHash
|
||||||
|
|
||||||
// Replace torrentPath with torrentFileName
|
// Replace torrentPath with torrentFileName
|
||||||
|
// There are a number of cases to handle here:
|
||||||
|
// * Originally we used absolute paths
|
||||||
|
// * Then, relative paths for the default torrents, eg '../static/sintel.torrent'
|
||||||
|
// * Then, paths computed at runtime for default torrents, eg 'sintel.torrent'
|
||||||
|
// * Finally, now we're getting rid of torrentPath altogether
|
||||||
var src, dst
|
var src, dst
|
||||||
if (ts.torrentPath) {
|
if (ts.torrentPath) {
|
||||||
// There are a number of cases to handle here:
|
|
||||||
// * Originally we used absolute paths
|
|
||||||
// * Then, relative paths for the default torrents, eg '../static/sintel.torrent'
|
|
||||||
// * Then, paths computed at runtime for default torrents, eg 'sintel.torrent'
|
|
||||||
// * Finally, now we're getting rid of torrentPath altogether
|
|
||||||
console.log('replacing torrentPath %s', ts.torrentPath)
|
console.log('replacing torrentPath %s', ts.torrentPath)
|
||||||
if (path.isAbsolute(ts.torrentPath)) {
|
if (path.isAbsolute(ts.torrentPath) || ts.torrentPath.startsWith('..')) {
|
||||||
src = ts.torrentPath
|
|
||||||
} else if (ts.torrentPath.startsWith('..')) {
|
|
||||||
src = ts.torrentPath
|
src = ts.torrentPath
|
||||||
} else {
|
} else {
|
||||||
src = path.join(config.STATIC_PATH, ts.torrentPath)
|
src = path.join(config.STATIC_PATH, ts.torrentPath)
|
||||||
}
|
}
|
||||||
dst = path.join(config.CONFIG_TORRENT_PATH, infoHash + '.torrent')
|
dst = path.join(config.TORRENT_PATH, infoHash + '.torrent')
|
||||||
// Synchronous FS calls aren't ideal, but probably OK in a migration
|
// Synchronous FS calls aren't ideal, but probably OK in a migration
|
||||||
// that only runs once
|
// that only runs once
|
||||||
if (src !== dst) fs.copySync(src, dst)
|
if (src !== dst) fs.copySync(src, dst)
|
||||||
@@ -71,7 +68,7 @@ function migrate_0_7_0 (state) {
|
|||||||
src = path.isAbsolute(ts.posterURL)
|
src = path.isAbsolute(ts.posterURL)
|
||||||
? ts.posterURL
|
? ts.posterURL
|
||||||
: path.join(config.STATIC_PATH, ts.posterURL)
|
: path.join(config.STATIC_PATH, ts.posterURL)
|
||||||
dst = path.join(config.CONFIG_POSTER_PATH, infoHash + extension)
|
dst = path.join(config.POSTER_PATH, infoHash + extension)
|
||||||
// Synchronous FS calls aren't ideal, but probably OK in a migration
|
// Synchronous FS calls aren't ideal, but probably OK in a migration
|
||||||
// that only runs once
|
// that only runs once
|
||||||
if (src !== dst) fs.copySync(src, dst)
|
if (src !== dst) fs.copySync(src, dst)
|
||||||
@@ -88,3 +85,11 @@ function migrate_0_7_0 (state) {
|
|||||||
delete ts.fileModtimes
|
delete ts.fileModtimes
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function migrate_0_7_2 (saved) {
|
||||||
|
if (!saved.prefs) {
|
||||||
|
saved.prefs = {
|
||||||
|
downloadPath: config.DEFAULT_DOWNLOAD_PATH
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,18 +1,20 @@
|
|||||||
var electron = require('electron')
|
|
||||||
var path = require('path')
|
|
||||||
|
|
||||||
var remote = electron.remote
|
|
||||||
|
|
||||||
var config = require('../../config')
|
|
||||||
var LocationHistory = require('./location-history')
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
getInitialState,
|
|
||||||
getDefaultPlayState,
|
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 {
|
return {
|
||||||
/*
|
/*
|
||||||
* Temporary state disappears once the program exits.
|
* 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 */
|
selectedInfoHash: null, /* the torrent we've selected to view details. see state.torrents */
|
||||||
playing: getDefaultPlayState(), /* the media (audio or video) that we're currently playing */
|
playing: getDefaultPlayState(), /* the media (audio or video) that we're currently playing */
|
||||||
devices: { /* playback devices like Chromecast and AppleTV */
|
devices: {}, /* playback devices like Chromecast and AppleTV */
|
||||||
airplay: null, /* airplay client. finds and manages AppleTVs */
|
|
||||||
chromecast: null /* chromecast client. finds and manages Chromecasts */
|
|
||||||
},
|
|
||||||
dock: {
|
dock: {
|
||||||
badge: 0,
|
badge: 0,
|
||||||
progress: 0
|
progress: 0
|
||||||
@@ -91,185 +90,58 @@ function getDefaultPlayState () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* If the saved state file doesn't exist yet, here's what we use instead */
|
/* If the saved state file doesn't exist yet, here's what we use instead */
|
||||||
function getDefaultSavedState () {
|
function setupSavedState (cb) {
|
||||||
return {
|
var fs = require('fs-extra')
|
||||||
version: config.APP_VERSION, /* make sure we can upgrade gracefully later */
|
var parseTorrent = require('parse-torrent')
|
||||||
torrents: [
|
var parallel = require('run-parallel')
|
||||||
{
|
|
||||||
status: 'paused',
|
var saved = {
|
||||||
infoHash: '88594aaacbde40ef3e2510c47374ec0aa396c08e',
|
|
||||||
magnetURI: 'magnet:?xt=urn:btih:88594aaacbde40ef3e2510c47374ec0aa396c08e&dn=bbb_sunflower_1080p_30fps_normal.mp4&tr=udp%3A%2F%2Ftracker.openbittorrent.com%3A80%2Fannounce&tr=udp%3A%2F%2Ftracker.publicbt.com%3A80%2Fannounce&tr=wss%3A%2F%2Ftracker.btorrent.xyz&tr=wss%3A%2F%2Ftracker.fastcast.nz&tr=wss%3A%2F%2Ftracker.openwebtorrent.com&tr=wss%3A%2F%2Ftracker.webtorrent.io&ws=http%3A%2F%2Fdistribution.bbb3d.renderfarming.net%2Fvideo%2Fmp4%2Fbbb_sunflower_1080p_30fps_normal.mp4',
|
|
||||||
displayName: 'Big Buck Bunny',
|
|
||||||
posterURL: 'bigBuckBunny.jpg',
|
|
||||||
torrentPath: 'bigBuckBunny.torrent',
|
|
||||||
files: [
|
|
||||||
{
|
|
||||||
length: 276134947,
|
|
||||||
name: 'bbb_sunflower_1080p_30fps_normal.mp4'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
status: 'paused',
|
|
||||||
infoHash: '6a9759bffd5c0af65319979fb7832189f4f3c35d',
|
|
||||||
magnetURI: 'magnet:?xt=urn:btih:6a9759bffd5c0af65319979fb7832189f4f3c35d&dn=sintel.mp4&tr=udp%3A%2F%2Fexodus.desync.com%3A6969&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969&tr=udp%3A%2F%2Ftracker.internetwarriors.net%3A1337&tr=udp%3A%2F%2Ftracker.leechers-paradise.org%3A6969&tr=udp%3A%2F%2Ftracker.openbittorrent.com%3A80&tr=wss%3A%2F%2Ftracker.btorrent.xyz&tr=wss%3A%2F%2Ftracker.fastcast.nz&tr=wss%3A%2F%2Ftracker.openwebtorrent.com&tr=wss%3A%2F%2Ftracker.webtorrent.io&ws=https%3A%2F%2Fwebtorrent.io%2Ftorrents%2Fsintel-1024-surround.mp4',
|
|
||||||
displayName: 'Sintel',
|
|
||||||
posterURL: 'sintel.jpg',
|
|
||||||
torrentPath: 'sintel.torrent',
|
|
||||||
files: [
|
|
||||||
{
|
|
||||||
length: 129241752,
|
|
||||||
name: 'sintel.mp4'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
status: 'paused',
|
|
||||||
infoHash: '02767050e0be2fd4db9a2ad6c12416ac806ed6ed',
|
|
||||||
magnetURI: 'magnet:?xt=urn:btih:02767050e0be2fd4db9a2ad6c12416ac806ed6ed&dn=tears_of_steel_1080p.webm&tr=wss%3A%2F%2Ftracker.btorrent.xyz&tr=wss%3A%2F%2Ftracker.fastcast.nz&tr=wss%3A%2F%2Ftracker.openwebtorrent.com&tr=wss%3A%2F%2Ftracker.webtorrent.io',
|
|
||||||
displayName: 'Tears of Steel',
|
|
||||||
posterURL: 'tearsOfSteel.jpg',
|
|
||||||
torrentPath: 'tearsOfSteel.torrent',
|
|
||||||
files: [
|
|
||||||
{
|
|
||||||
length: 571346576,
|
|
||||||
name: 'tears_of_steel_1080p.webm'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
status: 'paused',
|
|
||||||
infoHash: '6a02592d2bbc069628cd5ed8a54f88ee06ac0ba5',
|
|
||||||
magnetURI: 'magnet:?xt=urn:btih:6a02592d2bbc069628cd5ed8a54f88ee06ac0ba5&dn=CosmosLaundromatFirstCycle&tr=http%3A%2F%2Fbt1.archive.org%3A6969%2Fannounce&tr=http%3A%2F%2Fbt2.archive.org%3A6969%2Fannounce&tr=wss%3A%2F%2Ftracker.btorrent.xyz&tr=wss%3A%2F%2Ftracker.fastcast.nz&tr=wss%3A%2F%2Ftracker.openwebtorrent.com&tr=wss%3A%2F%2Ftracker.webtorrent.io&ws=http%3A%2F%2Fia601508.us.archive.org%2F14%2Fitems%2F&ws=http%3A%2F%2Fia801508.us.archive.org%2F14%2Fitems%2F&ws=https%3A%2F%2Farchive.org%2Fdownload%2F',
|
|
||||||
displayName: 'Cosmos Laundromat (Preview)',
|
|
||||||
posterURL: 'cosmosLaundromat.jpg',
|
|
||||||
torrentPath: 'cosmosLaundromat.torrent',
|
|
||||||
files: [
|
|
||||||
{
|
|
||||||
length: 223580,
|
|
||||||
name: 'Cosmos Laundromat - First Cycle (1080p).gif'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
length: 220087570,
|
|
||||||
name: 'Cosmos Laundromat - First Cycle (1080p).mp4'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
length: 56832560,
|
|
||||||
name: 'Cosmos Laundromat - First Cycle (1080p).ogv'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
length: 3949,
|
|
||||||
name: 'CosmosLaundromat-FirstCycle1080p.en.srt'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
length: 3907,
|
|
||||||
name: 'CosmosLaundromat-FirstCycle1080p.es.srt'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
length: 4119,
|
|
||||||
name: 'CosmosLaundromat-FirstCycle1080p.fr.srt'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
length: 3941,
|
|
||||||
name: 'CosmosLaundromat-FirstCycle1080p.it.srt'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
length: 11264,
|
|
||||||
name: 'CosmosLaundromatFirstCycle_meta.sqlite'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
length: 1204,
|
|
||||||
name: 'CosmosLaundromatFirstCycle_meta.xml'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
status: 'paused',
|
|
||||||
infoHash: '3ba219a8634bf7bae3d848192b2da75ae995589d',
|
|
||||||
magnetURI: 'magnet:?xt=urn:btih:3ba219a8634bf7bae3d848192b2da75ae995589d&dn=The+WIRED+CD+-+Rip.+Sample.+Mash.+Share.&tr=udp%3A%2F%2Fexodus.desync.com%3A6969&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969&tr=udp%3A%2F%2Ftracker.internetwarriors.net%3A1337&tr=udp%3A%2F%2Ftracker.leechers-paradise.org%3A6969&tr=udp%3A%2F%2Ftracker.openbittorrent.com%3A80&tr=wss%3A%2F%2Ftracker.btorrent.xyz&tr=wss%3A%2F%2Ftracker.fastcast.nz&tr=wss%3A%2F%2Ftracker.openwebtorrent.com&tr=wss%3A%2F%2Ftracker.webtorrent.io&ws=https%3A%2F%2Fwebtorrent.io%2Ftorrents%2F',
|
|
||||||
displayName: 'The WIRED CD - Rip. Sample. Mash. Share.',
|
|
||||||
posterURL: 'wired-cd.jpg',
|
|
||||||
torrentPath: 'wired-cd.torrent',
|
|
||||||
files: [
|
|
||||||
{
|
|
||||||
length: 1964275,
|
|
||||||
name: '01 - Beastie Boys - Now Get Busy.mp3'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
length: 3610523,
|
|
||||||
name: '02 - David Byrne - My Fair Lady.mp3'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
length: 2759377,
|
|
||||||
name: '03 - Zap Mama - Wadidyusay.mp3'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
length: 5816537,
|
|
||||||
name: '04 - My Morning Jacket - One Big Holiday.mp3'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
length: 2106421,
|
|
||||||
name: '05 - Spoon - Revenge!.mp3'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
length: 3347550,
|
|
||||||
name: '06 - Gilberto Gil - Oslodum.mp3'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
length: 2107577,
|
|
||||||
name: '07 - Dan The Automator - Relaxation Spa Treatment.mp3'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
length: 3108130,
|
|
||||||
name: '08 - Thievery Corporation - Dc 3000.mp3'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
length: 3051528,
|
|
||||||
name: '09 - Le Tigre - Fake French.mp3'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
length: 3270259,
|
|
||||||
name: '10 - Paul Westerberg - Looking Up In Heaven.mp3'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
length: 3263528,
|
|
||||||
name: '11 - Chuck D - No Meaning No (feat. Fine Arts Militia).mp3'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
length: 6380952,
|
|
||||||
name: '12 - The Rapture - Sister Saviour (Blackstrobe Remix).mp3'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
length: 6550396,
|
|
||||||
name: '13 - Cornelius - Wataridori 2.mp3'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
length: 3034692,
|
|
||||||
name: '14 - DJ Danger Mouse - What U Sittin\' On (feat. Jemini, Cee Lo And Tha Alkaholiks).mp3'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
length: 3854611,
|
|
||||||
name: '15 - DJ Dolores - Oslodum 2004.mp3'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
length: 1762120,
|
|
||||||
name: '16 - Matmos - Action At A Distance.mp3'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
length: 4071,
|
|
||||||
name: 'README.md'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
length: 78163,
|
|
||||||
name: 'poster.jpg'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
prefs: {
|
prefs: {
|
||||||
downloadPath: config.IS_PORTABLE
|
downloadPath: config.DEFAULT_DOWNLOAD_PATH
|
||||||
? path.join(config.CONFIG_PATH, 'Downloads')
|
},
|
||||||
: remote.app.getPath('downloads')
|
torrents: config.DEFAULT_TORRENTS.map(createTorrentObject),
|
||||||
|
version: config.APP_VERSION /* make sure we can upgrade gracefully later */
|
||||||
|
}
|
||||||
|
|
||||||
|
var tasks = []
|
||||||
|
|
||||||
|
config.DEFAULT_TORRENTS.map(function (t, i) {
|
||||||
|
var infoHash = saved.torrents[i].infoHash
|
||||||
|
tasks.push(function (cb) {
|
||||||
|
fs.copy(
|
||||||
|
path.join(config.STATIC_PATH, t.posterFileName),
|
||||||
|
path.join(config.POSTER_PATH, infoHash + path.extname(t.posterFileName)),
|
||||||
|
cb
|
||||||
|
)
|
||||||
|
})
|
||||||
|
tasks.push(function (cb) {
|
||||||
|
fs.copy(
|
||||||
|
path.join(config.STATIC_PATH, t.torrentFileName),
|
||||||
|
path.join(config.TORRENT_PATH, infoHash + '.torrent'),
|
||||||
|
cb
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
parallel(tasks, function (err) {
|
||||||
|
if (err) return cb(err)
|
||||||
|
cb(null, saved)
|
||||||
|
})
|
||||||
|
|
||||||
|
function createTorrentObject (t) {
|
||||||
|
var torrent = fs.readFileSync(path.join(config.STATIC_PATH, t.torrentFileName))
|
||||||
|
var parsedTorrent = parseTorrent(torrent)
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: 'paused',
|
||||||
|
infoHash: parsedTorrent.infoHash,
|
||||||
|
name: t.name,
|
||||||
|
displayName: t.name,
|
||||||
|
posterFileName: parsedTorrent.infoHash + path.extname(t.posterFileName),
|
||||||
|
torrentFileName: parsedTorrent.infoHash + '.torrent',
|
||||||
|
magnetURI: parseTorrent.toMagnetURI(parsedTorrent),
|
||||||
|
files: parsedTorrent.files,
|
||||||
|
selections: parsedTorrent.files.map((x) => true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -284,3 +156,57 @@ function getPlayingFileSummary () {
|
|||||||
if (!torrentSummary) return null
|
if (!torrentSummary) return null
|
||||||
return torrentSummary.files[this.playing.fileIndex]
|
return torrentSummary.files[this.playing.fileIndex]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function load (cb) {
|
||||||
|
var state = getDefaultState()
|
||||||
|
|
||||||
|
appConfig.read(function (err, saved) {
|
||||||
|
if (err || !saved.version) {
|
||||||
|
console.log('Missing config file: Creating new one')
|
||||||
|
setupSavedState(onSaved)
|
||||||
|
} else {
|
||||||
|
onSaved(null, saved)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function onSaved (err, saved) {
|
||||||
|
if (err) return cb(err)
|
||||||
|
state.saved = saved
|
||||||
|
migrations.run(state)
|
||||||
|
cb(null, state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write state.saved to the JSON state file
|
||||||
|
function save (state, cb) {
|
||||||
|
console.log('Saving state to ' + appConfig.filePath)
|
||||||
|
|
||||||
|
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',
|
'.mp4',
|
||||||
'.mpg',
|
'.mpg',
|
||||||
'.ogv',
|
'.ogv',
|
||||||
'.webm'
|
'.webm',
|
||||||
|
'.wmv'
|
||||||
].includes(ext)
|
].includes(ext)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ function torrentPoster (torrent, cb) {
|
|||||||
if (videoFile) return torrentPosterFromVideo(videoFile, torrent, cb)
|
if (videoFile) return torrentPosterFromVideo(videoFile, torrent, cb)
|
||||||
|
|
||||||
// Third, try to use the largest image file
|
// Third, try to use the largest image file
|
||||||
var imgFile = getLargestFileByExtension(torrent, ['.gif', '.jpg', '.png'])
|
var imgFile = getLargestFileByExtension(torrent, ['.gif', '.jpg', '.jpeg', '.png'])
|
||||||
if (imgFile) return torrentPosterFromImage(imgFile, torrent, cb)
|
if (imgFile) return torrentPosterFromImage(imgFile, torrent, cb)
|
||||||
|
|
||||||
// TODO: generate a waveform from the largest sound file
|
// TODO: generate a waveform from the largest sound file
|
||||||
|
|||||||
@@ -10,14 +10,14 @@ var config = require('../../config')
|
|||||||
// Returns an absolute path to the torrent file, or null if unavailable
|
// Returns an absolute path to the torrent file, or null if unavailable
|
||||||
function getTorrentPath (torrentSummary) {
|
function getTorrentPath (torrentSummary) {
|
||||||
if (!torrentSummary || !torrentSummary.torrentFileName) return null
|
if (!torrentSummary || !torrentSummary.torrentFileName) return null
|
||||||
return path.join(config.CONFIG_TORRENT_PATH, torrentSummary.torrentFileName)
|
return path.join(config.TORRENT_PATH, torrentSummary.torrentFileName)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Expects a torrentSummary
|
// Expects a torrentSummary
|
||||||
// Returns an absolute path to the poster image, or null if unavailable
|
// Returns an absolute path to the poster image, or null if unavailable
|
||||||
function getPosterPath (torrentSummary) {
|
function getPosterPath (torrentSummary) {
|
||||||
if (!torrentSummary || !torrentSummary.posterFileName) return null
|
if (!torrentSummary || !torrentSummary.posterFileName) return null
|
||||||
var posterPath = path.join(config.CONFIG_POSTER_PATH, torrentSummary.posterFileName)
|
var posterPath = path.join(config.POSTER_PATH, torrentSummary.posterFileName)
|
||||||
// Work around a Chrome bug (reproduced in vanilla Chrome, not just Electron):
|
// Work around a Chrome bug (reproduced in vanilla Chrome, not just Electron):
|
||||||
// Backslashes in URLS in CSS cause bizarre string encoding issues
|
// Backslashes in URLS in CSS cause bizarre string encoding issues
|
||||||
return posterPath.replace(/\\/g, '/')
|
return posterPath.replace(/\\/g, '/')
|
||||||
|
|||||||
@@ -260,7 +260,7 @@ table {
|
|||||||
|
|
||||||
.modal .modal-content {
|
.modal .modal-content {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 45px;
|
top: 38px;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
@@ -848,7 +848,7 @@ body.drag .app::after {
|
|||||||
height: 14px;
|
height: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.player .controls .subtitles-list {
|
.player .controls .options-list {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
background: rgba(40, 40, 40, 0.8);
|
background: rgba(40, 40, 40, 0.8);
|
||||||
min-width: 100px;
|
min-width: 100px;
|
||||||
@@ -862,7 +862,7 @@ body.drag .app::after {
|
|||||||
color: rgba(255, 255, 255, 0.8);
|
color: rgba(255, 255, 255, 0.8);
|
||||||
}
|
}
|
||||||
|
|
||||||
.player .controls .subtitles-list .icon {
|
.player .controls .options-list .icon {
|
||||||
display: inline;
|
display: inline;
|
||||||
font-size: 17px;
|
font-size: 17px;
|
||||||
vertical-align: bottom;
|
vertical-align: bottom;
|
||||||
|
|||||||
182
renderer/main.js
182
renderer/main.js
@@ -3,7 +3,6 @@ console.time('init')
|
|||||||
var crashReporter = require('../crash-reporter')
|
var crashReporter = require('../crash-reporter')
|
||||||
crashReporter.init()
|
crashReporter.init()
|
||||||
|
|
||||||
var appConfig = require('application-config')('WebTorrent')
|
|
||||||
var dragDrop = require('drag-drop')
|
var dragDrop = require('drag-drop')
|
||||||
var electron = require('electron')
|
var electron = require('electron')
|
||||||
var fs = require('fs-extra')
|
var fs = require('fs-extra')
|
||||||
@@ -15,49 +14,34 @@ var createElement = require('virtual-dom/create-element')
|
|||||||
var diff = require('virtual-dom/diff')
|
var diff = require('virtual-dom/diff')
|
||||||
var patch = require('virtual-dom/patch')
|
var patch = require('virtual-dom/patch')
|
||||||
|
|
||||||
var App = require('./views/app')
|
|
||||||
var config = require('../config')
|
var config = require('../config')
|
||||||
|
var App = require('./views/app')
|
||||||
|
var telemetry = require('./lib/telemetry')
|
||||||
var errors = require('./lib/errors')
|
var errors = require('./lib/errors')
|
||||||
var migrations = require('./lib/migrations')
|
|
||||||
var sound = require('./lib/sound')
|
var sound = require('./lib/sound')
|
||||||
var State = require('./lib/state')
|
var State = require('./lib/state')
|
||||||
var TorrentPlayer = require('./lib/torrent-player')
|
var TorrentPlayer = require('./lib/torrent-player')
|
||||||
var TorrentSummary = require('./lib/torrent-summary')
|
var TorrentSummary = require('./lib/torrent-summary')
|
||||||
|
|
||||||
var {setDispatch} = require('./lib/dispatcher')
|
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
|
// 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,
|
// 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
|
// and this IPC channel receives from and sends messages to the main process
|
||||||
var ipcRenderer = electron.ipcRenderer
|
var ipcRenderer = electron.ipcRenderer
|
||||||
|
|
||||||
// All state lives in state.js. `state.saved` is read from and written to a file.
|
var state, vdomLoop
|
||||||
// All other state is ephemeral. First we load state.saved then initialize the app.
|
|
||||||
loadState(init)
|
|
||||||
|
|
||||||
function loadState (cb) {
|
// This dependency is the slowest-loading, so we lazy load it
|
||||||
appConfig.read(function (err, data) {
|
var Cast = null
|
||||||
if (err) console.error(err)
|
|
||||||
|
|
||||||
// populate defaults if they're not there
|
init()
|
||||||
state.saved = Object.assign({}, State.getDefaultSavedState(), data)
|
|
||||||
state.saved.torrents.forEach(function (torrentSummary) {
|
|
||||||
if (torrentSummary.displayName) torrentSummary.name = torrentSummary.displayName
|
|
||||||
})
|
|
||||||
|
|
||||||
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
|
* Connects to the torrent networks, sets up the UI and OS integrations like
|
||||||
* the dock icon and drag+drop.
|
* the dock icon and drag+drop.
|
||||||
*/
|
*/
|
||||||
function init () {
|
function onState (err, _state) {
|
||||||
// Clean up the freshly-loaded config file, which may be from an older version
|
if (err) return onError(err)
|
||||||
migrations.run(state)
|
state = _state
|
||||||
|
|
||||||
|
// Add first page to location history
|
||||||
|
state.location.go({ url: 'home' })
|
||||||
|
|
||||||
// Restart everything we were torrenting last time the app ran
|
// Restart everything we were torrenting last time the app ran
|
||||||
resumeTorrents()
|
resumeTorrents()
|
||||||
@@ -105,15 +92,22 @@ function init () {
|
|||||||
window.addEventListener('focus', onFocus)
|
window.addEventListener('focus', onFocus)
|
||||||
window.addEventListener('blur', onBlur)
|
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
|
// Done! Ideally we want to get here < 500ms after the user clicks the app
|
||||||
|
sound.play('STARTUP')
|
||||||
console.timeEnd('init')
|
console.timeEnd('init')
|
||||||
}
|
}
|
||||||
|
|
||||||
function delayedInit () {
|
function delayedInit () {
|
||||||
lazyLoadCast()
|
lazyLoadCast()
|
||||||
sound.preload()
|
sound.preload()
|
||||||
|
telemetry.init(state)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lazily loads Chromecast and Airplay support
|
// Lazily loads Chromecast and Airplay support
|
||||||
@@ -181,7 +175,6 @@ function dispatch (action, ...args) {
|
|||||||
}
|
}
|
||||||
if (action === 'openTorrentAddress') {
|
if (action === 'openTorrentAddress') {
|
||||||
state.modal = { id: 'open-torrent-address-modal' }
|
state.modal = { id: 'open-torrent-address-modal' }
|
||||||
update()
|
|
||||||
}
|
}
|
||||||
if (action === 'createTorrent') {
|
if (action === 'createTorrent') {
|
||||||
createTorrent(args[0] /* options */)
|
createTorrent(args[0] /* options */)
|
||||||
@@ -204,11 +197,14 @@ function dispatch (action, ...args) {
|
|||||||
if (action === 'openTorrentContextMenu') {
|
if (action === 'openTorrentContextMenu') {
|
||||||
openTorrentContextMenu(args[0] /* infoHash */)
|
openTorrentContextMenu(args[0] /* infoHash */)
|
||||||
}
|
}
|
||||||
if (action === 'openDevice') {
|
if (action === 'toggleCastMenu') {
|
||||||
lazyLoadCast().open(args[0] /* deviceType */)
|
lazyLoadCast().toggleMenu(args[0] /* deviceType */)
|
||||||
}
|
}
|
||||||
if (action === 'closeDevice') {
|
if (action === 'selectCastDevice') {
|
||||||
lazyLoadCast().close()
|
lazyLoadCast().selectDevice(args[0] /* index */)
|
||||||
|
}
|
||||||
|
if (action === 'stopCasting') {
|
||||||
|
lazyLoadCast().stop()
|
||||||
}
|
}
|
||||||
if (action === 'setDimensions') {
|
if (action === 'setDimensions') {
|
||||||
setDimensions(args[0] /* dimensions */)
|
setDimensions(args[0] /* dimensions */)
|
||||||
@@ -266,6 +262,7 @@ function dispatch (action, ...args) {
|
|||||||
}
|
}
|
||||||
if (action === 'mediaError') {
|
if (action === 'mediaError') {
|
||||||
if (state.location.url() === 'player') {
|
if (state.location.url() === 'player') {
|
||||||
|
state.playing.result = 'error'
|
||||||
state.playing.location = 'error'
|
state.playing.location = 'error'
|
||||||
ipcRenderer.send('checkForVLC')
|
ipcRenderer.send('checkForVLC')
|
||||||
ipcRenderer.once('checkForVLC', function (e, isInstalled) {
|
ipcRenderer.once('checkForVLC', function (e, isInstalled) {
|
||||||
@@ -277,6 +274,9 @@ function dispatch (action, ...args) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (action === 'mediaSuccess') {
|
||||||
|
state.playing.result = 'success'
|
||||||
|
}
|
||||||
if (action === 'mediaTimeUpdate') {
|
if (action === 'mediaTimeUpdate') {
|
||||||
state.playing.lastTimeUpdate = new Date().getTime()
|
state.playing.lastTimeUpdate = new Date().getTime()
|
||||||
state.playing.isStalled = false
|
state.playing.isStalled = false
|
||||||
@@ -328,11 +328,14 @@ function dispatch (action, ...args) {
|
|||||||
saveStateThrottled()
|
saveStateThrottled()
|
||||||
}
|
}
|
||||||
if (action === 'saveState') {
|
if (action === 'saveState') {
|
||||||
saveState()
|
State.save(state)
|
||||||
}
|
}
|
||||||
if (action === 'setTitle') {
|
if (action === 'setTitle') {
|
||||||
state.window.title = args[0] /* title */
|
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
|
// Update the virtual-dom, unless it's just a mouse move event
|
||||||
if (action !== 'mediaMouseMoved' || showOrHidePlayerControls()) {
|
if (action !== 'mediaMouseMoved' || showOrHidePlayerControls()) {
|
||||||
@@ -369,11 +372,18 @@ function pause () {
|
|||||||
|
|
||||||
function playPause () {
|
function playPause () {
|
||||||
if (state.location.url() !== 'player') return
|
if (state.location.url() !== 'player') return
|
||||||
|
|
||||||
if (state.playing.isPaused) {
|
if (state.playing.isPaused) {
|
||||||
play()
|
play()
|
||||||
} else {
|
} else {
|
||||||
pause()
|
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) {
|
function jumpToTime (time) {
|
||||||
@@ -484,6 +494,8 @@ function setupIpc () {
|
|||||||
ipcRenderer.on('wt-audio-metadata', (e, ...args) => torrentAudioMetadata(...args))
|
ipcRenderer.on('wt-audio-metadata', (e, ...args) => torrentAudioMetadata(...args))
|
||||||
ipcRenderer.on('wt-server-running', (e, ...args) => torrentServerRunning(...args))
|
ipcRenderer.on('wt-server-running', (e, ...args) => torrentServerRunning(...args))
|
||||||
|
|
||||||
|
ipcRenderer.on('wt-uncaught-error', (e, err) => telemetry.logUncaughtError('webtorrent', err))
|
||||||
|
|
||||||
ipcRenderer.send('ipcReady')
|
ipcRenderer.send('ipcReady')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -512,7 +524,8 @@ function updatePreferences (property, value) {
|
|||||||
// All unsaved prefs take effect atomically, and are saved to config.json
|
// All unsaved prefs take effect atomically, and are saved to config.json
|
||||||
function savePreferences () {
|
function savePreferences () {
|
||||||
state.saved.prefs = Object.assign(state.saved.prefs || {}, state.unsaved.prefs)
|
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
|
// Don't write state.saved to file more than once a second
|
||||||
@@ -520,43 +533,11 @@ function saveStateThrottled () {
|
|||||||
if (state.saveStateTimeout) return
|
if (state.saveStateTimeout) return
|
||||||
state.saveStateTimeout = setTimeout(function () {
|
state.saveStateTimeout = setTimeout(function () {
|
||||||
delete state.saveStateTimeout
|
delete state.saveStateTimeout
|
||||||
saveState()
|
State.save(state)
|
||||||
|
update()
|
||||||
}, 1000)
|
}, 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
|
// 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)
|
// via any method (drag-drop, drag to app icon, command line)
|
||||||
function onOpen (files) {
|
function onOpen (files) {
|
||||||
@@ -782,6 +763,7 @@ function findFilesRecursive (paths, cb) {
|
|||||||
findFilesRecursive([path], function (fileObjs) {
|
findFilesRecursive([path], function (fileObjs) {
|
||||||
ret = ret.concat(fileObjs)
|
ret = ret.concat(fileObjs)
|
||||||
if (++numComplete === paths.length) {
|
if (++numComplete === paths.length) {
|
||||||
|
ret.sort((a, b) => a.path < b.path ? -1 : a.path > b.path)
|
||||||
cb(ret)
|
cb(ret)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -1018,10 +1000,13 @@ function openPlayer (infoHash, index, cb) {
|
|||||||
|
|
||||||
// update UI to show pending playback
|
// update UI to show pending playback
|
||||||
if (torrentSummary.progress !== 1) sound.play('PLAY')
|
if (torrentSummary.progress !== 1) sound.play('PLAY')
|
||||||
|
// TODO: remove torrentSummary.playStatus
|
||||||
torrentSummary.playStatus = 'requested'
|
torrentSummary.playStatus = 'requested'
|
||||||
update()
|
update()
|
||||||
|
|
||||||
var timeout = setTimeout(function () {
|
var timeout = setTimeout(function () {
|
||||||
|
telemetry.logPlayAttempt('timeout')
|
||||||
|
// TODO: remove torrentSummary.playStatus
|
||||||
torrentSummary.playStatus = 'timeout' /* no seeders available? */
|
torrentSummary.playStatus = 'timeout' /* no seeders available? */
|
||||||
sound.play('ERROR')
|
sound.play('ERROR')
|
||||||
cb(new Error('Playback timed out. Try again.'))
|
cb(new Error('Playback timed out. Try again.'))
|
||||||
@@ -1087,25 +1072,41 @@ function openPlayerFromActiveTorrent (torrentSummary, index, timeout, cb) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function closePlayer (cb) {
|
function closePlayer (cb) {
|
||||||
|
console.log('closePlayer')
|
||||||
|
|
||||||
|
// Quit any external players, like Chromecast/Airplay/etc or VLC
|
||||||
if (isCasting()) {
|
if (isCasting()) {
|
||||||
Cast.close()
|
Cast.stop()
|
||||||
}
|
}
|
||||||
if (state.playing.location === 'vlc') {
|
if (state.playing.location === 'vlc') {
|
||||||
ipcRenderer.send('vlcQuit')
|
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
|
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.playing = State.getDefaultPlayState()
|
||||||
state.server = null
|
state.server = null
|
||||||
|
|
||||||
|
// Reset the window size and location back to where it was
|
||||||
if (state.window.isFullScreen) {
|
if (state.window.isFullScreen) {
|
||||||
dispatch('toggleFullScreen', false)
|
dispatch('toggleFullScreen', false)
|
||||||
}
|
}
|
||||||
restoreBounds()
|
restoreBounds()
|
||||||
|
|
||||||
|
// Tell the WebTorrent process to kill the torrent-to-HTTP server
|
||||||
ipcRenderer.send('wt-stop-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('unblockPowerSave')
|
||||||
ipcRenderer.send('onPlayerClose')
|
ipcRenderer.send('onPlayerClose')
|
||||||
|
|
||||||
@@ -1136,9 +1137,14 @@ function toggleTorrent (infoHash) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO: use torrentKey, not infoHash
|
// TODO: use torrentKey, not infoHash
|
||||||
function deleteTorrent (infoHash) {
|
function deleteTorrent (infoHash, deleteData) {
|
||||||
ipcRenderer.send('wt-stop-torrenting', infoHash)
|
ipcRenderer.send('wt-stop-torrenting', infoHash)
|
||||||
|
|
||||||
|
if (deleteData) {
|
||||||
|
var torrentSummary = getTorrentSummary(infoHash)
|
||||||
|
moveItemToTrash(torrentSummary)
|
||||||
|
}
|
||||||
|
|
||||||
var index = state.saved.torrents.findIndex((x) => x.infoHash === infoHash)
|
var index = state.saved.torrents.findIndex((x) => x.infoHash === infoHash)
|
||||||
if (index > -1) state.saved.torrents.splice(index, 1)
|
if (index > -1) state.saved.torrents.splice(index, 1)
|
||||||
saveStateThrottled()
|
saveStateThrottled()
|
||||||
@@ -1164,6 +1170,20 @@ function openTorrentContextMenu (infoHash) {
|
|||||||
var torrentSummary = getTorrentSummary(infoHash)
|
var torrentSummary = getTorrentSummary(infoHash)
|
||||||
var menu = new electron.remote.Menu()
|
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) {
|
if (torrentSummary.files) {
|
||||||
menu.append(new electron.remote.MenuItem({
|
menu.append(new electron.remote.MenuItem({
|
||||||
label: process.platform === 'darwin' ? 'Show in Finder' : 'Show in Folder',
|
label: process.platform === 'darwin' ? 'Show in Finder' : 'Show in Folder',
|
||||||
@@ -1204,6 +1224,10 @@ function showItemInFolder (torrentSummary) {
|
|||||||
ipcRenderer.send('showItemInFolder', getTorrentPath(torrentSummary))
|
ipcRenderer.send('showItemInFolder', getTorrentPath(torrentSummary))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function moveItemToTrash (torrentSummary) {
|
||||||
|
ipcRenderer.send('moveItemToTrash', getTorrentPath(torrentSummary))
|
||||||
|
}
|
||||||
|
|
||||||
function saveTorrentFileAs (torrentSummary) {
|
function saveTorrentFileAs (torrentSummary) {
|
||||||
var newFileName = `${path.parse(torrentSummary.name).name}.torrent`
|
var newFileName = `${path.parse(torrentSummary.name).name}.torrent`
|
||||||
var opts = {
|
var opts = {
|
||||||
@@ -1341,3 +1365,7 @@ function onBlur () {
|
|||||||
state.window.isFocused = false
|
state.window.isFocused = false
|
||||||
update()
|
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
|
// Show a single torrentSummary file in the details view for a single torrent
|
||||||
function renderFileRow (torrentSummary, file, index) {
|
function renderFileRow (torrentSummary, file, index) {
|
||||||
// First, find out how much of the file we've downloaded
|
// First, find out how much of the file we've downloaded
|
||||||
var isSelected = torrentSummary.selections[index] // Are we even torrenting it?
|
// Are we even torrenting it?
|
||||||
|
var isSelected = torrentSummary.selections && torrentSummary.selections[index]
|
||||||
var isDone = false // Are we finished torrenting it?
|
var isDone = false // Are we finished torrenting it?
|
||||||
var progress = ''
|
var progress = ''
|
||||||
if (torrentSummary.progress && torrentSummary.progress.files) {
|
if (torrentSummary.progress && torrentSummary.progress.files) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
module.exports = OpenTorrentAddressModal
|
module.exports = OpenTorrentAddressModal
|
||||||
|
|
||||||
var {dispatch} = require('../lib/dispatcher')
|
var {dispatch, dispatcher} = require('../lib/dispatcher')
|
||||||
var hx = require('../lib/hx')
|
var hx = require('../lib/hx')
|
||||||
|
|
||||||
function OpenTorrentAddressModal (state) {
|
function OpenTorrentAddressModal (state) {
|
||||||
@@ -11,8 +11,8 @@ function OpenTorrentAddressModal (state) {
|
|||||||
<input id='add-torrent-url' type='text' onkeypress=${handleKeyPress} />
|
<input id='add-torrent-url' type='text' onkeypress=${handleKeyPress} />
|
||||||
</p>
|
</p>
|
||||||
<p class='float-right'>
|
<p class='float-right'>
|
||||||
<button class='button button-flat' onclick=${handleCancel}>CANCEL</button>
|
<button class='button button-flat' onclick=${dispatcher('exitModal')}>Cancel</button>
|
||||||
<button class='button button-raised' onclick=${handleOK}>OK</button>
|
<button class='button button-raised' onclick=${handleOK}>OK</button>
|
||||||
</p>
|
</p>
|
||||||
<script>document.querySelector('#add-torrent-url').focus()</script>
|
<script>document.querySelector('#add-torrent-url').focus()</script>
|
||||||
</div>
|
</div>
|
||||||
@@ -27,7 +27,3 @@ function handleOK () {
|
|||||||
dispatch('exitModal')
|
dispatch('exitModal')
|
||||||
dispatch('addTorrent', document.querySelector('#add-torrent-url').value)
|
dispatch('addTorrent', document.querySelector('#add-torrent-url').value)
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleCancel () {
|
|
||||||
dispatch('exitModal')
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -143,6 +143,7 @@ function renderMedia (state) {
|
|||||||
} else if (elem.webkitAudioDecodedByteCount === 0) {
|
} else if (elem.webkitAudioDecodedByteCount === 0) {
|
||||||
dispatch('mediaError', 'Audio codec unsupported')
|
dispatch('mediaError', 'Audio codec unsupported')
|
||||||
} else {
|
} else {
|
||||||
|
dispatch('mediaSuccess')
|
||||||
elem.play()
|
elem.play()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -279,8 +280,10 @@ function renderCastScreen (state) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var isStarting = state.playing.location.endsWith('-pending')
|
var isStarting = state.playing.location.endsWith('-pending')
|
||||||
|
var castName = state.playing.castName
|
||||||
var castStatus
|
var castStatus
|
||||||
if (isCast) castStatus = isStarting ? 'Connecting...' : 'Connected'
|
if (isCast && isStarting) castStatus = 'Connecting to ' + castName + '...'
|
||||||
|
else if (isCast && !isStarting) castStatus = 'Connected to ' + castName
|
||||||
else castStatus = ''
|
else castStatus = ''
|
||||||
|
|
||||||
// Show a nice title image, if possible
|
// Show a nice title image, if possible
|
||||||
@@ -299,6 +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) {
|
function renderSubtitlesOptions (state) {
|
||||||
var subtitles = state.playing.subtitles
|
var subtitles = state.playing.subtitles
|
||||||
if (!subtitles.tracks.length || !subtitles.showMenu) return
|
if (!subtitles.tracks.length || !subtitles.showMenu) return
|
||||||
@@ -316,7 +343,7 @@ function renderSubtitlesOptions (state) {
|
|||||||
var noneSelected = state.playing.subtitles.selectedIndex === -1
|
var noneSelected = state.playing.subtitles.selectedIndex === -1
|
||||||
var noneClass = 'radio_button_' + (noneSelected ? 'checked' : 'unchecked')
|
var noneClass = 'radio_button_' + (noneSelected ? 'checked' : 'unchecked')
|
||||||
return hx`
|
return hx`
|
||||||
<ul.subtitles-list>
|
<ul.options-list>
|
||||||
${items}
|
${items}
|
||||||
<li onclick=${dispatcher('selectSubtitle', -1)}>
|
<li onclick=${dispatcher('selectSubtitle', -1)}>
|
||||||
<i.icon>${noneClass}</i>
|
<i.icon>${noneClass}</i>
|
||||||
@@ -378,72 +405,49 @@ function renderPlayerControls (state) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If we've detected a Chromecast or AppleTV, the user can play video there
|
// If we've detected a Chromecast or AppleTV, the user can play video there
|
||||||
var isOnChromecast = state.playing.location.startsWith('chromecast')
|
var castTypes = ['chromecast', 'airplay', 'dlna']
|
||||||
var isOnAirplay = state.playing.location.startsWith('airplay')
|
var isCastingAnywhere = castTypes.some(
|
||||||
var isOnDlna = state.playing.location.startsWith('dlna')
|
(castType) => state.playing.location.startsWith(castType))
|
||||||
var chromecastClass, chromecastHandler
|
|
||||||
var airplayClass, airplayHandler
|
|
||||||
var dlnaClass, dlnaHandler
|
|
||||||
if (isOnChromecast) {
|
|
||||||
chromecastClass = 'active'
|
|
||||||
dlnaClass = 'disabled'
|
|
||||||
airplayClass = 'disabled'
|
|
||||||
chromecastHandler = dispatcher('closeDevice')
|
|
||||||
airplayHandler = undefined
|
|
||||||
dlnaHandler = undefined
|
|
||||||
} else if (isOnAirplay) {
|
|
||||||
chromecastClass = 'disabled'
|
|
||||||
dlnaClass = 'disabled'
|
|
||||||
airplayClass = 'active'
|
|
||||||
chromecastHandler = undefined
|
|
||||||
airplayHandler = dispatcher('closeDevice')
|
|
||||||
dlnaHandler = undefined
|
|
||||||
} else if (isOnDlna) {
|
|
||||||
chromecastClass = 'disabled'
|
|
||||||
dlnaClass = 'active'
|
|
||||||
airplayClass = 'disabled'
|
|
||||||
chromecastHandler = undefined
|
|
||||||
airplayHandler = undefined
|
|
||||||
dlnaHandler = dispatcher('closeDevice')
|
|
||||||
} else {
|
|
||||||
chromecastClass = ''
|
|
||||||
airplayClass = ''
|
|
||||||
dlnaClass = ''
|
|
||||||
chromecastHandler = dispatcher('openDevice', 'chromecast')
|
|
||||||
airplayHandler = dispatcher('openDevice', 'airplay')
|
|
||||||
dlnaHandler = dispatcher('openDevice', 'dlna')
|
|
||||||
}
|
|
||||||
if (state.devices.chromecast || isOnChromecast) {
|
|
||||||
var castIcon = isOnChromecast ? 'cast_connected' : 'cast'
|
|
||||||
elements.push(hx`
|
|
||||||
<i.icon.device.float-right
|
|
||||||
class=${chromecastClass}
|
|
||||||
onclick=${chromecastHandler}>
|
|
||||||
${castIcon}
|
|
||||||
</i>
|
|
||||||
`)
|
|
||||||
}
|
|
||||||
if (state.devices.airplay || isOnAirplay) {
|
|
||||||
elements.push(hx`
|
|
||||||
<i.icon.device.float-right
|
|
||||||
class=${airplayClass}
|
|
||||||
onclick=${airplayHandler}>
|
|
||||||
airplay
|
|
||||||
</i>
|
|
||||||
`)
|
|
||||||
}
|
|
||||||
if (state.devices.dlna || isOnDlna) {
|
|
||||||
elements.push(hx`
|
|
||||||
<i
|
|
||||||
class='icon device float-right'
|
|
||||||
class=${dlnaClass}
|
|
||||||
onclick=${dlnaHandler}>
|
|
||||||
tv
|
|
||||||
</i>
|
|
||||||
`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// render volume
|
// Add the cast buttons. Icons for each cast type, connected/disconnected:
|
||||||
|
var buttonIcons = {
|
||||||
|
'chromecast': {true: 'cast_connected', false: 'cast'},
|
||||||
|
'airplay': {true: 'airplay', false: 'airplay'},
|
||||||
|
'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 volume = state.playing.volume
|
||||||
var volumeIcon = 'volume_' + (
|
var volumeIcon = 'volume_' + (
|
||||||
volume === 0 ? 'off'
|
volume === 0 ? 'off'
|
||||||
@@ -496,6 +500,7 @@ function renderPlayerControls (state) {
|
|||||||
return hx`
|
return hx`
|
||||||
<div class='controls'>
|
<div class='controls'>
|
||||||
${elements}
|
${elements}
|
||||||
|
${renderCastOptions(state)}
|
||||||
${renderSubtitlesOptions(state)}
|
${renderSubtitlesOptions(state)}
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
|
|||||||
@@ -10,9 +10,9 @@ function UpdateAvailableModal (state) {
|
|||||||
<div class='update-available-modal'>
|
<div class='update-available-modal'>
|
||||||
<p><strong>A new version of WebTorrent is available: v${state.modal.version}</strong></p>
|
<p><strong>A new version of WebTorrent is available: v${state.modal.version}</strong></p>
|
||||||
<p>We have an auto-updater for Windows and Mac. We don't have one for Linux yet, so you'll have to download the new version manually.</p>
|
<p>We have an auto-updater for Windows and Mac. We don't have one for Linux yet, so you'll have to download the new version manually.</p>
|
||||||
<p>
|
<p class='float-right'>
|
||||||
<button class='primary' onclick=${handleOK}>Show Download Page</button>
|
<button class='button button-flat' onclick=${handleCancel}>Skip This Release</button>
|
||||||
<button class='cancel' onclick=${handleCancel}>Skip This Release</button>
|
<button class='button button-raised' onclick=${handleOK}>Show Download Page</button>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
|
|||||||
@@ -63,6 +63,10 @@ function init () {
|
|||||||
|
|
||||||
ipc.send('ipcReadyWebTorrent')
|
ipc.send('ipcReadyWebTorrent')
|
||||||
|
|
||||||
|
window.addEventListener('error', (e) =>
|
||||||
|
ipc.send('wt-uncaught-error', {message: e.error.message, stack: e.error.stack}),
|
||||||
|
true)
|
||||||
|
|
||||||
setInterval(updateTorrentProgress, 1000)
|
setInterval(updateTorrentProgress, 1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,7 +177,7 @@ function saveTorrentFile (torrentKey) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise, save the .torrent file, under the app config folder
|
// Otherwise, save the .torrent file, under the app config folder
|
||||||
fs.mkdir(config.CONFIG_TORRENT_PATH, function (_) {
|
fs.mkdir(config.TORRENT_PATH, function (_) {
|
||||||
fs.writeFile(torrentPath, torrent.torrentFile, function (err) {
|
fs.writeFile(torrentPath, torrent.torrentFile, function (err) {
|
||||||
if (err) return console.log('error saving torrent file %s: %o', torrentPath, err)
|
if (err) return console.log('error saving torrent file %s: %o', torrentPath, err)
|
||||||
console.log('saved torrent file %s', torrentPath)
|
console.log('saved torrent file %s', torrentPath)
|
||||||
@@ -186,7 +190,7 @@ function saveTorrentFile (torrentKey) {
|
|||||||
// Checks whether we've already resolved a given infohash to a torrent file
|
// Checks whether we've already resolved a given infohash to a torrent file
|
||||||
// Calls back with (torrentPath, exists). Logs, does not call back on error
|
// Calls back with (torrentPath, exists). Logs, does not call back on error
|
||||||
function checkIfTorrentFileExists (infoHash, cb) {
|
function checkIfTorrentFileExists (infoHash, cb) {
|
||||||
var torrentPath = path.join(config.CONFIG_TORRENT_PATH, infoHash + '.torrent')
|
var torrentPath = path.join(config.TORRENT_PATH, infoHash + '.torrent')
|
||||||
fs.exists(torrentPath, function (exists) {
|
fs.exists(torrentPath, function (exists) {
|
||||||
cb(torrentPath, exists)
|
cb(torrentPath, exists)
|
||||||
})
|
})
|
||||||
@@ -199,10 +203,10 @@ function generateTorrentPoster (torrentKey) {
|
|||||||
torrentPoster(torrent, function (err, buf, extension) {
|
torrentPoster(torrent, function (err, buf, extension) {
|
||||||
if (err) return console.log('error generating poster: %o', err)
|
if (err) return console.log('error generating poster: %o', err)
|
||||||
// save it for next time
|
// save it for next time
|
||||||
fs.mkdirp(config.CONFIG_POSTER_PATH, function (err) {
|
fs.mkdirp(config.POSTER_PATH, function (err) {
|
||||||
if (err) return console.log('error creating poster dir: %o', err)
|
if (err) return console.log('error creating poster dir: %o', err)
|
||||||
var posterFileName = torrent.infoHash + extension
|
var posterFileName = torrent.infoHash + extension
|
||||||
var posterFilePath = path.join(config.CONFIG_POSTER_PATH, posterFileName)
|
var posterFilePath = path.join(config.POSTER_PATH, posterFileName)
|
||||||
fs.writeFile(posterFilePath, buf, function (err) {
|
fs.writeFile(posterFilePath, buf, function (err) {
|
||||||
if (err) return console.log('error saving poster: %o', err)
|
if (err) return console.log('error saving poster: %o', err)
|
||||||
// show the poster
|
// show the poster
|
||||||
|
|||||||
BIN
static/PauseThumbnailBarButton.png
Normal file
BIN
static/PauseThumbnailBarButton.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 101 B |
BIN
static/PlayThumbnailBarButton.png
Normal file
BIN
static/PlayThumbnailBarButton.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 208 B |
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 76 KiB |
Reference in New Issue
Block a user