From 09d6fa550adce28187aa86ee625935f383e90e64 Mon Sep 17 00:00:00 2001 From: DC Date: Fri, 12 Aug 2016 09:31:28 -0700 Subject: [PATCH 1/3] Handle Save Torrent File As... -> Cancel --- src/renderer/controllers/torrent-list-controller.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/renderer/controllers/torrent-list-controller.js b/src/renderer/controllers/torrent-list-controller.js index 5d5a8987..bb39ce97 100644 --- a/src/renderer/controllers/torrent-list-controller.js +++ b/src/renderer/controllers/torrent-list-controller.js @@ -274,6 +274,7 @@ function saveTorrentFileAs (torrentSummary) { ] } electron.remote.dialog.showSaveDialog(electron.remote.getCurrentWindow(), opts, function (savePath) { + if (!savePath) return // They clicked Cancel var torrentPath = TorrentSummary.getTorrentPath(torrentSummary) fs.readFile(torrentPath, function (err, torrentFile) { if (err) return dispatch('error', err) From 1ec305162ef29c8925cf23c4de9f6e7d76448b2b Mon Sep 17 00:00:00 2001 From: DC Date: Fri, 12 Aug 2016 20:54:57 -0700 Subject: [PATCH 2/3] Check for missing download path Fixes #646 --- src/renderer/controllers/prefs-controller.js | 2 ++ src/renderer/main.js | 27 ++++++++++++++--- src/renderer/views/preferences.js | 4 +-- src/renderer/views/torrent-list.js | 32 ++++++++++++++++---- static/main.css | 7 +++++ 5 files changed, 59 insertions(+), 13 deletions(-) diff --git a/src/renderer/controllers/prefs-controller.js b/src/renderer/controllers/prefs-controller.js index d220c2b1..97171c8a 100644 --- a/src/renderer/controllers/prefs-controller.js +++ b/src/renderer/controllers/prefs-controller.js @@ -1,4 +1,5 @@ const State = require('../lib/state') +const {dispatch} = require('../lib/dispatcher') const ipcRenderer = require('electron').ipcRenderer // Controls the Preferences screen @@ -50,5 +51,6 @@ module.exports = class PrefsController { } state.saved.prefs = Object.assign(state.saved.prefs || {}, state.unsaved.prefs) State.save(state) + dispatch('checkDownloadPath') } } diff --git a/src/renderer/main.js b/src/renderer/main.js index 181a7e63..bfb1c4f0 100644 --- a/src/renderer/main.js +++ b/src/renderer/main.js @@ -7,6 +7,7 @@ const dragDrop = require('drag-drop') const electron = require('electron') const React = require('react') const ReactDOM = require('react-dom') +const fs = require('fs') const config = require('../config') const App = require('./views/app') @@ -74,6 +75,12 @@ function onState (err, _state) { } }) + // Calling update() updates the UI given the current state + // 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 + setInterval(update, 1000) + app = ReactDOM.render(, document.querySelector('#body')) + // Restart everything we were torrenting last time the app ran resumeTorrents() @@ -83,11 +90,8 @@ function onState (err, _state) { // Listen for messages from the main process setupIpc() - // Calling update() updates the UI given the current state - // 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 - setInterval(update, 1000) - app = ReactDOM.render(, document.querySelector('#body')) + // Warn if the download dir is gone, eg b/c an external drive is unplugged + checkDownloadPath() // OS integrations: // ...drag and drop files/text to start torrenting or seeding @@ -212,6 +216,7 @@ const dispatchHandlers = { // Preferences screen 'preferences': () => controllers.prefs.show(), 'updatePreferences': (key, value) => controllers.prefs.update(key, value), + 'checkDownloadPath': checkDownloadPath, // Update (check for new versions on Linux, where there's no auto updater) 'updateAvailable': (version) => controllers.update.updateAvailable(version), @@ -433,3 +438,15 @@ function onFullscreenChanged (e, isFullScreen) { update() } + +function checkDownloadPath () { + state.downloadPathStatus = undefined + fs.stat(state.saved.prefs.downloadPath, function (err, stat) { + if (err) { + state.downloadPathStatus = 'missing' + return console.error(err) + } + if (stat.isDirectory()) state.downloadPathStatus = 'ok' + else state.downloadPathStatus = 'missing' + }) +} diff --git a/src/renderer/views/preferences.js b/src/renderer/views/preferences.js index a751e0d8..c4d2feba 100644 --- a/src/renderer/views/preferences.js +++ b/src/renderer/views/preferences.js @@ -22,12 +22,12 @@ function renderGeneralSection (state) { description: '', icon: 'settings' }, [ - renderDownloadDirSelector(state), + renderDownloadPathSelector(state), renderFileHandlers(state) ]) } -function renderDownloadDirSelector (state) { +function renderDownloadPathSelector (state) { return renderFileSelector({ key: 'download-path', label: 'Download Path', diff --git a/src/renderer/views/torrent-list.js b/src/renderer/views/torrent-list.js index 4b85d2dd..a4849906 100644 --- a/src/renderer/views/torrent-list.js +++ b/src/renderer/views/torrent-list.js @@ -8,16 +8,36 @@ const {dispatcher} = require('../lib/dispatcher') module.exports = class TorrentList extends React.Component { render () { var state = this.props.state - var torrentRows = state.saved.torrents.map( - (torrentSummary) => this.renderTorrent(torrentSummary) - ) - return ( -
- {torrentRows} + var contents + if (!state.downloadPathStatus) { + contents = '' + } else if (state.downloadPathStatus === 'missing') { + contents = ( +
+

Download path missing: {state.saved.prefs.downloadPath}

+

Check that all drives are connected?

+

Alternatively, choose a new download path in + Preferences +

+
+ ) + } else if (state.downloadPathStatus === 'ok') { + contents = state.saved.torrents.map( + (torrentSummary) => this.renderTorrent(torrentSummary) + ) + contents.push(
Drop a torrent file here or paste a magnet link
+ ) + } else { + throw new Error('Unhandled downloadPathStatus ' + state.downloadPathStatus) + } + + return ( +
+ {contents}
) } diff --git a/static/main.css b/static/main.css index d56c9838..902c4c4e 100644 --- a/static/main.css +++ b/static/main.css @@ -551,6 +551,13 @@ input[type='text'] { line-height: 1.5em; } +/* + * TORRENT LIST: ERRORS + */ +.torrent-list p { + padding: 5px 20px; +} + /* * TORRENT LIST: DRAG-DROP TARGET */ From 0809e20a6e1de917a2f8315c2f1da3b9ef177a43 Mon Sep 17 00:00:00 2001 From: DC Date: Sat, 13 Aug 2016 20:28:20 -0700 Subject: [PATCH 3/3] Check path for each torrent --- .../controllers/playback-controller.js | 2 +- .../controllers/torrent-list-controller.js | 28 +++-- src/renderer/lib/state.js | 3 + src/renderer/main.js | 20 ++-- src/renderer/views/torrent-list.js | 107 ++++++++++++------ static/main.css | 8 +- 6 files changed, 110 insertions(+), 58 deletions(-) diff --git a/src/renderer/controllers/playback-controller.js b/src/renderer/controllers/playback-controller.js index c80489af..301354d4 100644 --- a/src/renderer/controllers/playback-controller.js +++ b/src/renderer/controllers/playback-controller.js @@ -188,7 +188,7 @@ module.exports = class PlaybackController { }, 10000) /* give it a few seconds */ if (torrentSummary.status === 'paused') { - dispatch('startTorrentingSummary', torrentSummary) + dispatch('startTorrentingSummary', torrentSummary.torrentKey) ipcRenderer.once('wt-ready-' + torrentSummary.infoHash, () => this.openPlayerFromActiveTorrent(torrentSummary, index, timeout, cb)) } else { diff --git a/src/renderer/controllers/torrent-list-controller.js b/src/renderer/controllers/torrent-list-controller.js index bb39ce97..647ddf15 100644 --- a/src/renderer/controllers/torrent-list-controller.js +++ b/src/renderer/controllers/torrent-list-controller.js @@ -76,21 +76,25 @@ module.exports = class TorrentListController { } // Starts downloading and/or seeding a given torrentSummary. - startTorrentingSummary (torrentSummary) { - var s = torrentSummary - - // Backward compatibility for config files save before we had torrentKey - if (!s.torrentKey) s.torrentKey = this.state.nextTorrentKey++ + startTorrentingSummary (torrentKey) { + var s = TorrentSummary.getByKey(this.state, torrentKey) + if (!s) throw new Error('Missing key: ' + torrentKey) // Use Downloads folder by default if (!s.path) s.path = this.state.saved.prefs.downloadPath - ipcRenderer.send('wt-start-torrenting', - s.torrentKey, - TorrentSummary.getTorrentID(s), - s.path, - s.fileModtimes, - s.selections) + fs.stat(TorrentSummary.getFileOrFolder(s), function (err) { + if (err) { + s.error = 'path-missing' + return + } + ipcRenderer.send('wt-start-torrenting', + s.torrentKey, + TorrentSummary.getTorrentID(s), + s.path, + s.fileModtimes, + s.selections) + }) } // TODO: use torrentKey, not infoHash @@ -98,7 +102,7 @@ module.exports = class TorrentListController { var torrentSummary = TorrentSummary.getByKey(this.state, infoHash) if (torrentSummary.status === 'paused') { torrentSummary.status = 'new' - this.startTorrentingSummary(torrentSummary) + this.startTorrentingSummary(torrentSummary.torrentKey) sound.play('ENABLE') } else { torrentSummary.status = 'paused' diff --git a/src/renderer/lib/state.js b/src/renderer/lib/state.js index 3205e1df..04350337 100644 --- a/src/renderer/lib/state.js +++ b/src/renderer/lib/state.js @@ -200,6 +200,9 @@ function save (state, cb) { if (key === 'playStatus') { continue // Don't save whether a torrent is playing / pending } + if (key === 'error') { + continue // Don't save error states + } torrent[key] = x[key] } return torrent diff --git a/src/renderer/main.js b/src/renderer/main.js index bfb1c4f0..15fd2183 100644 --- a/src/renderer/main.js +++ b/src/renderer/main.js @@ -75,15 +75,15 @@ function onState (err, _state) { } }) + // Restart everything we were torrenting last time the app ran + resumeTorrents() + // Calling update() updates the UI given the current state // 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 setInterval(update, 1000) app = ReactDOM.render(, document.querySelector('#body')) - // Restart everything we were torrenting last time the app ran - resumeTorrents() - // Lazy-load other stuff, like the AppleTV module, later to keep startup fast window.setTimeout(delayedInit, config.DELAYED_INIT) @@ -179,8 +179,7 @@ const dispatchHandlers = { 'deleteTorrent': (infoHash, deleteData) => controllers.torrentList.deleteTorrent(infoHash, deleteData), 'toggleSelectTorrent': (infoHash) => controllers.torrentList.toggleSelectTorrent(infoHash), 'openTorrentContextMenu': (infoHash) => controllers.torrentList.openTorrentContextMenu(infoHash), - 'startTorrentingSummary': (torrentSummary) => - controllers.torrentList.startTorrentingSummary(torrentSummary), + 'startTorrentingSummary': (torrentKey) => controllers.torrentList.startTorrentingSummary(torrentKey), // Playback 'playFile': (infoHash, index) => controllers.playback.playFile(infoHash, index), @@ -318,8 +317,14 @@ function escapeBack () { // Starts all torrents that aren't paused on program startup function resumeTorrents () { state.saved.torrents - .filter((torrentSummary) => torrentSummary.status !== 'paused') - .forEach((torrentSummary) => controllers.torrentList.startTorrentingSummary(torrentSummary)) + .map((torrentSummary) => { + // Torrent keys are ephemeral, reassigned each time the app runs. + // On startup, give all torrents a key, even the ones that are paused. + torrentSummary.torrentKey = state.nextTorrentKey++ + return torrentSummary + }) + .filter((s) => s.status !== 'paused') + .forEach((s) => controllers.torrentList.startTorrentingSummary(s.torrentKey)) } // Set window dimensions to match video dimensions or fill the screen @@ -440,7 +445,6 @@ function onFullscreenChanged (e, isFullScreen) { } function checkDownloadPath () { - state.downloadPathStatus = undefined fs.stat(state.saved.prefs.downloadPath, function (err, stat) { if (err) { state.downloadPathStatus = 'missing' diff --git a/src/renderer/views/torrent-list.js b/src/renderer/views/torrent-list.js index a4849906..50cf8bfe 100644 --- a/src/renderer/views/torrent-list.js +++ b/src/renderer/views/torrent-list.js @@ -9,31 +9,27 @@ module.exports = class TorrentList extends React.Component { render () { var state = this.props.state - var contents - if (!state.downloadPathStatus) { - contents = '' - } else if (state.downloadPathStatus === 'missing') { - contents = ( -
+ var contents = [] + if (state.downloadPathStatus === 'missing') { + contents.push( +

Download path missing: {state.saved.prefs.downloadPath}

Check that all drives are connected?

-

Alternatively, choose a new download path in - Preferences +

Alternatively, choose a new download path + in Preferences

) - } else if (state.downloadPathStatus === 'ok') { - contents = state.saved.torrents.map( - (torrentSummary) => this.renderTorrent(torrentSummary) - ) - contents.push( -
- Drop a torrent file here or paste a magnet link -
- ) - } else { - throw new Error('Unhandled downloadPathStatus ' + state.downloadPathStatus) } + var torrentElems = state.saved.torrents.map( + (torrentSummary) => this.renderTorrent(torrentSummary) + ) + contents.push(...torrentElems) + contents.push( +
+ Drop a torrent file here or paste a magnet link +
+ ) return (
@@ -64,7 +60,7 @@ module.exports = class TorrentList extends React.Component { if (torrentSummary.playStatus) classes.push(torrentSummary.playStatus) if (isSelected) classes.push('selected') if (!infoHash) classes.push('disabled') - if (torrentSummary.torrrentKey) console.error('Missing torrentKey', torrentSummary) + if (!torrentSummary.torrentKey) throw new Error('Missing torrentKey') return (
+ {getErrorMessage(torrentSummary)} +
+ ) + } else if (torrentSummary.status !== 'paused' && prog) { + elements.push(
{renderPercentProgress()} {renderTotalProgress()} @@ -98,7 +100,7 @@ module.exports = class TorrentList extends React.Component { {renderUploadSpeed()} {renderEta()}
- )) + ) } return (
{elements}
) @@ -195,8 +197,9 @@ module.exports = class TorrentList extends React.Component { } // Only show the play button for torrents that contain playable media - var playButton - if (TorrentPlayer.isPlayableTorrentSummary(torrentSummary)) { + var playButton, downloadButton + var noErrors = !torrentSummary.error + if (noErrors && TorrentPlayer.isPlayableTorrentSummary(torrentSummary)) { playButton = ( ) } - - return ( -
- {positionElem} - {playButton} + if (noErrors) { + downloadButton = ( {downloadIcon} + ) + } + + return ( +
+ {positionElem} + {playButton} + {downloadButton} {message}
) + if (torrentSummary.error || !torrentSummary.files) { + var message = '' + if (torrentSummary.error === 'path-missing') { + // Special case error: this torrent's download dir or file is missing + message = 'Missing path: ' + TorrentSummary.getFileOrFolder(torrentSummary) + } else if (torrentSummary.error) { + // General error for this torrent: just show the message + message = torrentSummary.error.message || torrentSummary.error + } else if (torrentSummary.status === 'paused') { + // No file info, no infohash, and we're not trying to download from the DHT + message = 'Failed to load torrent info. Click the download button to try again...' + } else { + // No file info, no infohash, trying to load from the DHT + message = 'Downloading torrent info...' + } + filesElement = ( +
+ {message} +
+ ) } else { // We do know the files. List them and show download stats for each one var fileRows = torrentSummary.files @@ -349,3 +371,16 @@ module.exports = class TorrentList extends React.Component { ) } } + +function getErrorMessage (torrentSummary) { + var err = torrentSummary.error + if (err === 'path-missing') { + return ( + + Path missing.
+ Fix and restart the app, or delete the torrent. +
+ ) + } + return 'Error' +} diff --git a/static/main.css b/static/main.css index 902c4c4e..a2b1a8b7 100644 --- a/static/main.css +++ b/static/main.css @@ -554,8 +554,14 @@ input[type='text'] { /* * TORRENT LIST: ERRORS */ + .torrent-list p { - padding: 5px 20px; + margin: 10px 20px; +} + +.torrent-list a { + color: #99f; + text-decoration: none; } /*