From 6518a1535ce17693b2ccd63e4f2a51bb7df4c739 Mon Sep 17 00:00:00 2001 From: DC Date: Wed, 11 May 2016 05:42:06 -0700 Subject: [PATCH] Allow selecting individual files to torrent Saves bandwidth and disk space when a torrent contains extra files you don't need Fixes #360 --- renderer/index.css | 25 ++++++++++++---- renderer/index.js | 21 ++++++++++++- renderer/views/torrent-list.js | 31 +++++++++++++------ renderer/webtorrent.js | 55 ++++++++++++++++++++++++++++++++-- 4 files changed, 113 insertions(+), 19 deletions(-) diff --git a/renderer/index.css b/renderer/index.css index b3424bef..23e51720 100644 --- a/renderer/index.css +++ b/renderer/index.css @@ -113,6 +113,10 @@ table { opacity: 0.3; } +.clickable { + cursor: pointer; +} + .float-right { float: right; } @@ -597,7 +601,7 @@ body.drag .app::after { } .torrent-details { - padding: 8em 20px 20px 20px; + padding: 8em 12px 20px 20px; } .torrent-details table { @@ -611,6 +615,10 @@ body.drag .app::after { height: 28px; } +.torrent-details td { + vertical-align: center; +} + .torrent-details tr:hover { background-color: rgba(200, 200, 200, 0.3); } @@ -621,16 +629,16 @@ body.drag .app::after { vertical-align: bottom; } -.torrent-details td.col-icon { - width: 2em; -} - -.torrent-details td.col-icon .icon { +.torrent-details td .icon { font-size: 18px; position: relative; top: 3px; } +.torrent-details td.col-icon { + width: 2em; +} + .torrent-details td.col-name { width: auto; text-overflow: ellipsis; @@ -646,6 +654,11 @@ body.drag .app::after { text-align: right; } +.torrent-details td.col-select { + width: 2em; + text-align: right; +} + /* * PLAYER */ diff --git a/renderer/index.js b/renderer/index.js index 4e05c997..56ae762a 100644 --- a/renderer/index.js +++ b/renderer/index.js @@ -153,6 +153,11 @@ function cleanUpConfig () { delete ts.posterURL ts.posterFileName = infoHash + extension } + + // Migration: add per-file selections + if (!ts.selections) { + ts.selections = ts.files.map((x) => true) + } }) } @@ -231,6 +236,9 @@ function dispatch (action, ...args) { if (action === 'toggleSelectTorrent') { toggleSelectTorrent(args[0] /* infoHash */) } + if (action === 'toggleTorrentFile') { + toggleTorrentFile(args[0] /* infoHash */, args[1] /* index */) + } if (action === 'openTorrentContextMenu') { openTorrentContextMenu(args[0] /* infoHash */) } @@ -709,7 +717,7 @@ function startTorrentingSummary (torrentSummary) { } console.log('start torrenting %s %s', s.torrentKey, torrentID) - ipcRenderer.send('wt-start-torrenting', s.torrentKey, torrentID, path, s.fileModtimes) + ipcRenderer.send('wt-start-torrenting', s.torrentKey, torrentID, path, s.fileModtimes, s.selections) } // @@ -818,6 +826,9 @@ function torrentMetadata (torrentKey, torrentInfo) { torrentSummary.path = torrentInfo.path torrentSummary.files = torrentInfo.files torrentSummary.magnetURI = torrentInfo.magnetURI + if (!torrentSummary.selections) { + torrentSummary.selections = torrentSummary.files.map((x) => true) + } update() // Save the .torrent file, if it hasn't been saved already @@ -1058,6 +1069,14 @@ function toggleSelectTorrent (infoHash) { update() } +function toggleTorrentFile (infoHash, index) { + var torrentSummary = getTorrentSummary(infoHash) + torrentSummary.selections[index] = !torrentSummary.selections[index] + + // Let the WebTorrent process know to start or stop fetching that file + ipcRenderer.send('wt-select-files', infoHash, torrentSummary.selections) +} + function openTorrentContextMenu (infoHash) { var torrentSummary = getTorrentSummary(infoHash) var menu = new electron.remote.Menu() diff --git a/renderer/views/torrent-list.js b/renderer/views/torrent-list.js index 615fc92f..4cfcff00 100644 --- a/renderer/views/torrent-list.js +++ b/renderer/views/torrent-list.js @@ -208,7 +208,8 @@ function TorrentList (state) { // Show a single torrentSummary file in the details view for a single torrent function renderFileRow (torrentSummary, file, index) { // First, find out how much of the file we've downloaded - var isDone = false + var isSelected = torrentSummary.selections[index] // Are we even torrenting it? + var isDone = false // Are we finished torrenting it? var progress = '' if (torrentSummary.progress && torrentSummary.progress.files) { var fileProg = torrentSummary.progress.files[index] @@ -217,26 +218,38 @@ function TorrentList (state) { } // Second, render the file as a table row + var isPlayable = TorrentPlayer.isPlayable(file) var infoHash = torrentSummary.infoHash var icon - var rowClass = '' var handleClick - if (TorrentPlayer.isPlayable(file)) { + if (isPlayable) { icon = 'play_arrow' /* playable? add option to play */ handleClick = dispatcher('play', infoHash, index) } else { icon = 'description' /* file icon, opens in OS default app */ - rowClass = isDone ? '' : 'disabled' handleClick = dispatcher('openFile', infoHash, index) } + var rowClass = 'clickable' + if (!isSelected) rowClass = 'disabled' // File deselected, not being torrented + if (!isDone && !isPlayable) rowClass = 'disabled' // Can't open yet, can't stream return hx` - - + + ${icon} - ${file.name} - ${progress} - ${prettyBytes(file.length)} + + ${file.name} + + + ${isSelected ? progress : ''} + + + ${prettyBytes(file.length)} + + + ${isSelected ? 'close' : 'add'} + ` } diff --git a/renderer/webtorrent.js b/renderer/webtorrent.js index c9d24723..19612fed 100644 --- a/renderer/webtorrent.js +++ b/renderer/webtorrent.js @@ -49,8 +49,8 @@ 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-start-torrenting', (e, torrentKey, torrentID, path, fileModtimes, selections) => + startTorrenting(torrentKey, torrentID, path, fileModtimes, selections)) ipc.on('wt-stop-torrenting', (e, infoHash) => stopTorrenting(infoHash)) ipc.on('wt-create-torrent', (e, torrentKey, options) => @@ -65,6 +65,8 @@ function init () { startServer(infoHash, index)) ipc.on('wt-stop-server', (e) => stopServer()) + ipc.on('wt-select-files', (e, infoHash, selections) => + selectFiles(infoHash, selections)) ipc.send('ipcReadyWebTorrent') @@ -73,7 +75,7 @@ function init () { // 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) { +function startTorrenting (torrentKey, torrentID, path, fileModtimes, selections) { console.log('starting torrent %s: %s', torrentKey, torrentID) var torrent = client.add(torrentID, { @@ -81,8 +83,13 @@ function startTorrenting (torrentKey, torrentID, path, fileModtimes) { fileModtimes: fileModtimes }) torrent.key = torrentKey + + // Listen for ready event, progress notifications, etc addTorrentEvents(torrent) + // Only download the files the user wants, not necessarily all files + torrent.once('ready', () => selectFiles(torrent, selections)) + return torrent } @@ -308,6 +315,48 @@ function getAudioMetadata (infoHash, index) { }) } +function selectFiles (torrentOrInfoHash, selections) { + // Get the torrent object + var torrent + if (typeof torrentOrInfoHash === 'string') { + torrent = client.get(torrentOrInfoHash) + } else { + torrent = torrentOrInfoHash + } + + // Selections not specified? + // Load all files. We still need to replace the default whole-torrent + // selection with individual selections for each file, so we can + // select/deselect files later on + if (!selections) { + selections = torrent.files.map((x) => true) + } + + // Selections specified incorrectly? + if (selections.length !== torrent.files.length) { + throw new Error('got ' + selections.length + ' file selections, ' + + 'but the torrent contains ' + torrent.files.length + ' files') + } + + // Remove default selection (whole torrent) + torrent.deselect(0, torrent.pieces.length - 1, false) + + // Add selections (individual files) + for (var i = 0; i < selections.length; i++) { + var file = torrent.files[i] + if (selections[i]) { + file.select() + } else { + console.log('deselecting file ' + i + ' of torrent ' + torrent.name) + file.deselect() + + // If we deselected a file, try to nuke it to save disk space + var filePath = path.join(torrent.path, file.path) + fs.unlink(filePath) // Ignore errors for now + } + } +} + // Gets a WebTorrent handle by torrentKey // Throws an Error if we're not currently torrenting anything w/ that key function getTorrent (torrentKey) {