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:
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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>
|
||||||
`
|
`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user