Allow selecting individual files to torrent

Saves bandwidth and disk space when a torrent contains extra files you don't need

Fixes #360
This commit is contained in:
DC
2016-05-11 05:42:06 -07:00
parent 0095687bf5
commit 6518a1535c
4 changed files with 113 additions and 19 deletions

View File

@@ -113,6 +113,10 @@ table {
opacity: 0.3; opacity: 0.3;
} }
.clickable {
cursor: pointer;
}
.float-right { .float-right {
float: right; float: right;
} }
@@ -597,7 +601,7 @@ body.drag .app::after {
} }
.torrent-details { .torrent-details {
padding: 8em 20px 20px 20px; padding: 8em 12px 20px 20px;
} }
.torrent-details table { .torrent-details table {
@@ -611,6 +615,10 @@ body.drag .app::after {
height: 28px; height: 28px;
} }
.torrent-details td {
vertical-align: center;
}
.torrent-details tr:hover { .torrent-details tr:hover {
background-color: rgba(200, 200, 200, 0.3); background-color: rgba(200, 200, 200, 0.3);
} }
@@ -621,16 +629,16 @@ body.drag .app::after {
vertical-align: bottom; vertical-align: bottom;
} }
.torrent-details td.col-icon { .torrent-details td .icon {
width: 2em;
}
.torrent-details td.col-icon .icon {
font-size: 18px; font-size: 18px;
position: relative; position: relative;
top: 3px; top: 3px;
} }
.torrent-details td.col-icon {
width: 2em;
}
.torrent-details td.col-name { .torrent-details td.col-name {
width: auto; width: auto;
text-overflow: ellipsis; text-overflow: ellipsis;
@@ -646,6 +654,11 @@ body.drag .app::after {
text-align: right; text-align: right;
} }
.torrent-details td.col-select {
width: 2em;
text-align: right;
}
/* /*
* PLAYER * PLAYER
*/ */

View File

@@ -153,6 +153,11 @@ function cleanUpConfig () {
delete ts.posterURL delete ts.posterURL
ts.posterFileName = infoHash + extension 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') { if (action === 'toggleSelectTorrent') {
toggleSelectTorrent(args[0] /* infoHash */) toggleSelectTorrent(args[0] /* infoHash */)
} }
if (action === 'toggleTorrentFile') {
toggleTorrentFile(args[0] /* infoHash */, args[1] /* index */)
}
if (action === 'openTorrentContextMenu') { if (action === 'openTorrentContextMenu') {
openTorrentContextMenu(args[0] /* infoHash */) openTorrentContextMenu(args[0] /* infoHash */)
} }
@@ -709,7 +717,7 @@ function startTorrentingSummary (torrentSummary) {
} }
console.log('start torrenting %s %s', s.torrentKey, torrentID) 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.path = torrentInfo.path
torrentSummary.files = torrentInfo.files torrentSummary.files = torrentInfo.files
torrentSummary.magnetURI = torrentInfo.magnetURI torrentSummary.magnetURI = torrentInfo.magnetURI
if (!torrentSummary.selections) {
torrentSummary.selections = torrentSummary.files.map((x) => true)
}
update() update()
// Save the .torrent file, if it hasn't been saved already // Save the .torrent file, if it hasn't been saved already
@@ -1058,6 +1069,14 @@ function toggleSelectTorrent (infoHash) {
update() 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) { function openTorrentContextMenu (infoHash) {
var torrentSummary = getTorrentSummary(infoHash) var torrentSummary = getTorrentSummary(infoHash)
var menu = new electron.remote.Menu() var menu = new electron.remote.Menu()

View File

@@ -208,7 +208,8 @@ function TorrentList (state) {
// Show a single torrentSummary file in the details view for a single torrent // Show a single torrentSummary file in the details view for a single torrent
function renderFileRow (torrentSummary, file, index) { function renderFileRow (torrentSummary, file, index) {
// First, find out how much of the file we've downloaded // First, find out how much of the file we've downloaded
var isDone = false var isSelected = torrentSummary.selections[index] // Are we even torrenting it?
var isDone = false // Are we finished torrenting it?
var progress = '' var progress = ''
if (torrentSummary.progress && torrentSummary.progress.files) { if (torrentSummary.progress && torrentSummary.progress.files) {
var fileProg = torrentSummary.progress.files[index] var fileProg = torrentSummary.progress.files[index]
@@ -217,26 +218,38 @@ function TorrentList (state) {
} }
// Second, render the file as a table row // Second, render the file as a table row
var isPlayable = TorrentPlayer.isPlayable(file)
var infoHash = torrentSummary.infoHash var infoHash = torrentSummary.infoHash
var icon var icon
var rowClass = ''
var handleClick var handleClick
if (TorrentPlayer.isPlayable(file)) { if (isPlayable) {
icon = 'play_arrow' /* playable? add option to play */ icon = 'play_arrow' /* playable? add option to play */
handleClick = dispatcher('play', infoHash, index) handleClick = dispatcher('play', infoHash, index)
} else { } else {
icon = 'description' /* file icon, opens in OS default app */ icon = 'description' /* file icon, opens in OS default app */
rowClass = isDone ? '' : 'disabled'
handleClick = dispatcher('openFile', infoHash, index) 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` return hx`
<tr onclick=${handleClick} class='${rowClass}'> <tr>
<td class='col-icon'> <td class='col-icon ${rowClass}' onclick=${handleClick}>
<i class='icon'>${icon}</i> <i class='icon'>${icon}</i>
</td> </td>
<td class='col-name'>${file.name}</td> <td class='col-name ${rowClass}' onclick=${handleClick}>
<td class='col-progress'>${progress}</td> ${file.name}
<td class='col-size'>${prettyBytes(file.length)}</td> </td>
<td class='col-progress ${rowClass}' onclick=${handleClick}>
${isSelected ? progress : ''}
</td>
<td class='col-size ${rowClass}' onclick=${handleClick}>
${prettyBytes(file.length)}
</td>
<td class='col-select'
onclick=${dispatcher('toggleTorrentFile', infoHash, index)}>
<i class='icon'>${isSelected ? 'close' : 'add'}</i>
</td>
</tr> </tr>
` `
} }

View File

@@ -49,8 +49,8 @@ function init () {
client.on('warning', (err) => ipc.send('wt-warning', null, err.message)) client.on('warning', (err) => ipc.send('wt-warning', null, err.message))
client.on('error', (err) => ipc.send('wt-error', null, err.message)) client.on('error', (err) => ipc.send('wt-error', null, err.message))
ipc.on('wt-start-torrenting', (e, torrentKey, torrentID, path, fileModtimes) => ipc.on('wt-start-torrenting', (e, torrentKey, torrentID, path, fileModtimes, selections) =>
startTorrenting(torrentKey, torrentID, path, fileModtimes)) startTorrenting(torrentKey, torrentID, path, fileModtimes, selections))
ipc.on('wt-stop-torrenting', (e, infoHash) => ipc.on('wt-stop-torrenting', (e, infoHash) =>
stopTorrenting(infoHash)) stopTorrenting(infoHash))
ipc.on('wt-create-torrent', (e, torrentKey, options) => ipc.on('wt-create-torrent', (e, torrentKey, options) =>
@@ -65,6 +65,8 @@ function init () {
startServer(infoHash, index)) startServer(infoHash, index))
ipc.on('wt-stop-server', (e) => ipc.on('wt-stop-server', (e) =>
stopServer()) stopServer())
ipc.on('wt-select-files', (e, infoHash, selections) =>
selectFiles(infoHash, selections))
ipc.send('ipcReadyWebTorrent') ipc.send('ipcReadyWebTorrent')
@@ -73,7 +75,7 @@ function init () {
// Starts a given TorrentID, which can be an infohash, magnet URI, etc. Returns WebTorrent object // 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- // 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) console.log('starting torrent %s: %s', torrentKey, torrentID)
var torrent = client.add(torrentID, { var torrent = client.add(torrentID, {
@@ -81,8 +83,13 @@ function startTorrenting (torrentKey, torrentID, path, fileModtimes) {
fileModtimes: fileModtimes fileModtimes: fileModtimes
}) })
torrent.key = torrentKey torrent.key = torrentKey
// Listen for ready event, progress notifications, etc
addTorrentEvents(torrent) addTorrentEvents(torrent)
// Only download the files the user wants, not necessarily all files
torrent.once('ready', () => selectFiles(torrent, selections))
return torrent 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 // Gets a WebTorrent handle by torrentKey
// Throws an Error if we're not currently torrenting anything w/ that key // Throws an Error if we're not currently torrenting anything w/ that key
function getTorrent (torrentKey) { function getTorrent (torrentKey) {