410 lines
12 KiB
JavaScript
410 lines
12 KiB
JavaScript
const fs = require('fs')
|
|
const path = require('path')
|
|
const { ipcRenderer, clipboard } = require('electron')
|
|
const remote = require('@electron/remote')
|
|
|
|
const { dispatch } = require('../lib/dispatcher')
|
|
const { TorrentKeyNotFoundError } = require('../lib/errors')
|
|
const sound = require('../lib/sound')
|
|
const TorrentSummary = require('../lib/torrent-summary')
|
|
|
|
const instantIoRegex = /^(https:\/\/)?instant\.io\/#/
|
|
|
|
// Controls the torrent list: creating, adding, deleting, & manipulating torrents
|
|
module.exports = class TorrentListController {
|
|
constructor (state) {
|
|
this.state = state
|
|
}
|
|
|
|
// Adds a torrent to the list, starts downloading/seeding.
|
|
// TorrentID can be a magnet URI, infohash, or torrent file: https://git.io/vik9M
|
|
addTorrent (torrentId) {
|
|
if (torrentId.path) {
|
|
// Use path string instead of W3C File object
|
|
torrentId = torrentId.path
|
|
}
|
|
|
|
// Trim extra spaces off pasted magnet links
|
|
if (typeof torrentId === 'string') {
|
|
torrentId = torrentId.trim()
|
|
}
|
|
|
|
// Allow a instant.io link to be pasted
|
|
if (typeof torrentId === 'string' && instantIoRegex.test(torrentId)) {
|
|
torrentId = torrentId.slice(torrentId.indexOf('#') + 1)
|
|
}
|
|
|
|
const torrentKey = this.state.nextTorrentKey++
|
|
const path = this.state.saved.prefs.downloadPath
|
|
|
|
ipcRenderer.send('wt-start-torrenting', torrentKey, torrentId, path)
|
|
|
|
dispatch('backToList')
|
|
}
|
|
|
|
// Shows the Create Torrent page with options to seed a given file or folder
|
|
showCreateTorrent (files) {
|
|
// You can only create torrents from the home screen.
|
|
if (this.state.location.url() !== 'home') {
|
|
return dispatch('error', 'Please go back to the torrent list before creating a new torrent.')
|
|
}
|
|
|
|
// Files will either be an array of file objects, which we can send directly
|
|
// to the create-torrent screen
|
|
if (files.length === 0 || typeof files[0] !== 'string') {
|
|
this.state.location.go({
|
|
url: 'create-torrent',
|
|
files,
|
|
setup: (cb) => {
|
|
this.state.window.title = 'Create New Torrent'
|
|
cb(null)
|
|
}
|
|
})
|
|
return
|
|
}
|
|
|
|
// ... or it will be an array of mixed file and folder paths. We have to walk
|
|
// through all the folders and find the files
|
|
findFilesRecursive(files, (allFiles) => this.showCreateTorrent(allFiles))
|
|
}
|
|
|
|
// Creates a new torrent and start seeeding
|
|
createTorrent (options) {
|
|
const state = this.state
|
|
const torrentKey = state.nextTorrentKey++
|
|
ipcRenderer.send('wt-create-torrent', torrentKey, options)
|
|
state.location.cancel()
|
|
}
|
|
|
|
// Starts downloading and/or seeding a given torrentSummary.
|
|
startTorrentingSummary (torrentKey) {
|
|
const s = TorrentSummary.getByKey(this.state, torrentKey)
|
|
if (!s) throw new TorrentKeyNotFoundError(torrentKey)
|
|
|
|
// New torrent: give it a path
|
|
if (!s.path) {
|
|
// Use Downloads folder by default
|
|
s.path = this.state.saved.prefs.downloadPath
|
|
return start()
|
|
}
|
|
|
|
const fileOrFolder = TorrentSummary.getFileOrFolder(s)
|
|
|
|
// New torrent: metadata not yet received
|
|
if (!fileOrFolder) return start()
|
|
|
|
// Existing torrent: check that the path is still there
|
|
fs.stat(fileOrFolder, err => {
|
|
if (err) {
|
|
s.error = 'path-missing'
|
|
dispatch('backToList')
|
|
return
|
|
}
|
|
start()
|
|
})
|
|
|
|
function start () {
|
|
ipcRenderer.send('wt-start-torrenting',
|
|
s.torrentKey,
|
|
TorrentSummary.getTorrentId(s),
|
|
s.path,
|
|
s.fileModtimes,
|
|
s.selections)
|
|
}
|
|
}
|
|
|
|
// TODO: use torrentKey, not infoHash
|
|
toggleTorrent (infoHash) {
|
|
const torrentSummary = TorrentSummary.getByKey(this.state, infoHash)
|
|
if (torrentSummary.status === 'paused') {
|
|
torrentSummary.status = 'new'
|
|
this.startTorrentingSummary(torrentSummary.torrentKey)
|
|
sound.play('ENABLE')
|
|
return
|
|
}
|
|
|
|
this.pauseTorrent(torrentSummary, true)
|
|
}
|
|
|
|
pauseAllTorrents () {
|
|
this.state.saved.torrents.forEach((torrentSummary) => {
|
|
if (torrentSummary.status === 'downloading' ||
|
|
torrentSummary.status === 'seeding') {
|
|
torrentSummary.status = 'paused'
|
|
ipcRenderer.send('wt-stop-torrenting', torrentSummary.infoHash)
|
|
}
|
|
})
|
|
sound.play('DISABLE')
|
|
}
|
|
|
|
resumeAllTorrents () {
|
|
this.state.saved.torrents.forEach((torrentSummary) => {
|
|
if (torrentSummary.status === 'paused') {
|
|
torrentSummary.status = 'downloading'
|
|
this.startTorrentingSummary(torrentSummary.torrentKey)
|
|
}
|
|
})
|
|
sound.play('ENABLE')
|
|
}
|
|
|
|
pauseTorrent (torrentSummary, playSound) {
|
|
torrentSummary.status = 'paused'
|
|
ipcRenderer.send('wt-stop-torrenting', torrentSummary.infoHash)
|
|
|
|
if (playSound) sound.play('DISABLE')
|
|
}
|
|
|
|
prioritizeTorrent (infoHash) {
|
|
this.state.saved.torrents
|
|
.filter(torrent => ['downloading', 'seeding'].includes(torrent.status)) // Active torrents only.
|
|
.forEach((torrent) => { // Pause all active torrents except the one that started playing.
|
|
if (infoHash === torrent.infoHash) return
|
|
|
|
// Pause torrent without playing sounds.
|
|
this.pauseTorrent(torrent, false)
|
|
|
|
this.state.saved.torrentsToResume.push(torrent.infoHash)
|
|
})
|
|
|
|
console.log('Playback Priority: paused torrents: ', this.state.saved.torrentsToResume)
|
|
}
|
|
|
|
resumePausedTorrents () {
|
|
console.log('Playback Priority: resuming paused torrents')
|
|
if (!this.state.saved.torrentsToResume || !this.state.saved.torrentsToResume.length) return
|
|
this.state.saved.torrentsToResume.forEach((infoHash) => {
|
|
this.toggleTorrent(infoHash)
|
|
})
|
|
|
|
// reset paused torrents
|
|
this.state.saved.torrentsToResume = []
|
|
}
|
|
|
|
toggleTorrentFile (infoHash, index) {
|
|
const torrentSummary = TorrentSummary.getByKey(this.state, infoHash)
|
|
torrentSummary.selections[index] = !torrentSummary.selections[index]
|
|
|
|
// Let the WebTorrent process know to start or stop fetching that file
|
|
if (torrentSummary.status !== 'paused') {
|
|
ipcRenderer.send('wt-select-files', infoHash, torrentSummary.selections)
|
|
}
|
|
}
|
|
|
|
confirmDeleteTorrent (infoHash, deleteData) {
|
|
this.state.modal = {
|
|
id: 'remove-torrent-modal',
|
|
infoHash,
|
|
deleteData
|
|
}
|
|
}
|
|
|
|
confirmDeleteAllTorrents (deleteData) {
|
|
this.state.modal = {
|
|
id: 'delete-all-torrents-modal',
|
|
deleteData
|
|
}
|
|
}
|
|
|
|
// TODO: use torrentKey, not infoHash
|
|
deleteTorrent (infoHash, deleteData) {
|
|
const index = this.state.saved.torrents.findIndex((x) => x.infoHash === infoHash)
|
|
|
|
if (index > -1) {
|
|
const summary = this.state.saved.torrents[index]
|
|
deleteTorrentFile(summary, deleteData)
|
|
|
|
// remove torrent from saved list
|
|
this.state.saved.torrents.splice(index, 1)
|
|
dispatch('stateSave')
|
|
|
|
// prevent user from going forward to a deleted torrent
|
|
this.state.location.clearForward('player')
|
|
sound.play('DELETE')
|
|
} else {
|
|
throw new TorrentKeyNotFoundError(infoHash)
|
|
}
|
|
}
|
|
|
|
deleteAllTorrents (deleteData) {
|
|
// Go back to list before the current playing torrent is deleted
|
|
if (this.state.location.url() === 'player') {
|
|
dispatch('backToList')
|
|
}
|
|
|
|
this.state.saved.torrents.forEach((summary) => deleteTorrentFile(summary, deleteData))
|
|
|
|
this.state.saved.torrents = []
|
|
dispatch('stateSave')
|
|
|
|
// prevent user from going forward to a deleted torrent
|
|
this.state.location.clearForward('player')
|
|
sound.play('DELETE')
|
|
}
|
|
|
|
toggleSelectTorrent (infoHash) {
|
|
if (this.state.selectedInfoHash === infoHash) {
|
|
this.state.selectedInfoHash = null
|
|
} else {
|
|
this.state.selectedInfoHash = infoHash
|
|
}
|
|
}
|
|
|
|
openTorrentContextMenu (infoHash) {
|
|
const torrentSummary = TorrentSummary.getByKey(this.state, infoHash)
|
|
const menu = new remote.Menu()
|
|
|
|
menu.append(new remote.MenuItem({
|
|
label: 'Remove From List',
|
|
click: () => dispatch('confirmDeleteTorrent', torrentSummary.infoHash, false)
|
|
}))
|
|
|
|
menu.append(new remote.MenuItem({
|
|
label: 'Remove Data File',
|
|
click: () => dispatch('confirmDeleteTorrent', torrentSummary.infoHash, true)
|
|
}))
|
|
|
|
menu.append(new remote.MenuItem({
|
|
type: 'separator'
|
|
}))
|
|
|
|
if (torrentSummary.files) {
|
|
menu.append(new remote.MenuItem({
|
|
label: process.platform === 'darwin' ? 'Show in Finder' : 'Show in Folder',
|
|
click: () => showItemInFolder(torrentSummary)
|
|
}))
|
|
menu.append(new remote.MenuItem({
|
|
type: 'separator'
|
|
}))
|
|
}
|
|
|
|
menu.append(new remote.MenuItem({
|
|
label: 'Copy Magnet Link to Clipboard',
|
|
click: () => clipboard.writeText(torrentSummary.magnetURI)
|
|
}))
|
|
|
|
menu.append(new remote.MenuItem({
|
|
label: 'Copy Instant.io Link to Clipboard',
|
|
click: () => clipboard.writeText(`https://instant.io/#${torrentSummary.infoHash}`)
|
|
}))
|
|
|
|
menu.append(new remote.MenuItem({
|
|
label: 'Save Torrent File As...',
|
|
click: () => dispatch('saveTorrentFileAs', torrentSummary.torrentKey),
|
|
enabled: torrentSummary.torrentFileName != null
|
|
}))
|
|
|
|
menu.append(new remote.MenuItem({
|
|
type: 'separator'
|
|
}))
|
|
|
|
const sortedByName = this.state.saved.prefs.sortByName
|
|
menu.append(new remote.MenuItem({
|
|
label: `${sortedByName ? '✓ ' : ''}Sort by Name`,
|
|
click: () => dispatch('updatePreferences', 'sortByName', !sortedByName)
|
|
}))
|
|
|
|
menu.popup({ window: remote.getCurrentWindow() })
|
|
}
|
|
|
|
// Takes a torrentSummary or torrentKey
|
|
// Shows a Save File dialog, then saves the .torrent file wherever the user requests
|
|
saveTorrentFileAs (torrentKey) {
|
|
const torrentSummary = TorrentSummary.getByKey(this.state, torrentKey)
|
|
if (!torrentSummary) throw new TorrentKeyNotFoundError(torrentKey)
|
|
const downloadPath = this.state.saved.prefs.downloadPath
|
|
const newFileName = path.parse(torrentSummary.name).name + '.torrent'
|
|
const win = remote.getCurrentWindow()
|
|
const opts = {
|
|
title: 'Save Torrent File',
|
|
defaultPath: path.join(downloadPath, newFileName),
|
|
filters: [
|
|
{ name: 'Torrent Files', extensions: ['torrent'] },
|
|
{ name: 'All Files', extensions: ['*'] }
|
|
],
|
|
buttonLabel: 'Save'
|
|
}
|
|
|
|
const savePath = remote.dialog.showSaveDialogSync(win, opts)
|
|
|
|
if (!savePath) return // They clicked Cancel
|
|
console.log('Saving torrent ' + torrentKey + ' to ' + savePath)
|
|
const torrentPath = TorrentSummary.getTorrentPath(torrentSummary)
|
|
fs.readFile(torrentPath, (err, torrentFile) => {
|
|
if (err) return dispatch('error', err)
|
|
fs.writeFile(savePath, torrentFile, err => {
|
|
if (err) return dispatch('error', err)
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
// Recursively finds {name, path, size} for all files in a folder
|
|
// Calls `cb` on success, calls `onError` on failure
|
|
function findFilesRecursive (paths, cb_) {
|
|
if (paths.length > 1) {
|
|
let numComplete = 0
|
|
const ret = []
|
|
paths.forEach(path => {
|
|
findFilesRecursive([path], fileObjs => {
|
|
ret.push(...fileObjs)
|
|
if (++numComplete === paths.length) {
|
|
ret.sort((a, b) => a.path < b.path ? -1 : Number(a.path > b.path))
|
|
cb_(ret)
|
|
}
|
|
})
|
|
})
|
|
return
|
|
}
|
|
|
|
const fileOrFolder = paths[0]
|
|
fs.stat(fileOrFolder, (err, stat) => {
|
|
if (err) return dispatch('error', err)
|
|
|
|
// Files: return name, path, and size
|
|
if (!stat.isDirectory()) {
|
|
const filePath = fileOrFolder
|
|
return cb_([{
|
|
name: path.basename(filePath),
|
|
path: filePath,
|
|
size: stat.size
|
|
}])
|
|
}
|
|
|
|
// Folders: recurse, make a list of all the files
|
|
const folderPath = fileOrFolder
|
|
fs.readdir(folderPath, (err, fileNames) => {
|
|
if (err) return dispatch('error', err)
|
|
const paths = fileNames.map((fileName) => path.join(folderPath, fileName))
|
|
findFilesRecursive(paths, cb_)
|
|
})
|
|
})
|
|
}
|
|
|
|
function deleteFile (path) {
|
|
if (!path) return
|
|
fs.unlink(path, err => {
|
|
if (err) dispatch('error', err)
|
|
})
|
|
}
|
|
|
|
// Delete all files in a torrent
|
|
function moveItemToTrash (torrentSummary) {
|
|
const filePath = TorrentSummary.getFileOrFolder(torrentSummary)
|
|
if (filePath) ipcRenderer.send('moveItemToTrash', filePath)
|
|
}
|
|
|
|
function showItemInFolder (torrentSummary) {
|
|
ipcRenderer.send('showItemInFolder', TorrentSummary.getFileOrFolder(torrentSummary))
|
|
}
|
|
|
|
function deleteTorrentFile (torrentSummary, deleteData) {
|
|
ipcRenderer.send('wt-stop-torrenting', torrentSummary.infoHash)
|
|
|
|
// remove torrent and poster file
|
|
deleteFile(TorrentSummary.getTorrentPath(torrentSummary))
|
|
deleteFile(TorrentSummary.getPosterPath(torrentSummary))
|
|
|
|
// optionally delete the torrent data
|
|
if (deleteData) moveItemToTrash(torrentSummary)
|
|
}
|