WebTorrent process
* Separate hidden window, with its own renderer process, for WebTorrent (Must be a window. You cannot run WebRTC at all in a Web Worker, and you can't run it well in a node process like the electron main process.) * Disabled the create-torrent-modal for now. That gives us a consistent UX regardless of whether the user dragged files or folders onto the app or opened the Create New Torrent menu item. * Main process routes all messages between the main and webtorrent windows. * The renderer index.js is smaller now (but still too big), with the WebTorrent interface moved to webtorrent.js / it's own process. * The UI should be faster now, and should not lag under load.
This commit is contained in:
@@ -65,6 +65,7 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
WINDOW_ABOUT: 'file://' + path.join(__dirname, 'renderer', 'about.html'),
|
WINDOW_ABOUT: 'file://' + path.join(__dirname, 'renderer', 'about.html'),
|
||||||
|
WINDOW_WEBTORRENT: 'file://' + path.join(__dirname, 'renderer', 'webtorrent.html'),
|
||||||
WINDOW_MAIN: 'file://' + path.join(__dirname, 'renderer', 'main.html')
|
WINDOW_MAIN: 'file://' + path.join(__dirname, 'renderer', 'main.html')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ function init () {
|
|||||||
app.on('ready', function () {
|
app.on('ready', function () {
|
||||||
menu.init()
|
menu.init()
|
||||||
windows.createMainWindow()
|
windows.createMainWindow()
|
||||||
|
windows.createWebTorrentHiddenWindow()
|
||||||
shortcuts.init()
|
shortcuts.init()
|
||||||
tray.init()
|
tray.init()
|
||||||
handlers.install()
|
handlers.install()
|
||||||
|
|||||||
22
main/ipc.js
22
main/ipc.js
@@ -69,6 +69,28 @@ function init () {
|
|||||||
menu.onPlayerClose()
|
menu.onPlayerClose()
|
||||||
shortcuts.unregisterPlayerShortcuts()
|
shortcuts.unregisterPlayerShortcuts()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Capture all events
|
||||||
|
var oldEmit = ipcMain.emit
|
||||||
|
ipcMain.emit = function (name, e, ...args) {
|
||||||
|
// Relay messages between the main window and the WebTorrent hidden window
|
||||||
|
if (name.startsWith('wt-')) {
|
||||||
|
var recipient, recipientStr
|
||||||
|
if (e.sender.browserWindowOptions.title === 'webtorrent-hidden-window') {
|
||||||
|
recipient = windows.main
|
||||||
|
recipientStr = 'main'
|
||||||
|
} else {
|
||||||
|
recipient = windows.webtorrent
|
||||||
|
recipientStr = 'webtorrent'
|
||||||
|
}
|
||||||
|
console.log('sending %s to %s', name, recipientStr)
|
||||||
|
recipient.send(name, ...args)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Emit all other events normally
|
||||||
|
oldEmit.call(ipcMain, name, e, ...args)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function setBounds (bounds, maximize) {
|
function setBounds (bounds, maximize) {
|
||||||
|
|||||||
@@ -124,7 +124,10 @@ function showCreateTorrent () {
|
|||||||
properties: [ 'openFile', 'openDirectory' ]
|
properties: [ 'openFile', 'openDirectory' ]
|
||||||
}, function (filenames) {
|
}, function (filenames) {
|
||||||
if (!Array.isArray(filenames)) return
|
if (!Array.isArray(filenames)) return
|
||||||
windows.main.send('dispatch', 'seed', filenames[0])
|
var options = {
|
||||||
|
files: filenames[0]
|
||||||
|
}
|
||||||
|
windows.main.send('dispatch', 'createTorrent', options)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
var windows = module.exports = {
|
var windows = module.exports = {
|
||||||
about: null,
|
about: null,
|
||||||
main: null,
|
main: null,
|
||||||
createAboutWindow: createAboutWindow,
|
createAboutWindow,
|
||||||
createMainWindow: createMainWindow,
|
createWebTorrentHiddenWindow,
|
||||||
focusWindow: focusWindow
|
createMainWindow,
|
||||||
|
focusWindow
|
||||||
}
|
}
|
||||||
|
|
||||||
var electron = require('electron')
|
var electron = require('electron')
|
||||||
@@ -46,6 +47,25 @@ function createAboutWindow () {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createWebTorrentHiddenWindow () {
|
||||||
|
var win = windows.webtorrent = new electron.BrowserWindow({
|
||||||
|
backgroundColor: '#ECECEC',
|
||||||
|
show: false,
|
||||||
|
center: true,
|
||||||
|
title: 'webtorrent-hidden-window',
|
||||||
|
width: 500,
|
||||||
|
height: 500,
|
||||||
|
minimizable: false,
|
||||||
|
maximizable: false,
|
||||||
|
fullscreen: false,
|
||||||
|
skipTaskbar: true
|
||||||
|
})
|
||||||
|
win.loadURL(config.WINDOW_WEBTORRENT)
|
||||||
|
|
||||||
|
// To debug the WebTorrent process, set `show` to true above and uncomment:
|
||||||
|
// win.webContents.openDevTools({detached: false})
|
||||||
|
}
|
||||||
|
|
||||||
function createMainWindow () {
|
function createMainWindow () {
|
||||||
if (windows.main) {
|
if (windows.main) {
|
||||||
return focusWindow(windows.main)
|
return focusWindow(windows.main)
|
||||||
@@ -56,7 +76,7 @@ function createMainWindow () {
|
|||||||
icon: config.APP_ICON + '.png',
|
icon: config.APP_ICON + '.png',
|
||||||
minWidth: 375,
|
minWidth: 375,
|
||||||
minHeight: 38 + (120 * 2), // header height + 2 torrents
|
minHeight: 38 + (120 * 2), // header height + 2 torrents
|
||||||
show: false, // Hide window until DOM finishes loading
|
show: true, // Hide window until DOM finishes loading
|
||||||
title: config.APP_WINDOW_TITLE,
|
title: config.APP_WINDOW_TITLE,
|
||||||
titleBarStyle: 'hidden-inset', // Hide OS chrome, except traffic light buttons (OS X)
|
titleBarStyle: 'hidden-inset', // Hide OS chrome, except traffic light buttons (OS X)
|
||||||
useContentSize: true, // Specify web page size without OS chrome
|
useContentSize: true, // Specify web page size without OS chrome
|
||||||
|
|||||||
@@ -17,8 +17,10 @@
|
|||||||
"airplay-js": "guerrerocarlos/node-airplay-js",
|
"airplay-js": "guerrerocarlos/node-airplay-js",
|
||||||
"application-config": "^0.2.0",
|
"application-config": "^0.2.0",
|
||||||
"application-config-path": "^0.1.0",
|
"application-config-path": "^0.1.0",
|
||||||
|
"bitfield": "^1.0.2",
|
||||||
"chromecasts": "^1.8.0",
|
"chromecasts": "^1.8.0",
|
||||||
"create-torrent": "^3.22.1",
|
"create-torrent": "^3.22.1",
|
||||||
|
"deep-equal": "^1.0.1",
|
||||||
"dlnacasts": "^0.0.2",
|
"dlnacasts": "^0.0.2",
|
||||||
"drag-drop": "^2.11.0",
|
"drag-drop": "^2.11.0",
|
||||||
"electron-localshortcut": "^0.6.0",
|
"electron-localshortcut": "^0.6.0",
|
||||||
|
|||||||
@@ -1,15 +1,11 @@
|
|||||||
console.time('init')
|
console.time('init')
|
||||||
|
|
||||||
var cfg = require('application-config')('WebTorrent')
|
var cfg = require('application-config')('WebTorrent')
|
||||||
var defaultAnnounceList = require('create-torrent').announceList
|
|
||||||
var dragDrop = require('drag-drop')
|
var dragDrop = require('drag-drop')
|
||||||
var electron = require('electron')
|
var electron = require('electron')
|
||||||
var EventEmitter = require('events')
|
var EventEmitter = require('events')
|
||||||
var fs = require('fs')
|
var fs = require('fs')
|
||||||
var mainLoop = require('main-loop')
|
var mainLoop = require('main-loop')
|
||||||
var mkdirp = require('mkdirp')
|
|
||||||
var musicmetadata = require('musicmetadata')
|
|
||||||
var networkAddress = require('network-address')
|
|
||||||
var path = require('path')
|
var path = require('path')
|
||||||
var remote = require('remote')
|
var remote = require('remote')
|
||||||
|
|
||||||
@@ -21,14 +17,11 @@ var App = require('./views/app')
|
|||||||
var errors = require('./lib/errors')
|
var errors = require('./lib/errors')
|
||||||
var config = require('../config')
|
var config = require('../config')
|
||||||
var TorrentPlayer = require('./lib/torrent-player')
|
var TorrentPlayer = require('./lib/torrent-player')
|
||||||
var torrentPoster = require('./lib/torrent-poster')
|
|
||||||
var util = require('./util')
|
var util = require('./util')
|
||||||
var {setDispatch} = require('./lib/dispatcher')
|
var {setDispatch} = require('./lib/dispatcher')
|
||||||
setDispatch(dispatch)
|
setDispatch(dispatch)
|
||||||
|
|
||||||
// These two dependencies are the slowest-loading, so we lazy load them
|
// This dependency is the slowest-loading, so we lazy load it
|
||||||
// This cuts time from icon click to rendered window from ~550ms to ~150ms on my laptop
|
|
||||||
var WebTorrent = null
|
|
||||||
var Cast = null
|
var Cast = null
|
||||||
|
|
||||||
// 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
|
||||||
@@ -43,11 +36,6 @@ var dialog = remote.require('dialog')
|
|||||||
// For easy debugging in Developer Tools
|
// For easy debugging in Developer Tools
|
||||||
var state = global.state = require('./state')
|
var state = global.state = require('./state')
|
||||||
|
|
||||||
// Force use of webtorrent trackers on all torrents
|
|
||||||
global.WEBTORRENT_ANNOUNCE = defaultAnnounceList
|
|
||||||
.map((arr) => arr[0])
|
|
||||||
.filter((url) => url.indexOf('wss://') === 0 || url.indexOf('ws://') === 0)
|
|
||||||
|
|
||||||
var vdomLoop
|
var vdomLoop
|
||||||
|
|
||||||
// All state lives in state.js. `state.saved` is read from and written to a file.
|
// All state lives in state.js. `state.saved` is read from and written to a file.
|
||||||
@@ -66,10 +54,8 @@ function init () {
|
|||||||
state.location.go({ url: 'home' })
|
state.location.go({ url: 'home' })
|
||||||
|
|
||||||
// Lazily load the WebTorrent, Chromecast, and Airplay modules
|
// Lazily load the WebTorrent, Chromecast, and Airplay modules
|
||||||
window.setTimeout(function () {
|
initWebtorrent()
|
||||||
lazyLoadClient()
|
window.setTimeout(lazyLoadCast, 750)
|
||||||
lazyLoadCast()
|
|
||||||
}, 750)
|
|
||||||
|
|
||||||
// The UI is built with virtual-dom, a minimalist library extracted from React
|
// The UI is built with virtual-dom, a minimalist library extracted from React
|
||||||
// The concepts--one way data flow, a pure function that renders state to a
|
// The concepts--one way data flow, a pure function that renders state to a
|
||||||
@@ -125,12 +111,6 @@ function init () {
|
|||||||
console.timeEnd('init')
|
console.timeEnd('init')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lazily loads the WebTorrent module and creates the WebTorrent client
|
|
||||||
function lazyLoadClient () {
|
|
||||||
if (!WebTorrent) initWebtorrent()
|
|
||||||
return state.client
|
|
||||||
}
|
|
||||||
|
|
||||||
// Lazily loads Chromecast and Airplay support
|
// Lazily loads Chromecast and Airplay support
|
||||||
function lazyLoadCast () {
|
function lazyLoadCast () {
|
||||||
if (!Cast) {
|
if (!Cast) {
|
||||||
@@ -140,41 +120,25 @@ function lazyLoadCast () {
|
|||||||
return Cast
|
return Cast
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load the WebTorrent module, connect to both the WebTorrent and BitTorrent
|
// Talk to WebTorrent process, resume torrents, start monitoring torrent progress
|
||||||
// networks, resume torrents, start monitoring torrent progress
|
|
||||||
function initWebtorrent () {
|
function initWebtorrent () {
|
||||||
WebTorrent = require('webtorrent')
|
|
||||||
|
|
||||||
// Connect to the WebTorrent and BitTorrent networks. WebTorrent Desktop is a hybrid
|
|
||||||
// client, as explained here: https://webtorrent.io/faq
|
|
||||||
state.client = new WebTorrent()
|
|
||||||
state.client.on('warning', onWarning)
|
|
||||||
state.client.on('error', function (err) {
|
|
||||||
// TODO: WebTorrent should have semantic errors
|
|
||||||
if (err.message.startsWith('There is already a swarm')) {
|
|
||||||
onError(new Error('Couldn\'t add duplicate torrent'))
|
|
||||||
} else {
|
|
||||||
onError(err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Restart everything we were torrenting last time the app ran
|
// Restart everything we were torrenting last time the app ran
|
||||||
resumeTorrents()
|
resumeTorrents()
|
||||||
|
|
||||||
// Calling update() updates the UI given the current state
|
// Calling update() updates the UI given the current state
|
||||||
// Do this at least once a second to give every file in every torrentSummary
|
// Do this at least once a second to give every file in every torrentSummary
|
||||||
// a progress bar and to keep the cursor in sync when playing a video
|
// a progress bar and to keep the cursor in sync when playing a video
|
||||||
setInterval(function () {
|
setInterval(update, 1000)
|
||||||
if (!updateTorrentProgress()) {
|
|
||||||
update() // If we didn't just update(), do so now, for the video cursor
|
|
||||||
}
|
|
||||||
}, 1000)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// This is the (mostly) pure function from state -> UI. Returns a virtual DOM
|
// This is the (mostly) pure function from state -> UI. Returns a virtual DOM
|
||||||
// tree. Any events, such as button clicks, will turn into calls to dispatch()
|
// tree. Any events, such as button clicks, will turn into calls to dispatch()
|
||||||
function render (state) {
|
function render (state) {
|
||||||
return App(state)
|
try {
|
||||||
|
return App(state)
|
||||||
|
} catch (e) {
|
||||||
|
console.log('rendering error: %s\n\t%s', e.message, e.stack)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calls render() to go from state -> UI, then applies to vdom to the real DOM.
|
// Calls render() to go from state -> UI, then applies to vdom to the real DOM.
|
||||||
@@ -217,16 +181,8 @@ function dispatch (action, ...args) {
|
|||||||
if (action === 'showOpenTorrentFile') {
|
if (action === 'showOpenTorrentFile') {
|
||||||
ipcRenderer.send('showOpenTorrentFile') /* open torrent file */
|
ipcRenderer.send('showOpenTorrentFile') /* open torrent file */
|
||||||
}
|
}
|
||||||
if (action === 'seed') {
|
if (action === 'createTorrent') {
|
||||||
// TODO: right now, creating a torrent thru File > Create New Torrent
|
createTorrent(args[0] /* options */)
|
||||||
// creates and starts seeding a new torrent directly, while dragging files
|
|
||||||
// or folders onto the app opens the create-torrent-modal
|
|
||||||
//
|
|
||||||
// That's because the former gets a single string and the latter gets a list
|
|
||||||
// of W3C File objects. We should fix this inconsitency, ideally without
|
|
||||||
// duping this code in the drag-drop module:
|
|
||||||
// https://github.com/feross/drag-drop/blob/master/index.js
|
|
||||||
createTorrent({files: args[0]} /* file or folder path */)
|
|
||||||
}
|
}
|
||||||
if (action === 'openFile') {
|
if (action === 'openFile') {
|
||||||
openFile(args[0] /* infoHash */, args[1] /* index */)
|
openFile(args[0] /* infoHash */, args[1] /* index */)
|
||||||
@@ -321,14 +277,11 @@ function dispatch (action, ...args) {
|
|||||||
if (action === 'skipVersion') {
|
if (action === 'skipVersion') {
|
||||||
if (!state.saved.skippedVersions) state.saved.skippedVersions = []
|
if (!state.saved.skippedVersions) state.saved.skippedVersions = []
|
||||||
state.saved.skippedVersions.push(args[0] /* version */)
|
state.saved.skippedVersions.push(args[0] /* version */)
|
||||||
saveState()
|
saveStateThrottled()
|
||||||
}
|
}
|
||||||
if (action === 'saveState') {
|
if (action === 'saveState') {
|
||||||
saveState()
|
saveState()
|
||||||
}
|
}
|
||||||
if (action === 'createTorrent') {
|
|
||||||
createTorrent(args[0] /* options */)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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') {
|
if (action !== 'mediaMouseMoved') {
|
||||||
@@ -404,6 +357,19 @@ function setupIpc () {
|
|||||||
state.devices[device] = player
|
state.devices[device] = player
|
||||||
update()
|
update()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
ipcRenderer.on('wt-infohash', (e, ...args) => torrentInfoHash(...args))
|
||||||
|
ipcRenderer.on('wt-ready', (e, ...args) => torrentReady(...args))
|
||||||
|
ipcRenderer.on('wt-done', (e, ...args) => torrentDone(...args))
|
||||||
|
ipcRenderer.on('wt-warning', (e, ...args) => torrentWarning(...args))
|
||||||
|
ipcRenderer.on('wt-error', (e, ...args) => torrentError(...args))
|
||||||
|
|
||||||
|
ipcRenderer.on('wt-progress', (e, ...args) => torrentProgress(...args))
|
||||||
|
ipcRenderer.on('wt-file-modtimes', (e, ...args) => torrentFileModtimes(...args))
|
||||||
|
ipcRenderer.on('wt-file-saved', (e, ...args) => torrentFileSaved(...args))
|
||||||
|
ipcRenderer.on('wt-poster', (e, ...args) => torrentPosterSaved(...args))
|
||||||
|
ipcRenderer.on('wt-audio-metadata', (e, ...args) => torrentAudioMetadata(...args))
|
||||||
|
ipcRenderer.on('wt-server-running', (e, ...args) => torrentServerRunning(...args))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load state.saved from the JSON state file
|
// Load state.saved from the JSON state file
|
||||||
@@ -429,24 +395,46 @@ function resumeTorrents () {
|
|||||||
.forEach((x) => startTorrentingSummary(x))
|
.forEach((x) => startTorrentingSummary(x))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Don't write state.saved to file more than once a second
|
||||||
|
function saveStateThrottled () {
|
||||||
|
if (state.saveStateTimeout) return
|
||||||
|
state.saveStateTimeout = setTimeout(function () {
|
||||||
|
delete state.saveStateTimeout
|
||||||
|
saveState()
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
// Write state.saved to the JSON state file
|
// Write state.saved to the JSON state file
|
||||||
function saveState () {
|
function saveState () {
|
||||||
console.log('saving state to ' + cfg.filePath)
|
console.log('saving state to ' + cfg.filePath)
|
||||||
|
|
||||||
// Clean up, so that we're not saving any pending state
|
// Clean up, so that we're not saving any pending state
|
||||||
var copy = JSON.parse(JSON.stringify(state.saved))
|
var copy = Object.assign({}, state.saved)
|
||||||
// Remove torrents pending addition to the list, where we haven't finished
|
// 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
|
// reading the torrent file or file(s) to seed & don't have an infohash
|
||||||
copy.torrents = copy.torrents.filter((x) => x.infoHash)
|
copy.torrents = copy.torrents
|
||||||
copy.torrents.forEach(function (x) {
|
.filter((x) => x.infoHash)
|
||||||
if (x.playStatus !== 'unplayable') delete x.playStatus
|
.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' && x.playStatus !== 'unplayable') {
|
||||||
|
continue // Don't save whether a torrent is playing / pending
|
||||||
|
}
|
||||||
|
torrent[key] = x[key]
|
||||||
|
}
|
||||||
|
return torrent
|
||||||
|
})
|
||||||
|
|
||||||
cfg.write(copy, function (err) {
|
cfg.write(copy, function (err) {
|
||||||
if (err) console.error(err)
|
if (err) console.error(err)
|
||||||
ipcRenderer.send('savedState')
|
ipcRenderer.send('savedState')
|
||||||
update()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Update right away, don't wait for the state to save
|
||||||
|
update()
|
||||||
}
|
}
|
||||||
|
|
||||||
function onOpen (files) {
|
function onOpen (files) {
|
||||||
@@ -454,11 +442,11 @@ function onOpen (files) {
|
|||||||
|
|
||||||
// .torrent file = start downloading the torrent
|
// .torrent file = start downloading the torrent
|
||||||
files.filter(isTorrent).forEach(function (torrentFile) {
|
files.filter(isTorrent).forEach(function (torrentFile) {
|
||||||
addTorrent(torrentFile)
|
addTorrent(torrentFile.path)
|
||||||
})
|
})
|
||||||
|
|
||||||
// everything else = seed these files
|
// everything else = seed these files
|
||||||
showCreateTorrentModal(files.filter(isNotTorrent))
|
createTorrentFromFileObjects(files)
|
||||||
}
|
}
|
||||||
|
|
||||||
function onPaste (e) {
|
function onPaste (e) {
|
||||||
@@ -485,316 +473,233 @@ function isNotTorrent (file) {
|
|||||||
|
|
||||||
// Gets a torrent summary {name, infoHash, status} from state.saved.torrents
|
// Gets a torrent summary {name, infoHash, status} from state.saved.torrents
|
||||||
// Returns undefined if we don't know that infoHash
|
// Returns undefined if we don't know that infoHash
|
||||||
function getTorrentSummary (infoHash) {
|
function getTorrentSummary (torrentKey) {
|
||||||
if (!infoHash) return undefined
|
if (!torrentKey) return undefined
|
||||||
return state.saved.torrents.find((x) => x.infoHash === infoHash)
|
return state.saved.torrents.find((x) =>
|
||||||
}
|
x.torrentKey === torrentKey || x.infoHash === torrentKey)
|
||||||
|
|
||||||
// Get an active torrent from state.client.torrents
|
|
||||||
// Returns undefined if we are not currently torrenting that infoHash
|
|
||||||
function getTorrent (infoHash) {
|
|
||||||
if (!infoHash) return undefined
|
|
||||||
var pending = state.pendingTorrents[infoHash]
|
|
||||||
if (pending) return pending
|
|
||||||
return lazyLoadClient().torrents.find((x) => x.infoHash === infoHash)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Adds a torrent to the list, starts downloading/seeding. TorrentID can be a
|
// Adds a torrent to the list, starts downloading/seeding. TorrentID can be a
|
||||||
// magnet URI, infohash, or torrent file: https://github.com/feross/webtorrent#clientaddtorrentid-opts-function-ontorrent-torrent-
|
// magnet URI, infohash, or torrent file: https://github.com/feross/webtorrent#clientaddtorrentid-opts-function-ontorrent-torrent-
|
||||||
function addTorrent (torrentId) {
|
function addTorrent (torrentId) {
|
||||||
var torrent = startTorrentingID(torrentId)
|
var torrentKey = state.nextTorrentKey++
|
||||||
torrent.on('infoHash', function () {
|
var path = state.saved.downloadPath
|
||||||
addTorrentToList(torrent)
|
ipcRenderer.send('wt-start-torrenting', torrentKey, torrentId, path)
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function addTorrentToList (torrent) {
|
|
||||||
if (getTorrentSummary(torrent.infoHash)) {
|
|
||||||
return // Skip, torrent is already in state.saved
|
|
||||||
}
|
|
||||||
|
|
||||||
var torrentSummary = {
|
|
||||||
status: 'new',
|
|
||||||
name: torrent.name
|
|
||||||
}
|
|
||||||
state.saved.torrents.push(torrentSummary)
|
|
||||||
playInterfaceSound('ADD')
|
|
||||||
|
|
||||||
// If torrentId is a remote torrent (filesystem path, http url, etc.), wait for
|
|
||||||
// WebTorrent to finish reading it
|
|
||||||
if (torrent.infoHash) onInfoHash()
|
|
||||||
else torrent.on('infoHash', onInfoHash)
|
|
||||||
|
|
||||||
function onInfoHash () {
|
|
||||||
torrentSummary.infoHash = torrent.infoHash
|
|
||||||
torrentSummary.magnetURI = torrent.magnetURI
|
|
||||||
saveState()
|
|
||||||
update()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Starts downloading and/or seeding a given torrentSummary. Returns WebTorrent object
|
// Starts downloading and/or seeding a given torrentSummary. Returns WebTorrent object
|
||||||
function startTorrentingSummary (torrentSummary) {
|
function startTorrentingSummary (torrentSummary) {
|
||||||
var s = torrentSummary
|
var s = torrentSummary
|
||||||
if (s.torrentPath) {
|
|
||||||
var torrentPath = util.getAbsoluteStaticPath(s.torrentPath)
|
// Backward compatibility for config files save before we had torrentKey
|
||||||
var ret = startTorrentingID(torrentPath, s.path, s.fileModtimes)
|
if (!s.torrentKey) s.torrentKey = state.nextTorrentKey++
|
||||||
if (s.infoHash) state.pendingTorrents[s.infoHash] = ret
|
|
||||||
return ret
|
// Use Downloads folder by default
|
||||||
} else if (s.magnetURI) {
|
var path = s.path || state.saved.downloadPath
|
||||||
return startTorrentingID(s.magnetURI, s.path, s.fileModtimes)
|
|
||||||
} else {
|
var torrentID
|
||||||
return startTorrentingID(s.infoHash, s.path, s.fileModtimes)
|
if (s.torrentPath) { // Load torrent file from disk
|
||||||
|
torrentID = util.getAbsoluteStaticPath(s.torrentPath)
|
||||||
|
} else { // Load torrent from DHT
|
||||||
|
torrentID = s.magnetURI || s.infoHash
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ipcRenderer.send('wt-start-torrenting', s.torrentKey, torrentID, path, s.fileModtimes)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Starts a given TorrentID, which can be an infohash, magnet URI, etc. Returns WebTorrent object
|
// TODO: maybe have a "create torrent" modal in the future, with options like
|
||||||
// See https://github.com/feross/webtorrent/blob/master/docs/api.md#clientaddtorrentid-opts-function-ontorrent-torrent-
|
// custom trackers, private flag, and so on?
|
||||||
function startTorrentingID (torrentID, path, fileModtimes) {
|
//
|
||||||
console.log('starting torrent ' + torrentID)
|
// Right now create-torrent-modal is v basic, only user input is OK / Cancel
|
||||||
var torrent = lazyLoadClient().add(torrentID, {
|
//
|
||||||
path: path || state.saved.downloadPath, // Use downloads folder
|
// Also, if you uncomment below below, creating a torrent thru
|
||||||
fileModtimes: fileModtimes
|
// File > Create New Torrent will still create a new torrent directly, while
|
||||||
})
|
// dragging files or folders onto the app opens the create-torrent-modal
|
||||||
addTorrentEvents(torrent)
|
//
|
||||||
return torrent
|
// That's because the former gets a single string and the latter gets a list
|
||||||
}
|
// of W3C File objects. We should fix this inconsistency, ideally without
|
||||||
|
// duping this code in the drag-drop module:
|
||||||
|
// https://github.com/feross/drag-drop/blob/master/index.js
|
||||||
|
//
|
||||||
|
// function showCreateTorrentModal (files) {
|
||||||
|
// if (files.length === 0) return
|
||||||
|
// state.modal = {
|
||||||
|
// id: 'create-torrent-modal',
|
||||||
|
// files: files
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
// Stops downloading and/or seeding
|
//
|
||||||
function stopTorrenting (infoHash) {
|
// TORRENT MANAGEMENT
|
||||||
var torrent = getTorrent(infoHash)
|
// Send commands to the WebTorrent process, handle events
|
||||||
if (torrent) torrent.destroy()
|
//
|
||||||
}
|
|
||||||
|
|
||||||
// Prompts the user to create a torrent for a local file or folder
|
// Creates a new torrent from a drag-dropped file or folder
|
||||||
function showCreateTorrentModal (files) {
|
function createTorrentFromFileObjects (files) {
|
||||||
if (files.length === 0) return
|
var filePaths = (files
|
||||||
state.modal = {
|
.filter(isNotTorrent)
|
||||||
id: 'create-torrent-modal',
|
.map((x) => x.path))
|
||||||
files: files
|
|
||||||
|
// Single-file torrents are easy. Multi-file torrents require special handling
|
||||||
|
// make sure WebTorrent seeds all files in place, without copying to /tmp
|
||||||
|
if (filePaths.length === 1) {
|
||||||
|
return createTorrent({files: filePaths[0]})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// First, extract the base folder that the files are all in
|
||||||
|
var pathPrefix = files.map((x) => x.path).reduce(findCommonPrefix)
|
||||||
|
if (files.length > 0 && !pathPrefix.endsWith('/') && !pathPrefix.endsWith('\\')) {
|
||||||
|
pathPrefix = path.dirname(pathPrefix)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then, use the name of the base folder (or sole file, for a single file torrent)
|
||||||
|
// as the default name. Show all files relative to the base folder.
|
||||||
|
var defaultName = path.basename(pathPrefix)
|
||||||
|
var basePath = path.dirname(pathPrefix)
|
||||||
|
var options = {
|
||||||
|
// TODO: we can't let the user choose their own name if we want WebTorrent
|
||||||
|
// to use the files in place rather than creating a new folder.
|
||||||
|
name: defaultName,
|
||||||
|
path: basePath,
|
||||||
|
files: filePaths
|
||||||
|
}
|
||||||
|
|
||||||
|
createTorrent(options)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Creates a new torrent and start seeeding
|
// Creates a new torrent and start seeeding
|
||||||
function createTorrent (options) {
|
function createTorrent (options) {
|
||||||
var torrent = lazyLoadClient().seed(options.files, options)
|
var torrentKey = state.nextTorrentKey++
|
||||||
addTorrentToList(torrent)
|
ipcRenderer.send('wt-create-torrent', torrentKey, options)
|
||||||
addTorrentEvents(torrent)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function addTorrentEvents (torrent) {
|
function torrentInfoHash (torrentKey, infoHash) {
|
||||||
torrent.on('infoHash', function () {
|
var torrentSummary = getTorrentSummary(torrentKey)
|
||||||
var infoHash = torrent.infoHash
|
console.log('got infohash for %s torrent %s',
|
||||||
if (state.pendingTorrents[infoHash]) delete state.pendingTorrents[infoHash]
|
torrentSummary ? 'existing' : 'new', torrentKey)
|
||||||
update()
|
|
||||||
})
|
|
||||||
torrent.on('ready', torrentReady)
|
|
||||||
torrent.on('done', torrentDone)
|
|
||||||
torrent.on('warning', onWarning)
|
|
||||||
torrent.on('error', torrentError)
|
|
||||||
|
|
||||||
function torrentError (err) {
|
if (!torrentSummary) {
|
||||||
|
torrentSummary = {
|
||||||
|
torrentKey: torrentKey,
|
||||||
|
status: 'new'
|
||||||
|
}
|
||||||
|
state.saved.torrents.push(torrentSummary)
|
||||||
|
playInterfaceSound('ADD')
|
||||||
|
}
|
||||||
|
|
||||||
|
torrentSummary.infoHash = infoHash
|
||||||
|
update()
|
||||||
|
}
|
||||||
|
|
||||||
|
function torrentWarning (torrentKey, message) {
|
||||||
|
onWarning(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
function torrentError (torrentKey, message) {
|
||||||
|
var torrentSummary = getTorrentSummary(torrentKey)
|
||||||
|
|
||||||
|
// TODO: WebTorrent should have semantic errors
|
||||||
|
if (message.startsWith('There is already a swarm')) {
|
||||||
|
onError(new Error('Couldn\'t add duplicate torrent'))
|
||||||
|
} else if (!torrentSummary) {
|
||||||
|
onError(message)
|
||||||
|
} else {
|
||||||
console.log('error, stopping torrent %s (%s):\n\t%o',
|
console.log('error, stopping torrent %s (%s):\n\t%o',
|
||||||
torrent.name, torrent.infoHash, err.message)
|
torrentSummary.name, torrentSummary.infoHash, message)
|
||||||
// TODO: update torrentSummary, even if it doesn't have an infohash yet
|
torrentSummary.status = 'paused'
|
||||||
if (torrent.infoHash) {
|
|
||||||
getTorrentSummary(torrent.infoHash).status = 'paused'
|
|
||||||
update()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function torrentReady () {
|
|
||||||
// Summarize torrent
|
|
||||||
var torrentSummary = getTorrentSummary(torrent.infoHash)
|
|
||||||
torrentSummary.status = 'downloading'
|
|
||||||
torrentSummary.ready = true
|
|
||||||
torrentSummary.name = torrentSummary.displayName || torrent.name
|
|
||||||
torrentSummary.path = torrent.path
|
|
||||||
|
|
||||||
// Summarize torrent files
|
|
||||||
torrentSummary.files = torrent.files.map(summarizeFileInTorrent)
|
|
||||||
updateTorrentProgress()
|
|
||||||
|
|
||||||
// Save the .torrent file, if it hasn't been saved already
|
|
||||||
if (!torrentSummary.torrentPath) saveTorrentFile(torrentSummary, torrent)
|
|
||||||
|
|
||||||
// Auto-generate a poster image, if it hasn't been generated already
|
|
||||||
if (!torrentSummary.posterURL) generateTorrentPoster(torrent, torrentSummary)
|
|
||||||
|
|
||||||
update()
|
|
||||||
}
|
|
||||||
|
|
||||||
function torrentDone () {
|
|
||||||
// Update the torrent summary
|
|
||||||
var torrentSummary = getTorrentSummary(torrent.infoHash)
|
|
||||||
torrentSummary.status = 'seeding'
|
|
||||||
updateTorrentProgress()
|
|
||||||
torrent.getFileModtimes(function (err, fileModtimes) {
|
|
||||||
if (err) return onError(err)
|
|
||||||
torrentSummary.fileModtimes = fileModtimes
|
|
||||||
saveState()
|
|
||||||
})
|
|
||||||
|
|
||||||
// Notify the user that a torrent finished, but only if we actually DL'd at least part of it.
|
|
||||||
// Don't notify if we merely finished verifying data files that were already on disk.
|
|
||||||
if (torrent.received > 0) {
|
|
||||||
if (!state.window.isFocused) {
|
|
||||||
state.dock.badge += 1
|
|
||||||
}
|
|
||||||
showDoneNotification(torrent)
|
|
||||||
}
|
|
||||||
|
|
||||||
update()
|
update()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateTorrentProgress () {
|
function torrentReady (torrentKey, torrentInfo) {
|
||||||
var changed = false
|
// Summarize torrent
|
||||||
|
var torrentSummary = getTorrentSummary(torrentKey)
|
||||||
|
torrentSummary.status = 'downloading'
|
||||||
|
torrentSummary.ready = true
|
||||||
|
torrentSummary.name = torrentSummary.displayName || torrentInfo.name
|
||||||
|
torrentSummary.path = torrentInfo.path
|
||||||
|
torrentSummary.files = torrentInfo.files
|
||||||
|
update()
|
||||||
|
|
||||||
|
// Save the .torrent file, if it hasn't been saved already
|
||||||
|
if (!torrentSummary.torrentPath) ipcRenderer.send('wt-save-torrent-file', torrentKey)
|
||||||
|
|
||||||
|
// Auto-generate a poster image, if it hasn't been generated already
|
||||||
|
if (!torrentSummary.posterURL) ipcRenderer.send('wt-generate-torrent-poster', torrentKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
function torrentDone (torrentKey, torrentInfo) {
|
||||||
|
// Update the torrent summary
|
||||||
|
var torrentSummary = getTorrentSummary(torrentKey)
|
||||||
|
torrentSummary.status = 'seeding'
|
||||||
|
|
||||||
|
// Notify the user that a torrent finished, but only if we actually DL'd at least part of it.
|
||||||
|
// Don't notify if we merely finished verifying data files that were already on disk.
|
||||||
|
if (torrentInfo.bytesReceived > 0) {
|
||||||
|
if (!state.window.isFocused) {
|
||||||
|
state.dock.badge += 1
|
||||||
|
}
|
||||||
|
showDoneNotification(torrentKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
update()
|
||||||
|
}
|
||||||
|
|
||||||
|
function torrentProgress (progressInfo) {
|
||||||
|
// Overall progress across all active torrents, 0 to 1
|
||||||
|
var progress = progressInfo.progress
|
||||||
|
var hasActiveTorrents = progressInfo.hasActiveTorrents
|
||||||
|
|
||||||
// First, track overall progress
|
|
||||||
var progress = lazyLoadClient().progress
|
|
||||||
var activeTorrentsExist = lazyLoadClient().torrents.some(function (torrent) {
|
|
||||||
return torrent.progress !== 1
|
|
||||||
})
|
|
||||||
// Hide progress bar when client has no torrents, or progress is 100%
|
// Hide progress bar when client has no torrents, or progress is 100%
|
||||||
if (!activeTorrentsExist || progress === 1) {
|
// TODO: isn't this equivalent to: if (progress === 1) ?
|
||||||
|
if (!hasActiveTorrents || progress === 1) {
|
||||||
progress = -1
|
progress = -1
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show progress bar under the WebTorrent taskbar icon, on OSX
|
// Show progress bar under the WebTorrent taskbar icon, on OSX
|
||||||
if (state.dock.progress !== progress) changed = true
|
|
||||||
state.dock.progress = progress
|
state.dock.progress = progress
|
||||||
|
|
||||||
// Track progress for every file in each torrentSummary
|
// Update progress for each individual torrent
|
||||||
// TODO: ideally this would be tracked by WebTorrent, which could do it
|
progressInfo.torrents.forEach(function (p) {
|
||||||
// more efficiently than looping over torrent.bitfield
|
var torrentSummary = getTorrentSummary(p.torrentKey)
|
||||||
lazyLoadClient().torrents.forEach(function (torrent) {
|
if (!torrentSummary) {
|
||||||
var torrentSummary = getTorrentSummary(torrent.infoHash)
|
console.log('warning: got progress for missing torrent %s', p.torrentKey)
|
||||||
if (!torrentSummary || !torrent.ready) return
|
|
||||||
torrent.files.forEach(function (file, index) {
|
|
||||||
var numPieces = file._endPiece - file._startPiece + 1
|
|
||||||
var numPiecesPresent = 0
|
|
||||||
for (var piece = file._startPiece; piece <= file._endPiece; piece++) {
|
|
||||||
if (torrent.bitfield.get(piece)) numPiecesPresent++
|
|
||||||
}
|
|
||||||
|
|
||||||
var fileSummary = torrentSummary.files[index]
|
|
||||||
if (fileSummary.numPiecesPresent !== numPiecesPresent || fileSummary.numPieces !== numPieces) {
|
|
||||||
fileSummary.numPieces = numPieces
|
|
||||||
fileSummary.numPiecesPresent = numPiecesPresent
|
|
||||||
changed = true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
if (changed) update()
|
|
||||||
return changed
|
|
||||||
}
|
|
||||||
|
|
||||||
function generateTorrentPoster (torrent, torrentSummary) {
|
|
||||||
torrentPoster(torrent, function (err, buf, extension) {
|
|
||||||
if (err) return onWarning(err)
|
|
||||||
// save it for next time
|
|
||||||
mkdirp(config.CONFIG_POSTER_PATH, function (err) {
|
|
||||||
if (err) return onWarning(err)
|
|
||||||
var posterFilePath = path.join(config.CONFIG_POSTER_PATH, torrent.infoHash + extension)
|
|
||||||
fs.writeFile(posterFilePath, buf, function (err) {
|
|
||||||
if (err) return onWarning(err)
|
|
||||||
// show the poster
|
|
||||||
torrentSummary.posterURL = posterFilePath
|
|
||||||
update()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Produces a JSON saveable summary of a file in a torrent
|
|
||||||
function summarizeFileInTorrent (file) {
|
|
||||||
return {
|
|
||||||
name: file.name,
|
|
||||||
length: file.length,
|
|
||||||
numPiecesPresent: 0,
|
|
||||||
numPieces: null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Every time we resolve a magnet URI, save the torrent file so that we never
|
|
||||||
// have to download it again. Never ask the DHT the same question twice.
|
|
||||||
function saveTorrentFile (torrentSummary, torrent) {
|
|
||||||
checkIfTorrentFileExists(torrentSummary.infoHash, function (torrentPath, exists) {
|
|
||||||
if (exists) {
|
|
||||||
// We've already saved the file
|
|
||||||
torrentSummary.torrentPath = torrentPath
|
|
||||||
saveState()
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
torrentSummary.progress = p
|
||||||
// Otherwise, save the .torrent file, under the app config folder
|
|
||||||
fs.mkdir(config.CONFIG_TORRENT_PATH, function (_) {
|
|
||||||
fs.writeFile(torrentPath, torrent.torrentFile, function (err) {
|
|
||||||
if (err) return console.log('error saving torrent file %s: %o', torrentPath, err)
|
|
||||||
console.log('saved torrent file %s', torrentPath)
|
|
||||||
torrentSummary.torrentPath = torrentPath
|
|
||||||
saveState()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
update()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Checks whether we've already resolved a given infohash to a torrent file
|
function torrentFileModtimes (torrentKey, fileModtimes) {
|
||||||
// Calls back with (torrentPath, exists). Logs, does not call back on error
|
var torrentSummary = getTorrentSummary(torrentKey)
|
||||||
function checkIfTorrentFileExists (infoHash, cb) {
|
torrentSummary.fileModtimes = fileModtimes
|
||||||
var torrentPath = path.join(config.CONFIG_TORRENT_PATH, infoHash + '.torrent')
|
saveStateThrottled()
|
||||||
fs.exists(torrentPath, function (exists) {
|
|
||||||
cb(torrentPath, exists)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function startServer (torrentSummary, index, cb) {
|
function torrentFileSaved (torrentKey, torrentPath) {
|
||||||
if (state.server) return cb()
|
console.log('torrent file saved %s: %s', torrentKey, torrentPath)
|
||||||
|
var torrentSummary = getTorrentSummary(torrentKey)
|
||||||
var torrent = getTorrent(torrentSummary.infoHash)
|
torrentSummary.torrentPath = torrentPath
|
||||||
if (!torrent) torrent = startTorrentingSummary(torrentSummary)
|
saveStateThrottled()
|
||||||
if (torrent.ready) startServerFromReadyTorrent(torrent, index, cb)
|
|
||||||
else torrent.on('ready', () => startServerFromReadyTorrent(torrent, index, cb))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function startServerFromReadyTorrent (torrent, index, cb) {
|
function torrentPosterSaved (torrentKey, posterPath) {
|
||||||
// automatically choose which file in the torrent to play, if necessary
|
var torrentSummary = getTorrentSummary(torrentKey)
|
||||||
if (index === undefined) index = pickFileToPlay(torrent.files)
|
torrentSummary.posterURL = posterPath
|
||||||
if (index === undefined) return cb(new errors.UnplayableError())
|
saveStateThrottled()
|
||||||
var file = torrent.files[index]
|
}
|
||||||
|
|
||||||
// update state
|
function torrentAudioMetadata (infoHash, index, info) {
|
||||||
state.playing.infoHash = torrent.infoHash
|
var torrentSummary = getTorrentSummary(infoHash)
|
||||||
state.playing.fileIndex = index
|
|
||||||
state.playing.type = TorrentPlayer.isVideo(file) ? 'video'
|
|
||||||
: TorrentPlayer.isAudio(file) ? 'audio'
|
|
||||||
: 'other'
|
|
||||||
|
|
||||||
// if it's audio, parse out the metadata (artist, title, etc)
|
|
||||||
var torrentSummary = getTorrentSummary(torrent.infoHash)
|
|
||||||
var fileSummary = torrentSummary.files[index]
|
var fileSummary = torrentSummary.files[index]
|
||||||
if (state.playing.type === 'audio' && !fileSummary.audioInfo) {
|
fileSummary.audioInfo = info
|
||||||
musicmetadata(file.createReadStream(), function (err, info) {
|
update()
|
||||||
if (err) return
|
}
|
||||||
console.log('got audio metadata for %s: %o', file.name, info)
|
|
||||||
fileSummary.audioInfo = info
|
|
||||||
update()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// either way, start a streaming torrent-to-http server
|
function torrentServerRunning (serverInfo) {
|
||||||
var server = torrent.createServer()
|
state.server = serverInfo
|
||||||
server.listen(0, function () {
|
|
||||||
var port = server.address().port
|
|
||||||
var urlSuffix = ':' + port + '/' + index
|
|
||||||
state.server = {
|
|
||||||
server: server,
|
|
||||||
localURL: 'http://localhost' + urlSuffix,
|
|
||||||
networkURL: 'http://' + networkAddress() + urlSuffix
|
|
||||||
}
|
|
||||||
cb()
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Picks the default file to play from a list of torrent or torrentSummary files
|
// Picks the default file to play from a list of torrent or torrentSummary files
|
||||||
@@ -820,18 +725,22 @@ function pickFileToPlay (files) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function stopServer () {
|
function stopServer () {
|
||||||
if (!state.server) return
|
ipcRenderer.send('wt-stop-server')
|
||||||
state.server.server.destroy()
|
|
||||||
state.server = null
|
|
||||||
state.playing.infoHash = null
|
state.playing.infoHash = null
|
||||||
state.playing.fileIndex = null
|
state.playing.fileIndex = null
|
||||||
|
state.server = null
|
||||||
}
|
}
|
||||||
|
|
||||||
// Opens the video player
|
// Opens the video player
|
||||||
function openPlayer (infoHash, index, cb) {
|
function openPlayer (infoHash, index, cb) {
|
||||||
var torrentSummary = getTorrentSummary(infoHash)
|
var torrentSummary = getTorrentSummary(infoHash)
|
||||||
var torrent = lazyLoadClient().get(infoHash)
|
|
||||||
if (!torrent || !torrent.done) playInterfaceSound('PLAY')
|
// automatically choose which file in the torrent to play, if necessary
|
||||||
|
if (index === undefined) index = pickFileToPlay(torrentSummary.files)
|
||||||
|
if (index === undefined) return cb(new errors.UnplayableError())
|
||||||
|
|
||||||
|
// update UI to show pending playback
|
||||||
|
if (torrentSummary.progress !== 1) playInterfaceSound('PLAY')
|
||||||
torrentSummary.playStatus = 'requested'
|
torrentSummary.playStatus = 'requested'
|
||||||
update()
|
update()
|
||||||
|
|
||||||
@@ -842,14 +751,43 @@ function openPlayer (infoHash, index, cb) {
|
|||||||
update()
|
update()
|
||||||
}, 10000) /* give it a few seconds */
|
}, 10000) /* give it a few seconds */
|
||||||
|
|
||||||
startServer(torrentSummary, index, function (err) {
|
if (['downloading', 'seeding'].includes(torrentSummary.status)) {
|
||||||
|
openPlayerFromActiveTorrent(torrentSummary, index, timeout, cb)
|
||||||
|
} else {
|
||||||
|
startTorrentingSummary(torrentSummary)
|
||||||
|
ipcRenderer.once('wt-ready-' + torrentSummary.infoHash,
|
||||||
|
() => openPlayerFromActiveTorrent(torrentSummary, index, timeout, cb))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openPlayerFromActiveTorrent (torrentSummary, index, timeout, cb) {
|
||||||
|
var fileSummary = torrentSummary.files[index]
|
||||||
|
|
||||||
|
// update state
|
||||||
|
state.playing.infoHash = torrentSummary.infoHash
|
||||||
|
state.playing.fileIndex = index
|
||||||
|
state.playing.type = TorrentPlayer.isVideo(fileSummary) ? 'video'
|
||||||
|
: TorrentPlayer.isAudio(fileSummary) ? 'audio'
|
||||||
|
: 'other'
|
||||||
|
|
||||||
|
// if it's audio, parse out the metadata (artist, title, etc)
|
||||||
|
if (state.playing.type === 'audio' && !fileSummary.audioInfo) {
|
||||||
|
ipcRenderer.send('wt-get-audio-metadata', torrentSummary.infoHash, index)
|
||||||
|
}
|
||||||
|
|
||||||
|
ipcRenderer.send('wt-start-server', torrentSummary.infoHash, index)
|
||||||
|
ipcRenderer.once('wt-server-' + torrentSummary.infoHash, function (e, info) {
|
||||||
clearTimeout(timeout)
|
clearTimeout(timeout)
|
||||||
|
|
||||||
|
/**
|
||||||
|
TODO: where do we know if it's unplayable?
|
||||||
if (err) {
|
if (err) {
|
||||||
torrentSummary.playStatus = 'unplayable'
|
torrentSummary.playStatus = 'unplayable'
|
||||||
playInterfaceSound('ERROR')
|
playInterfaceSound('ERROR')
|
||||||
update()
|
update()
|
||||||
return onError(err)
|
return onError(err)
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
// if we timed out (user clicked play a long time ago), don't autoplay
|
// if we timed out (user clicked play a long time ago), don't autoplay
|
||||||
var timedOut = torrentSummary.playStatus === 'timeout'
|
var timedOut = torrentSummary.playStatus === 'timeout'
|
||||||
@@ -884,28 +822,25 @@ function closePlayer (cb) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function openFile (infoHash, index) {
|
function openFile (infoHash, index) {
|
||||||
var torrent = lazyLoadClient().get(infoHash)
|
var torrentSummary = getTorrentSummary(infoHash)
|
||||||
if (!torrent) return
|
var filePath = path.join(
|
||||||
|
torrentSummary.path,
|
||||||
var filePath = path.join(torrent.path, torrent.files[index].path)
|
torrentSummary.files[index].path)
|
||||||
ipcRenderer.send('openItem', filePath)
|
ipcRenderer.send('openItem', filePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
function openFolder (infoHash) {
|
function openFolder (infoHash) {
|
||||||
var torrent = lazyLoadClient().get(infoHash)
|
var torrentSummary = getTorrentSummary(infoHash)
|
||||||
if (!torrent) return
|
|
||||||
|
|
||||||
var folderPath = path.join(torrent.path, torrent.name)
|
var firstFilePath = path.join(
|
||||||
// Multi-file torrents create their own folder, single file torrents just
|
torrentSummary.path,
|
||||||
// drop the file directly into the Downloads folder
|
torrentSummary.files[0].path)
|
||||||
fs.stat(folderPath, function (err, stats) {
|
var folderPath = path.dirname(firstFilePath)
|
||||||
if (err || !stats.isDirectory()) {
|
|
||||||
folderPath = torrent.path
|
ipcRenderer.send('openItem', folderPath)
|
||||||
}
|
|
||||||
ipcRenderer.send('openItem', folderPath)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: use torrentKey, not infoHash
|
||||||
function toggleTorrent (infoHash) {
|
function toggleTorrent (infoHash) {
|
||||||
var torrentSummary = getTorrentSummary(infoHash)
|
var torrentSummary = getTorrentSummary(infoHash)
|
||||||
if (torrentSummary.status === 'paused') {
|
if (torrentSummary.status === 'paused') {
|
||||||
@@ -914,18 +849,18 @@ function toggleTorrent (infoHash) {
|
|||||||
playInterfaceSound('ENABLE')
|
playInterfaceSound('ENABLE')
|
||||||
} else {
|
} else {
|
||||||
torrentSummary.status = 'paused'
|
torrentSummary.status = 'paused'
|
||||||
stopTorrenting(torrentSummary.infoHash)
|
ipcRenderer.send('wt-stop-torrenting', torrentSummary.infoHash)
|
||||||
playInterfaceSound('DISABLE')
|
playInterfaceSound('DISABLE')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: use torrentKey, not infoHash
|
||||||
function deleteTorrent (infoHash) {
|
function deleteTorrent (infoHash) {
|
||||||
var torrent = getTorrent(infoHash)
|
ipcRenderer.send('wt-stop-torrenting', infoHash)
|
||||||
if (torrent) torrent.destroy()
|
|
||||||
|
|
||||||
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)
|
||||||
saveState()
|
saveStateThrottled()
|
||||||
state.location.clearForward() // prevent user from going forward to a deleted torrent
|
state.location.clearForward() // prevent user from going forward to a deleted torrent
|
||||||
playInterfaceSound('DELETE')
|
playInterfaceSound('DELETE')
|
||||||
}
|
}
|
||||||
@@ -1032,7 +967,7 @@ function onError (err) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onWarning (err) {
|
function onWarning (err) {
|
||||||
console.log('warning: %s', err.message)
|
console.log('warning: %s', err.message || err)
|
||||||
}
|
}
|
||||||
|
|
||||||
function showDoneNotification (torrent) {
|
function showDoneNotification (torrent) {
|
||||||
@@ -1067,3 +1002,13 @@ function setupCrashReporter () {
|
|||||||
submitURL: config.CRASH_REPORT_URL
|
submitURL: config.CRASH_REPORT_URL
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Finds the longest common prefix
|
||||||
|
function findCommonPrefix (a, b) {
|
||||||
|
for (var i = 0; i < a.length && i < b.length; i++) {
|
||||||
|
if (a.charCodeAt(i) !== b.charCodeAt(i)) break
|
||||||
|
}
|
||||||
|
if (i === a.length) return a
|
||||||
|
if (i === b.length) return b
|
||||||
|
return a.substring(0, i)
|
||||||
|
}
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ module.exports = {
|
|||||||
lastTimeUpdate: 0, /* Unix time in ms */
|
lastTimeUpdate: 0, /* Unix time in ms */
|
||||||
mouseStationarySince: 0 /* Unix time in ms */
|
mouseStationarySince: 0 /* Unix time in ms */
|
||||||
},
|
},
|
||||||
pendingTorrents: {}, /* infohash to WebTorrent handle */
|
|
||||||
devices: { /* playback devices like Chromecast and AppleTV */
|
devices: { /* playback devices like Chromecast and AppleTV */
|
||||||
airplay: null, /* airplay client. finds and manages AppleTVs */
|
airplay: null, /* airplay client. finds and manages AppleTVs */
|
||||||
chromecast: null /* chromecast client. finds and manages Chromecasts */
|
chromecast: null /* chromecast client. finds and manages Chromecasts */
|
||||||
@@ -43,6 +42,7 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
modal: null, /* modal popover */
|
modal: null, /* modal popover */
|
||||||
errors: [], /* user-facing errors */
|
errors: [], /* user-facing errors */
|
||||||
|
nextTorrentKey: 1, /* identify torrents for IPC between the main and webtorrent windows */
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Saved state is read from and written to a file every time the app runs.
|
* Saved state is read from and written to a file every time the app runs.
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ var hyperx = require('hyperx')
|
|||||||
var hx = hyperx(h)
|
var hx = hyperx(h)
|
||||||
|
|
||||||
var prettyBytes = require('prettier-bytes')
|
var prettyBytes = require('prettier-bytes')
|
||||||
|
var Bitfield = require('bitfield')
|
||||||
|
|
||||||
var util = require('../util')
|
var util = require('../util')
|
||||||
var {dispatch, dispatcher} = require('../lib/dispatcher')
|
var {dispatch, dispatcher} = require('../lib/dispatcher')
|
||||||
@@ -20,7 +21,7 @@ function Player (state) {
|
|||||||
onmousemove=${dispatcher('mediaMouseMoved')}>
|
onmousemove=${dispatcher('mediaMouseMoved')}>
|
||||||
${showVideo ? renderMedia(state) : renderCastScreen(state)}
|
${showVideo ? renderMedia(state) : renderCastScreen(state)}
|
||||||
${renderPlayerControls(state)}
|
${renderPlayerControls(state)}
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,18 +162,20 @@ function renderLoadingSpinner (state) {
|
|||||||
(new Date().getTime() - state.playing.lastTimeUpdate > 2000)
|
(new Date().getTime() - state.playing.lastTimeUpdate > 2000)
|
||||||
if (!isProbablyStalled) return
|
if (!isProbablyStalled) return
|
||||||
|
|
||||||
var torrentSummary = getPlayingTorrentSummary(state)
|
var prog = getPlayingTorrentSummary(state).progress || {}
|
||||||
var torrent = state.client.get(torrentSummary.infoHash)
|
var fileProgress = 0
|
||||||
var file = torrentSummary.files[state.playing.fileIndex]
|
if (prog.files) {
|
||||||
var progress = Math.floor(100 * file.numPiecesPresent / file.numPieces)
|
var file = prog.files[state.playing.fileIndex]
|
||||||
|
fileProgress = Math.floor(100 * file.numPiecesPresent / file.numPieces)
|
||||||
|
}
|
||||||
|
|
||||||
return hx`
|
return hx`
|
||||||
<div class='media-stalled'>
|
<div class='media-stalled'>
|
||||||
<div class='loading-spinner'> </div>
|
<div class='loading-spinner'> </div>
|
||||||
<div class='loading-status ellipsis'>
|
<div class='loading-status ellipsis'>
|
||||||
<span class='progress'>${progress}%</span> downloaded,
|
<span class='progress'>${fileProgress}%</span> downloaded,
|
||||||
<span>↓ ${prettyBytes(torrent.downloadSpeed || 0)}/s</span>
|
<span>↓ ${prettyBytes(prog.downloadSpeed || 0)}/s</span>
|
||||||
<span>↑ ${prettyBytes(torrent.uploadSpeed || 0)}/s</span>
|
<span>↑ ${prettyBytes(prog.uploadSpeed || 0)}/s</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
@@ -203,25 +206,6 @@ function renderCastScreen (state) {
|
|||||||
`
|
`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns the CSS background-image string for a poster image + dark vignette
|
|
||||||
function cssBackgroundImagePoster (state) {
|
|
||||||
var torrentSummary = getPlayingTorrentSummary(state)
|
|
||||||
if (!torrentSummary || !torrentSummary.posterURL) return ''
|
|
||||||
var posterURL = util.getAbsoluteStaticPath(torrentSummary.posterURL)
|
|
||||||
var cleanURL = posterURL.replace(/\\/g, '/')
|
|
||||||
return cssBackgroundImageDarkGradient() + `, url(${cleanURL})`
|
|
||||||
}
|
|
||||||
|
|
||||||
function cssBackgroundImageDarkGradient () {
|
|
||||||
return 'radial-gradient(circle at center, ' +
|
|
||||||
'rgba(0,0,0,0.4) 0%, rgba(0,0,0,1) 100%)'
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPlayingTorrentSummary (state) {
|
|
||||||
var infoHash = state.playing.infoHash
|
|
||||||
return state.saved.torrents.find((x) => x.infoHash === infoHash)
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderPlayerControls (state) {
|
function renderPlayerControls (state) {
|
||||||
var positionPercent = 100 * state.playing.currentTime / state.playing.duration
|
var positionPercent = 100 * state.playing.currentTime / state.playing.duration
|
||||||
var playbackCursorStyle = { left: 'calc(' + positionPercent + '% - 8px)' }
|
var playbackCursorStyle = { left: 'calc(' + positionPercent + '% - 8px)' }
|
||||||
@@ -340,24 +324,24 @@ function renderPlayerControls (state) {
|
|||||||
// Renders the loading bar. Shows which parts of the torrent are loaded, which
|
// Renders the loading bar. Shows which parts of the torrent are loaded, which
|
||||||
// can be "spongey" / non-contiguous
|
// can be "spongey" / non-contiguous
|
||||||
function renderLoadingBar (state) {
|
function renderLoadingBar (state) {
|
||||||
var torrent = state.client.get(state.playing.infoHash)
|
var torrentSummary = getPlayingTorrentSummary(state)
|
||||||
if (torrent === null) {
|
if (!torrentSummary.progress) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
var file = torrent.files[state.playing.fileIndex]
|
|
||||||
|
|
||||||
// Find all contiguous parts of the torrent which are loaded
|
// Find all contiguous parts of the torrent which are loaded
|
||||||
|
var prog = torrentSummary.progress
|
||||||
|
var fileProg = prog.files[state.playing.fileIndex]
|
||||||
var parts = []
|
var parts = []
|
||||||
var lastPartPresent = false
|
var lastPiecePresent = false
|
||||||
var numParts = file._endPiece - file._startPiece + 1
|
for (var i = fileProg.startPiece; i <= fileProg.endPiece; i++) {
|
||||||
for (var i = file._startPiece; i <= file._endPiece; i++) {
|
var partPresent = Bitfield.prototype.get.call(prog.bitfield, i)
|
||||||
var partPresent = torrent.bitfield.get(i)
|
if (partPresent && !lastPiecePresent) {
|
||||||
if (partPresent && !lastPartPresent) {
|
parts.push({start: i - fileProg.startPiece, count: 1})
|
||||||
parts.push({start: i - file._startPiece, count: 1})
|
|
||||||
} else if (partPresent) {
|
} else if (partPresent) {
|
||||||
parts[parts.length - 1].count++
|
parts[parts.length - 1].count++
|
||||||
}
|
}
|
||||||
lastPartPresent = partPresent
|
lastPiecePresent = partPresent
|
||||||
}
|
}
|
||||||
|
|
||||||
// Output some bars to show which parts of the file are loaded
|
// Output some bars to show which parts of the file are loaded
|
||||||
@@ -365,8 +349,8 @@ function renderLoadingBar (state) {
|
|||||||
<div class='loading-bar'>
|
<div class='loading-bar'>
|
||||||
${parts.map(function (part) {
|
${parts.map(function (part) {
|
||||||
var style = {
|
var style = {
|
||||||
left: (100 * part.start / numParts) + '%',
|
left: (100 * part.start / fileProg.numPieces) + '%',
|
||||||
width: (100 * part.count / numParts) + '%'
|
width: (100 * part.count / fileProg.numPieces) + '%'
|
||||||
}
|
}
|
||||||
|
|
||||||
return hx`<div class='loading-bar-part' style=${style}></div>`
|
return hx`<div class='loading-bar-part' style=${style}></div>`
|
||||||
@@ -374,3 +358,22 @@ function renderLoadingBar (state) {
|
|||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Returns the CSS background-image string for a poster image + dark vignette
|
||||||
|
function cssBackgroundImagePoster (state) {
|
||||||
|
var torrentSummary = getPlayingTorrentSummary(state)
|
||||||
|
if (!torrentSummary || !torrentSummary.posterURL) return ''
|
||||||
|
var posterURL = util.getAbsoluteStaticPath(torrentSummary.posterURL)
|
||||||
|
var cleanURL = posterURL.replace(/\\/g, '/')
|
||||||
|
return cssBackgroundImageDarkGradient() + `, url(${cleanURL})`
|
||||||
|
}
|
||||||
|
|
||||||
|
function cssBackgroundImageDarkGradient () {
|
||||||
|
return 'radial-gradient(circle at center, ' +
|
||||||
|
'rgba(0,0,0,0.4) 0%, rgba(0,0,0,1) 100%)'
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPlayingTorrentSummary (state) {
|
||||||
|
var infoHash = state.playing.infoHash
|
||||||
|
return state.saved.torrents.find((x) => x.infoHash === infoHash)
|
||||||
|
}
|
||||||
|
|||||||
@@ -27,9 +27,6 @@ function TorrentList (state) {
|
|||||||
function renderTorrent (torrentSummary) {
|
function renderTorrent (torrentSummary) {
|
||||||
// Get ephemeral data (like progress %) directly from the WebTorrent handle
|
// Get ephemeral data (like progress %) directly from the WebTorrent handle
|
||||||
var infoHash = torrentSummary.infoHash
|
var infoHash = torrentSummary.infoHash
|
||||||
var torrent = state.client
|
|
||||||
? state.client.torrents.find((x) => x.infoHash === infoHash)
|
|
||||||
: null
|
|
||||||
var isSelected = infoHash && state.selectedInfoHash === infoHash
|
var isSelected = infoHash && state.selectedInfoHash === infoHash
|
||||||
|
|
||||||
// Background image: show some nice visuals, like a frame from the movie, if possible
|
// Background image: show some nice visuals, like a frame from the movie, if possible
|
||||||
@@ -57,34 +54,34 @@ function TorrentList (state) {
|
|||||||
<div style=${style} class=${classes}
|
<div style=${style} class=${classes}
|
||||||
oncontextmenu=${infoHash && dispatcher('openTorrentContextMenu', infoHash)}
|
oncontextmenu=${infoHash && dispatcher('openTorrentContextMenu', infoHash)}
|
||||||
onclick=${infoHash && dispatcher('toggleSelectTorrent', infoHash)}>
|
onclick=${infoHash && dispatcher('toggleSelectTorrent', infoHash)}>
|
||||||
${renderTorrentMetadata(torrent, torrentSummary)}
|
${renderTorrentMetadata(torrentSummary)}
|
||||||
${infoHash ? renderTorrentButtons(torrentSummary) : ''}
|
${infoHash ? renderTorrentButtons(torrentSummary) : ''}
|
||||||
${isSelected ? renderTorrentDetails(torrent, torrentSummary) : ''}
|
${isSelected ? renderTorrentDetails(torrentSummary) : ''}
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show name, download status, % complete
|
// Show name, download status, % complete
|
||||||
function renderTorrentMetadata (torrent, torrentSummary) {
|
function renderTorrentMetadata (torrentSummary) {
|
||||||
var name = torrentSummary.name || 'Loading torrent...'
|
var name = torrentSummary.name || 'Loading torrent...'
|
||||||
var elements = [hx`
|
var elements = [hx`
|
||||||
<div class='name ellipsis'>${name}</div>
|
<div class='name ellipsis'>${name}</div>
|
||||||
`]
|
`]
|
||||||
|
|
||||||
// If a torrent is paused and we only get the torrentSummary
|
// If it's downloading/seeding then show progress info
|
||||||
// If it's downloading/seeding then we have more information
|
var prog = torrentSummary.progress
|
||||||
if (torrent) {
|
if (torrentSummary.state !== 'paused' && prog) {
|
||||||
var progress = Math.floor(100 * torrent.progress)
|
var progress = Math.floor(100 * prog.progress)
|
||||||
var downloaded = prettyBytes(torrent.downloaded)
|
var downloaded = prettyBytes(prog.downloaded)
|
||||||
var total = prettyBytes(torrent.length || 0)
|
var total = prettyBytes(prog.length || 0)
|
||||||
if (downloaded !== total) downloaded += ` / ${total}`
|
if (downloaded !== total) downloaded += ` / ${total}`
|
||||||
|
|
||||||
elements.push(hx`
|
elements.push(hx`
|
||||||
<div class='status ellipsis'>
|
<div class='status ellipsis'>
|
||||||
${getFilesLength()}
|
${getFilesLength()}
|
||||||
<span>${getPeers()}</span>
|
<span>${getPeers()}</span>
|
||||||
<span>↓ ${prettyBytes(torrent.downloadSpeed || 0)}/s</span>
|
<span>↓ ${prettyBytes(prog.downloadSpeed || 0)}/s</span>
|
||||||
<span>↑ ${prettyBytes(torrent.uploadSpeed || 0)}/s</span>
|
<span>↑ ${prettyBytes(prog.uploadSpeed || 0)}/s</span>
|
||||||
</div>
|
</div>
|
||||||
`)
|
`)
|
||||||
elements.push(hx`
|
elements.push(hx`
|
||||||
@@ -98,13 +95,13 @@ function TorrentList (state) {
|
|||||||
return hx`<div class='metadata'>${elements}</div>`
|
return hx`<div class='metadata'>${elements}</div>`
|
||||||
|
|
||||||
function getPeers () {
|
function getPeers () {
|
||||||
var count = torrent.numPeers === 1 ? 'peer' : 'peers'
|
var count = prog.numPeers === 1 ? 'peer' : 'peers'
|
||||||
return `${torrent.numPeers} ${count}`
|
return `${prog.numPeers} ${count}`
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFilesLength () {
|
function getFilesLength () {
|
||||||
if (torrent.ready && torrent.files.length > 1) {
|
if (torrentSummary.files && torrentSummary.files.length > 1) {
|
||||||
return hx`<span class='files'>${torrent.files.length} files</span>`
|
return hx`<span class='files'>${torrentSummary.files.length} files</span>`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -165,19 +162,19 @@ function TorrentList (state) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Show files, per-file download status and play buttons, and so on
|
// Show files, per-file download status and play buttons, and so on
|
||||||
function renderTorrentDetails (torrent, torrentSummary) {
|
function renderTorrentDetails (torrentSummary) {
|
||||||
var infoHash = torrentSummary.infoHash
|
var infoHash = torrentSummary.infoHash
|
||||||
var filesElement
|
var filesElement
|
||||||
if (!torrentSummary.files) {
|
if (!torrentSummary.files) {
|
||||||
// We don't know what files this torrent contains
|
// We don't know what files this torrent contains
|
||||||
var message = torrent
|
var message = torrentSummary.status === 'paused'
|
||||||
? 'Downloading torrent info...'
|
? 'Failed to load torrent info. Click the download button to try again...'
|
||||||
: 'Failed to load torrent info. Click the download button to try again...'
|
: 'Downloading torrent info...'
|
||||||
filesElement = hx`<div class='files warning'>${message}</div>`
|
filesElement = hx`<div class='files warning'>${message}</div>`
|
||||||
} else {
|
} else {
|
||||||
// We do know the files. List them and show download stats for each one
|
// We do know the files. List them and show download stats for each one
|
||||||
var fileRows = torrentSummary.files.map(
|
var fileRows = torrentSummary.files.map(
|
||||||
(file, index) => renderFileRow(torrent, torrentSummary, file, index))
|
(file, index) => renderFileRow(torrentSummary, file, index))
|
||||||
filesElement = hx`
|
filesElement = hx`
|
||||||
<div class='files'>
|
<div class='files'>
|
||||||
<strong>Files</strong>
|
<strong>Files</strong>
|
||||||
@@ -200,10 +197,14 @@ 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 (torrent, 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 isDone = file.numPiecesPresent === file.numPieces
|
var isDone = file.numPiecesPresent === file.numPieces
|
||||||
var progress = Math.round(100 * file.numPiecesPresent / (file.numPieces || 0)) + '%'
|
var progress = ''
|
||||||
|
if (torrentSummary.progress) {
|
||||||
|
var fileProg = torrentSummary.progress.files[index]
|
||||||
|
progress = Math.round(100 * fileProg.numPiecesPresent / fileProg.numPieces) + '%'
|
||||||
|
}
|
||||||
|
|
||||||
// Second, render the file as a table row
|
// Second, render the file as a table row
|
||||||
var infoHash = torrentSummary.infoHash
|
var infoHash = torrentSummary.infoHash
|
||||||
|
|||||||
3
renderer/webtorrent.html
Normal file
3
renderer/webtorrent.html
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<script async src="webtorrent.js"></script>
|
||||||
|
<h1>WebTorrent Hidden Window</h1>
|
||||||
298
renderer/webtorrent.js
Normal file
298
renderer/webtorrent.js
Normal file
@@ -0,0 +1,298 @@
|
|||||||
|
// To keep the UI snappy, we run WebTorrent in its own hidden window, a separate
|
||||||
|
// process from the main window.
|
||||||
|
console.time('init')
|
||||||
|
|
||||||
|
var WebTorrent = require('webtorrent')
|
||||||
|
var defaultAnnounceList = require('create-torrent').announceList
|
||||||
|
var deepEqual = require('deep-equal')
|
||||||
|
var electron = require('electron')
|
||||||
|
var fs = require('fs')
|
||||||
|
var mkdirp = require('mkdirp')
|
||||||
|
var musicmetadata = require('musicmetadata')
|
||||||
|
var networkAddress = require('network-address')
|
||||||
|
var config = require('../config')
|
||||||
|
var torrentPoster = require('./lib/torrent-poster')
|
||||||
|
var path = require('path')
|
||||||
|
|
||||||
|
// Send & receive messages from the main window
|
||||||
|
var ipc = electron.ipcRenderer
|
||||||
|
|
||||||
|
// Force use of webtorrent trackers on all torrents
|
||||||
|
global.WEBTORRENT_ANNOUNCE = defaultAnnounceList
|
||||||
|
.map((arr) => arr[0])
|
||||||
|
.filter((url) => url.indexOf('wss://') === 0 || url.indexOf('ws://') === 0)
|
||||||
|
|
||||||
|
// Connect to the WebTorrent and BitTorrent networks. WebTorrent Desktop is a hybrid
|
||||||
|
// client, as explained here: https://webtorrent.io/faq
|
||||||
|
var client = new WebTorrent()
|
||||||
|
|
||||||
|
// WebTorrent-to-HTTP streaming sever
|
||||||
|
var server = null
|
||||||
|
|
||||||
|
// Used for diffing, so we only send progress updates when necessary
|
||||||
|
var prevProgress = null
|
||||||
|
|
||||||
|
init()
|
||||||
|
|
||||||
|
function init () {
|
||||||
|
client.on('warning', (err) => ipc.send('wt-warning', null, err.message))
|
||||||
|
client.on('error', (err) => ipc.send('wt-error', null, err.message))
|
||||||
|
|
||||||
|
ipc.on('wt-start-torrenting', (e, torrentKey, torrentID, path, fileModtimes) =>
|
||||||
|
startTorrenting(torrentKey, torrentID, path, fileModtimes))
|
||||||
|
ipc.on('wt-stop-torrenting', (e, infoHash) =>
|
||||||
|
stopTorrenting(infoHash))
|
||||||
|
ipc.on('wt-create-torrent', (e, torrentKey, options) =>
|
||||||
|
createTorrent(torrentKey, options))
|
||||||
|
ipc.on('wt-save-torrent-file', (e, torrentKey) =>
|
||||||
|
saveTorrentFile(torrentKey))
|
||||||
|
ipc.on('wt-generate-torrent-poster', (e, torrentKey) =>
|
||||||
|
generateTorrentPoster(torrentKey))
|
||||||
|
ipc.on('wt-get-audio-metadata', (e, infoHash, index) =>
|
||||||
|
getAudioMetadata(infoHash, index))
|
||||||
|
ipc.on('wt-start-server', (e, infoHash, index) =>
|
||||||
|
startServer(infoHash, index))
|
||||||
|
ipc.on('wt-stop-server', (e) =>
|
||||||
|
stopServer())
|
||||||
|
|
||||||
|
setInterval(updateTorrentProgress, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Starts a given TorrentID, which can be an infohash, magnet URI, etc. Returns WebTorrent object
|
||||||
|
// See https://github.com/feross/webtorrent/blob/master/docs/api.md#clientaddtorrentid-opts-function-ontorrent-torrent-
|
||||||
|
function startTorrenting (torrentKey, torrentID, path, fileModtimes) {
|
||||||
|
console.log('starting torrent %s: %s', torrentKey, torrentID)
|
||||||
|
var torrent = client.add(torrentID, {
|
||||||
|
path: path,
|
||||||
|
fileModtimes: fileModtimes
|
||||||
|
})
|
||||||
|
torrent.key = torrentKey
|
||||||
|
addTorrentEvents(torrent)
|
||||||
|
return torrent
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopTorrenting (infoHash) {
|
||||||
|
var torrent = client.get(infoHash)
|
||||||
|
torrent.destroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new torrent, start seeding
|
||||||
|
function createTorrent (torrentKey, options) {
|
||||||
|
console.log('creating torrent %s', torrentKey, options)
|
||||||
|
var torrent = client.seed(options.files, options)
|
||||||
|
torrent.key = torrentKey
|
||||||
|
addTorrentEvents(torrent)
|
||||||
|
ipc.send('wt-new-torrent')
|
||||||
|
}
|
||||||
|
|
||||||
|
function addTorrentEvents (torrent) {
|
||||||
|
torrent.on('warning', (err) =>
|
||||||
|
ipc.send('wt-warning', torrent.key, err.message))
|
||||||
|
torrent.on('error', (err) =>
|
||||||
|
ipc.send('wt-error', torrent.key, err.message))
|
||||||
|
torrent.on('infoHash', () =>
|
||||||
|
ipc.send('wt-infohash', torrent.key, torrent.infoHash))
|
||||||
|
torrent.on('ready', torrentReady)
|
||||||
|
torrent.on('done', torrentDone)
|
||||||
|
|
||||||
|
function torrentReady () {
|
||||||
|
var info = getTorrentInfo(torrent)
|
||||||
|
ipc.send('wt-ready', torrent.key, info)
|
||||||
|
ipc.send('wt-ready-' + torrent.infoHash, torrent.key, info)
|
||||||
|
|
||||||
|
updateTorrentProgress()
|
||||||
|
}
|
||||||
|
|
||||||
|
function torrentDone () {
|
||||||
|
var info = getTorrentInfo(torrent)
|
||||||
|
ipc.send('wt-done', torrent.key, info)
|
||||||
|
|
||||||
|
updateTorrentProgress()
|
||||||
|
|
||||||
|
torrent.getFileModtimes(function (err, fileModtimes) {
|
||||||
|
if (err) return onError(err)
|
||||||
|
ipc.send('wt-file-modtimes', torrent.key, fileModtimes)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Produces a JSON saveable summary of a torrent
|
||||||
|
function getTorrentInfo (torrent) {
|
||||||
|
return {
|
||||||
|
infoHash: torrent.infoHash,
|
||||||
|
magnetURI: torrent.magnetURI,
|
||||||
|
name: torrent.name,
|
||||||
|
path: torrent.path,
|
||||||
|
files: torrent.files.map(getTorrentFileInfo),
|
||||||
|
bytesReceived: torrent.received
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Produces a JSON saveable summary of a file in a torrent
|
||||||
|
function getTorrentFileInfo (file) {
|
||||||
|
return {
|
||||||
|
name: file.name,
|
||||||
|
length: file.length,
|
||||||
|
path: file.path,
|
||||||
|
numPiecesPresent: 0,
|
||||||
|
numPieces: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Every time we resolve a magnet URI, save the torrent file so that we never
|
||||||
|
// have to download it again. Never ask the DHT the same question twice.
|
||||||
|
function saveTorrentFile (torrentKey) {
|
||||||
|
var torrent = getTorrent(torrentKey)
|
||||||
|
checkIfTorrentFileExists(torrent.infoHash, function (torrentPath, exists) {
|
||||||
|
if (exists) {
|
||||||
|
// We've already saved the file
|
||||||
|
return ipc.send('wt-file-saved', torrentKey, torrentPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, save the .torrent file, under the app config folder
|
||||||
|
fs.mkdir(config.CONFIG_TORRENT_PATH, function (_) {
|
||||||
|
fs.writeFile(torrentPath, torrent.torrentFile, function (err) {
|
||||||
|
if (err) return console.log('error saving torrent file %s: %o', torrentPath, err)
|
||||||
|
console.log('saved torrent file %s', torrentPath)
|
||||||
|
return ipc.send('wt-file-saved', torrentKey, torrentPath)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checks whether we've already resolved a given infohash to a torrent file
|
||||||
|
// Calls back with (torrentPath, exists). Logs, does not call back on error
|
||||||
|
function checkIfTorrentFileExists (infoHash, cb) {
|
||||||
|
var torrentPath = path.join(config.CONFIG_TORRENT_PATH, infoHash + '.torrent')
|
||||||
|
fs.exists(torrentPath, function (exists) {
|
||||||
|
cb(torrentPath, exists)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save a JPG that represents a torrent.
|
||||||
|
// Auto chooses either a frame from a video file, an image, etc
|
||||||
|
function generateTorrentPoster (torrentKey) {
|
||||||
|
var torrent = getTorrent(torrentKey)
|
||||||
|
torrentPoster(torrent, function (err, buf, extension) {
|
||||||
|
if (err) return console.log('error generating poster: %o', err)
|
||||||
|
// save it for next time
|
||||||
|
mkdirp(config.CONFIG_POSTER_PATH, function (err) {
|
||||||
|
if (err) return console.log('error creating poster dir: %o', err)
|
||||||
|
var posterFilePath = path.join(config.CONFIG_POSTER_PATH, torrent.infoHash + extension)
|
||||||
|
fs.writeFile(posterFilePath, buf, function (err) {
|
||||||
|
if (err) return console.log('error saving poster: %o', err)
|
||||||
|
// show the poster
|
||||||
|
ipc.send('wt-poster', torrentKey, posterFilePath)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateTorrentProgress () {
|
||||||
|
var progress = getTorrentProgress()
|
||||||
|
// TODO: diff torrent-by-torrent, not once for the whole update
|
||||||
|
if (prevProgress && deepEqual(progress, prevProgress, {strict: true})) {
|
||||||
|
return /* don't send heavy object if it hasn't changed */
|
||||||
|
}
|
||||||
|
ipc.send('wt-progress', progress)
|
||||||
|
prevProgress = progress
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTorrentProgress () {
|
||||||
|
// First, track overall progress
|
||||||
|
var progress = client.progress
|
||||||
|
var hasActiveTorrents = client.torrents.some(function (torrent) {
|
||||||
|
return torrent.progress !== 1
|
||||||
|
})
|
||||||
|
|
||||||
|
// Track progress for every file in each torrent
|
||||||
|
// TODO: ideally this would be tracked by WebTorrent, which could do it
|
||||||
|
// more efficiently than looping over torrent.bitfield
|
||||||
|
var torrentProg = client.torrents.map(function (torrent) {
|
||||||
|
var fileProg = torrent.files && torrent.files.map(function (file, index) {
|
||||||
|
var numPieces = file._endPiece - file._startPiece + 1
|
||||||
|
var numPiecesPresent = 0
|
||||||
|
for (var piece = file._startPiece; piece <= file._endPiece; piece++) {
|
||||||
|
if (torrent.bitfield.get(piece)) numPiecesPresent++
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
startPiece: file._startPiece,
|
||||||
|
endPiece: file._endPiece,
|
||||||
|
numPieces,
|
||||||
|
numPiecesPresent
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
torrentKey: torrent.key,
|
||||||
|
ready: torrent.ready,
|
||||||
|
progress: torrent.progress,
|
||||||
|
downloaded: torrent.downloaded,
|
||||||
|
downloadSpeed: torrent.downloadSpeed,
|
||||||
|
uploadSpeed: torrent.uploadSpeed,
|
||||||
|
numPeers: torrent.numPeers,
|
||||||
|
length: torrent.length,
|
||||||
|
bitfield: torrent.bitfield,
|
||||||
|
files: fileProg
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
torrents: torrentProg,
|
||||||
|
progress,
|
||||||
|
hasActiveTorrents
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function startServer (infoHash, index) {
|
||||||
|
if (server) return
|
||||||
|
var torrent = client.get(infoHash)
|
||||||
|
if (torrent.ready) startServerFromReadyTorrent(torrent, index)
|
||||||
|
else torrent.on('ready', () => startServerFromReadyTorrent(torrent, index))
|
||||||
|
}
|
||||||
|
|
||||||
|
function startServerFromReadyTorrent (torrent, index, cb) {
|
||||||
|
if (server) return
|
||||||
|
|
||||||
|
// start the streaming torrent-to-http server
|
||||||
|
server = torrent.createServer()
|
||||||
|
server.listen(0, function () {
|
||||||
|
var port = server.address().port
|
||||||
|
var urlSuffix = ':' + port + '/' + index
|
||||||
|
var info = {
|
||||||
|
torrentKey: torrent.key,
|
||||||
|
localURL: 'http://localhost' + urlSuffix,
|
||||||
|
networkURL: 'http://' + networkAddress() + urlSuffix
|
||||||
|
}
|
||||||
|
|
||||||
|
ipc.send('wt-server-running', info)
|
||||||
|
ipc.send('wt-server-' + torrent.infoHash, info)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopServer () {
|
||||||
|
if (!server) return
|
||||||
|
server.destroy()
|
||||||
|
server = null
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAudioMetadata (infoHash, index) {
|
||||||
|
var torrent = client.get(infoHash)
|
||||||
|
var file = torrent.files[index]
|
||||||
|
musicmetadata(file.createReadStream(), function (err, info) {
|
||||||
|
if (err) return
|
||||||
|
console.log('got audio metadata for %s: %o', file.name, info)
|
||||||
|
ipc.send('wt-audio-metadata', infoHash, index, info)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gets a WebTorrent handle by torrentKey
|
||||||
|
// Throws an Error if we're not currently torrenting anything w/ that key
|
||||||
|
function getTorrent (torrentKey) {
|
||||||
|
var ret = client.torrents.find((x) => x.key === torrentKey)
|
||||||
|
if (!ret) throw new Error('missing torrent key ' + torrentKey)
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
function onError (err) {
|
||||||
|
console.log(err)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user