Compare commits

..

18 Commits

Author SHA1 Message Date
Zlatin Balevsky
5dcef3ca05 Release 0.2.5 2019-06-17 12:53:58 +01:00
Zlatin Balevsky
eaa0e46ce5 Merge branch 'separate-incomplete-files' 2019-06-17 12:45:51 +01:00
Zlatin Balevsky
c4f48c02b6 delete incomplete file on cancel 2019-06-17 12:33:44 +01:00
Zlatin Balevsky
5c16335969 if no row is selected do not enable buttons 2019-06-17 12:26:28 +01:00
Zlatin Balevsky
546eb4e9d3 only allow one download per infohash from gui 2019-06-17 11:25:21 +01:00
Zlatin Balevsky
c3d9e852ba separate incomplete files 2019-06-17 07:49:06 +01:00
Zlatin Balevsky
0db7077a45 Release 0.2.4 2019-06-17 03:22:52 +01:00
Zlatin Balevsky
614ecc85fe new piece selection logic to avoid high cpu bug 2019-06-17 03:21:37 +01:00
Zlatin Balevsky
af66a79376 fix sorting by progress 2019-06-17 00:56:16 +01:00
Zlatin Balevsky
465171c81d prevent multiple identical shared files 2019-06-17 00:38:05 +01:00
Zlatin Balevsky
b507361c58 close the file before marking pieces complete 2019-06-16 23:45:23 +01:00
Zlatin Balevsky
4d001ae74b thread-safe access to the pieces file 2019-06-16 22:56:09 +01:00
Zlatin Balevsky
36a6e2769f Release 0.2.3 2019-06-16 19:05:12 +01:00
Zlatin Balevsky
69eeb7d77a fix 2019-06-16 18:58:52 +01:00
Zlatin Balevsky
551982b72a batch results sent to the GUI to prevent freeze 2019-06-16 18:51:07 +01:00
Zlatin Balevsky
8d808f0b8f Release 0.2.2 2019-06-16 13:30:11 +01:00
Zlatin Balevsky
7833a83c87 mark hash queries for V2 results 2019-06-16 13:17:32 +01:00
Zlatin Balevsky
3160c1a8f3 fix for silent uploader exceptions 2019-06-16 13:01:14 +01:00
19 changed files with 249 additions and 119 deletions

View File

@@ -34,7 +34,7 @@ class Cli {
Core core Core core
try { try {
core = new Core(props, home, "0.2.1") core = new Core(props, home, "0.2.5")
} catch (Exception bad) { } catch (Exception bad) {
bad.printStackTrace(System.out) bad.printStackTrace(System.out)
println "Failed to initialize core, exiting" println "Failed to initialize core, exiting"

View File

@@ -53,7 +53,7 @@ class CliDownloader {
Core core Core core
try { try {
core = new Core(props, home, "0.2.1") core = new Core(props, home, "0.2.5")
} catch (Exception bad) { } catch (Exception bad) {
bad.printStackTrace(System.out) bad.printStackTrace(System.out)
println "Failed to initialize core, exiting" println "Failed to initialize core, exiting"

View File

@@ -268,7 +268,7 @@ public class Core {
} }
} }
Core core = new Core(props, home, "0.2.1") Core core = new Core(props, home, "0.2.5")
core.startServices() core.startServices()
// ... at the end, sleep or execute script // ... at the end, sleep or execute script

View File

@@ -17,6 +17,8 @@ import com.muwire.core.upload.UploadManager
import com.muwire.core.search.InvalidSearchResultException import com.muwire.core.search.InvalidSearchResultException
import com.muwire.core.search.ResultsParser import com.muwire.core.search.ResultsParser
import com.muwire.core.search.SearchManager import com.muwire.core.search.SearchManager
import com.muwire.core.search.UIResultBatchEvent
import com.muwire.core.search.UIResultEvent
import com.muwire.core.search.UnexpectedResultsException import com.muwire.core.search.UnexpectedResultsException
import groovy.json.JsonOutput import groovy.json.JsonOutput
@@ -225,13 +227,15 @@ class ConnectionAcceptor {
if (sender.destination != e.getDestination()) if (sender.destination != e.getDestination())
throw new IOException("Sender destination mismatch expected $e.getDestination(), got $sender.destination") throw new IOException("Sender destination mismatch expected $e.getDestination(), got $sender.destination")
int nResults = dis.readUnsignedShort() int nResults = dis.readUnsignedShort()
UIResultEvent[] results = new UIResultEvent[nResults]
for (int i = 0; i < nResults; i++) { for (int i = 0; i < nResults; i++) {
int jsonSize = dis.readUnsignedShort() int jsonSize = dis.readUnsignedShort()
byte [] payload = new byte[jsonSize] byte [] payload = new byte[jsonSize]
dis.readFully(payload) dis.readFully(payload)
def json = slurper.parse(payload) def json = slurper.parse(payload)
eventBus.publish(ResultsParser.parse(sender, resultsUUID, json)) results[i] = ResultsParser.parse(sender, resultsUUID, json)
} }
eventBus.publish(new UIResultBatchEvent(uuid: resultsUUID, results: results))
} catch (IOException | UnexpectedResultsException | InvalidSearchResultException bad) { } catch (IOException | UnexpectedResultsException | InvalidSearchResultException bad) {
log.log(Level.WARNING, "failed to process POST", bad) log.log(Level.WARNING, "failed to process POST", bad)
} finally { } finally {

View File

@@ -2,7 +2,6 @@ package com.muwire.core.connection
import java.io.InputStream import java.io.InputStream
import java.io.OutputStream import java.io.OutputStream
import java.nio.charset.StandardCharsets
import com.muwire.core.EventBus import com.muwire.core.EventBus
import com.muwire.core.MuWireSettings import com.muwire.core.MuWireSettings
@@ -67,7 +66,7 @@ class PeerConnection extends Connection {
protected void write(Object message) { protected void write(Object message) {
byte[] payload byte[] payload
if (message instanceof Map) { if (message instanceof Map) {
payload = JsonOutput.toJson(message).getBytes(StandardCharsets.UTF_8) payload = JsonOutput.toJson(message).bytes
DataUtil.packHeader(payload.length, writeHeader) DataUtil.packHeader(payload.length, writeHeader)
log.fine "$name writing message type ${message.type} length $payload.length" log.fine "$name writing message type ${message.type} length $payload.length"
writeHeader[0] &= (byte)0x7F writeHeader[0] &= (byte)0x7F

View File

@@ -16,6 +16,7 @@ import java.nio.file.Files
import java.nio.file.StandardOpenOption import java.nio.file.StandardOpenOption
import java.security.MessageDigest import java.security.MessageDigest
import java.security.NoSuchAlgorithmException import java.security.NoSuchAlgorithmException
import java.util.logging.Level
@Log @Log
class DownloadSession { class DownloadSession {
@@ -23,7 +24,7 @@ class DownloadSession {
private static int SAMPLES = 10 private static int SAMPLES = 10
private final String meB64 private final String meB64
private final Pieces downloaded, claimed private final Pieces pieces
private final InfoHash infoHash private final InfoHash infoHash
private final Endpoint endpoint private final Endpoint endpoint
private final File file private final File file
@@ -36,11 +37,10 @@ class DownloadSession {
private ByteBuffer mapped private ByteBuffer mapped
DownloadSession(String meB64, Pieces downloaded, Pieces claimed, InfoHash infoHash, Endpoint endpoint, File file, DownloadSession(String meB64, Pieces pieces, InfoHash infoHash, Endpoint endpoint, File file,
int pieceSize, long fileLength) { int pieceSize, long fileLength) {
this.meB64 = meB64 this.meB64 = meB64
this.downloaded = downloaded this.pieces = pieces
this.claimed = claimed
this.endpoint = endpoint this.endpoint = endpoint
this.infoHash = infoHash this.infoHash = infoHash
this.file = file this.file = file
@@ -63,20 +63,11 @@ class DownloadSession {
OutputStream os = endpoint.getOutputStream() OutputStream os = endpoint.getOutputStream()
InputStream is = endpoint.getInputStream() InputStream is = endpoint.getInputStream()
int piece int piece = pieces.claim()
while(true) { if (piece == -1)
piece = downloaded.getRandomPiece() return false
if (claimed.isMarked(piece)) { boolean unclaim = true
if (downloaded.donePieces() + claimed.donePieces() == downloaded.nPieces) {
log.info("all pieces claimed")
return false
}
continue
}
break
}
claimed.markDownloaded(piece)
log.info("will download piece $piece") log.info("will download piece $piece")
long start = piece * pieceSize long start = piece * pieceSize
@@ -85,7 +76,6 @@ class DownloadSession {
String root = Base64.encode(infoHash.getRoot()) String root = Base64.encode(infoHash.getRoot())
FileChannel channel
try { try {
os.write("GET $root\r\n".getBytes(StandardCharsets.US_ASCII)) os.write("GET $root\r\n".getBytes(StandardCharsets.US_ASCII))
os.write("Range: $start-$end\r\n".getBytes(StandardCharsets.US_ASCII)) os.write("Range: $start-$end\r\n".getBytes(StandardCharsets.US_ASCII))
@@ -135,41 +125,46 @@ class DownloadSession {
} }
// start the download // start the download
channel = Files.newByteChannel(file.toPath(), EnumSet.of(StandardOpenOption.READ, StandardOpenOption.WRITE, FileChannel channel
StandardOpenOption.SPARSE, StandardOpenOption.CREATE)) // TODO: double-check, maybe CREATE_NEW try {
mapped = channel.map(FileChannel.MapMode.READ_WRITE, start, end - start + 1) channel = Files.newByteChannel(file.toPath(), EnumSet.of(StandardOpenOption.READ, StandardOpenOption.WRITE,
StandardOpenOption.SPARSE, StandardOpenOption.CREATE)) // TODO: double-check, maybe CREATE_NEW
byte[] tmp = new byte[0x1 << 13] mapped = channel.map(FileChannel.MapMode.READ_WRITE, start, end - start + 1)
while(mapped.hasRemaining()) {
if (mapped.remaining() < tmp.length) byte[] tmp = new byte[0x1 << 13]
tmp = new byte[mapped.remaining()] while(mapped.hasRemaining()) {
int read = is.read(tmp) if (mapped.remaining() < tmp.length)
if (read == -1) tmp = new byte[mapped.remaining()]
throw new IOException() int read = is.read(tmp)
synchronized(this) { if (read == -1)
mapped.put(tmp, 0, read) throw new IOException()
synchronized(this) {
if (timestamps.size() == SAMPLES) { mapped.put(tmp, 0, read)
timestamps.removeFirst()
reads.removeFirst() if (timestamps.size() == SAMPLES) {
timestamps.removeFirst()
reads.removeFirst()
}
timestamps.addLast(System.currentTimeMillis())
reads.addLast(read)
} }
timestamps.addLast(System.currentTimeMillis())
reads.addLast(read)
} }
mapped.clear()
digest.update(mapped)
byte [] hash = digest.digest()
byte [] expected = new byte[32]
System.arraycopy(infoHash.getHashList(), piece * 32, expected, 0, 32)
if (hash != expected)
throw new BadHashException()
} finally {
try { channel?.close() } catch (IOException ignore) {}
} }
pieces.markDownloaded(piece)
mapped.clear() unclaim = false
digest.update(mapped)
byte [] hash = digest.digest()
byte [] expected = new byte[32]
System.arraycopy(infoHash.getHashList(), piece * 32, expected, 0, 32)
if (hash != expected)
throw new BadHashException()
downloaded.markDownloaded(piece)
} finally { } finally {
claimed.clear(piece) if (unclaim)
try { channel?.close() } catch (IOException ignore) {} pieces.unclaim(piece)
} }
return true return true
} }

View File

@@ -4,9 +4,13 @@ import com.muwire.core.InfoHash
import com.muwire.core.Persona import com.muwire.core.Persona
import com.muwire.core.connection.Endpoint import com.muwire.core.connection.Endpoint
import java.nio.file.AtomicMoveNotSupportedException
import java.nio.file.Files
import java.nio.file.StandardCopyOption
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ExecutorService import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors import java.util.concurrent.Executors
import java.util.concurrent.atomic.AtomicBoolean
import java.util.logging.Level import java.util.logging.Level
import com.muwire.core.Constants import com.muwire.core.Constants
@@ -34,7 +38,7 @@ public class Downloader {
private final DownloadManager downloadManager private final DownloadManager downloadManager
private final Persona me private final Persona me
private final File file private final File file
private final Pieces downloaded, claimed private final Pieces pieces
private final long length private final long length
private InfoHash infoHash private InfoHash infoHash
private final int pieceSize private final int pieceSize
@@ -42,12 +46,14 @@ public class Downloader {
private final Set<Destination> destinations private final Set<Destination> destinations
private final int nPieces private final int nPieces
private final File piecesFile private final File piecesFile
private final File incompleteFile
final int pieceSizePow2 final int pieceSizePow2
private final Map<Destination, DownloadWorker> activeWorkers = new ConcurrentHashMap<>() private final Map<Destination, DownloadWorker> activeWorkers = new ConcurrentHashMap<>()
private volatile boolean cancelled private volatile boolean cancelled
private volatile boolean eventFired private final AtomicBoolean eventFired = new AtomicBoolean()
private boolean piecesFileClosed
public Downloader(EventBus eventBus, DownloadManager downloadManager, public Downloader(EventBus eventBus, DownloadManager downloadManager,
Persona me, File file, long length, InfoHash infoHash, Persona me, File file, long length, InfoHash infoHash,
@@ -62,6 +68,7 @@ public class Downloader {
this.connector = connector this.connector = connector
this.destinations = destinations this.destinations = destinations
this.piecesFile = new File(incompletes, file.getName()+".pieces") this.piecesFile = new File(incompletes, file.getName()+".pieces")
this.incompleteFile = new File(incompletes, file.getName()+".part")
this.pieceSizePow2 = pieceSizePow2 this.pieceSizePow2 = pieceSizePow2
this.pieceSize = 1 << pieceSizePow2 this.pieceSize = 1 << pieceSizePow2
@@ -72,8 +79,7 @@ public class Downloader {
nPieces = length / pieceSize + 1 nPieces = length / pieceSize + 1
this.nPieces = nPieces this.nPieces = nPieces
downloaded = new Pieces(nPieces, Constants.DOWNLOAD_SEQUENTIAL_RATIO) pieces = new Pieces(nPieces, Constants.DOWNLOAD_SEQUENTIAL_RATIO)
claimed = new Pieces(nPieces)
} }
public synchronized InfoHash getInfoHash() { public synchronized InfoHash getInfoHash() {
@@ -100,20 +106,24 @@ public class Downloader {
return return
piecesFile.eachLine { piecesFile.eachLine {
int piece = Integer.parseInt(it) int piece = Integer.parseInt(it)
downloaded.markDownloaded(piece) pieces.markDownloaded(piece)
} }
} }
void writePieces() { void writePieces() {
piecesFile.withPrintWriter { writer -> synchronized(piecesFile) {
downloaded.getDownloaded().each { piece -> if (piecesFileClosed)
writer.println(piece) return
piecesFile.withPrintWriter { writer ->
pieces.getDownloaded().each { piece ->
writer.println(piece)
}
} }
} }
} }
public long donePieces() { public long donePieces() {
downloaded.donePieces() pieces.donePieces()
} }
@@ -136,7 +146,7 @@ public class Downloader {
allFinished &= it.currentState == WorkerState.FINISHED allFinished &= it.currentState == WorkerState.FINISHED
} }
if (allFinished) { if (allFinished) {
if (downloaded.isComplete()) if (pieces.isComplete())
return DownloadState.FINISHED return DownloadState.FINISHED
return DownloadState.FAILED return DownloadState.FAILED
} }
@@ -170,8 +180,11 @@ public class Downloader {
public void cancel() { public void cancel() {
cancelled = true cancelled = true
stop() stop()
file.delete() synchronized(piecesFile) {
piecesFile.delete() piecesFileClosed = true
piecesFile.delete()
}
incompleteFile.delete()
} }
void stop() { void stop() {
@@ -231,8 +244,8 @@ public class Downloader {
} }
currentState = WorkerState.DOWNLOADING currentState = WorkerState.DOWNLOADING
boolean requestPerformed boolean requestPerformed
while(!downloaded.isComplete()) { while(!pieces.isComplete()) {
currentSession = new DownloadSession(me.toBase64(), downloaded, claimed, getInfoHash(), endpoint, file, pieceSize, length) currentSession = new DownloadSession(me.toBase64(), pieces, getInfoHash(), endpoint, incompleteFile, pieceSize, length)
requestPerformed = currentSession.request() requestPerformed = currentSession.request()
if (!requestPerformed) if (!requestPerformed)
break break
@@ -242,9 +255,17 @@ public class Downloader {
log.log(Level.WARNING,"Exception while downloading",bad) log.log(Level.WARNING,"Exception while downloading",bad)
} finally { } finally {
currentState = WorkerState.FINISHED currentState = WorkerState.FINISHED
if (downloaded.isComplete() && !eventFired) { if (pieces.isComplete() && eventFired.compareAndSet(false, true)) {
piecesFile.delete() synchronized(piecesFile) {
eventFired = true piecesFileClosed = true
piecesFile.delete()
}
try {
Files.move(incompleteFile.toPath(), file.toPath(), StandardCopyOption.ATOMIC_MOVE)
} catch (AtomicMoveNotSupportedException e) {
Files.copy(incompleteFile.toPath(), file.toPath(), StandardCopyOption.REPLACE_EXISTING)
incompleteFile.delete()
}
eventBus.publish( eventBus.publish(
new FileDownloadedEvent( new FileDownloadedEvent(
downloadedFile : new DownloadedFile(file, getInfoHash(), pieceSizePow2, Collections.emptySet()), downloadedFile : new DownloadedFile(file, getInfoHash(), pieceSizePow2, Collections.emptySet()),

View File

@@ -1,7 +1,7 @@
package com.muwire.core.download package com.muwire.core.download
class Pieces { class Pieces {
private final BitSet bitSet private final BitSet done, claimed
private final int nPieces private final int nPieces
private final float ratio private final float ratio
private final Random random = new Random() private final Random random = new Random()
@@ -13,52 +13,53 @@ class Pieces {
Pieces(int nPieces, float ratio) { Pieces(int nPieces, float ratio) {
this.nPieces = nPieces this.nPieces = nPieces
this.ratio = ratio this.ratio = ratio
bitSet = new BitSet(nPieces) done = new BitSet(nPieces)
claimed = new BitSet(nPieces)
} }
synchronized int getRandomPiece() { synchronized int claim() {
int cardinality = bitSet.cardinality() int claimedCardinality = claimed.cardinality()
if (cardinality == nPieces) if (claimedCardinality == nPieces)
return -1 return -1
// if fuller than ratio just do sequential // if fuller than ratio just do sequential
if ( (1.0f * cardinality) / nPieces > ratio) { if ( (1.0f * claimedCardinality) / nPieces > ratio) {
return bitSet.nextClearBit(0) int rv = claimed.nextClearBit(0)
claimed.set(rv)
return rv
} }
while(true) { while(true) {
int start = random.nextInt(nPieces) int start = random.nextInt(nPieces)
if (bitSet.get(start)) if (claimed.get(start))
continue continue
claimed.set(start)
return start return start
} }
} }
def getDownloaded() { synchronized def getDownloaded() {
def rv = [] def rv = []
for (int i = bitSet.nextSetBit(0); i >= 0; i = bitSet.nextSetBit(i+1)) { for (int i = done.nextSetBit(0); i >= 0; i = done.nextSetBit(i+1)) {
rv << i rv << i
} }
rv rv
} }
synchronized void markDownloaded(int piece) { synchronized void markDownloaded(int piece) {
bitSet.set(piece) done.set(piece)
claimed.set(piece)
} }
synchronized void clear(int piece) { synchronized void unclaim(int piece) {
bitSet.clear(piece) claimed.clear(piece)
} }
synchronized boolean isComplete() { synchronized boolean isComplete() {
bitSet.cardinality() == nPieces done.cardinality() == nPieces
}
synchronized boolean isMarked(int piece) {
bitSet.get(piece)
} }
synchronized int donePieces() { synchronized int donePieces() {
bitSet.cardinality() done.cardinality()
} }
} }

View File

@@ -0,0 +1,8 @@
package com.muwire.core.search
import com.muwire.core.Event
class UIResultBatchEvent extends Event {
UUID uuid
UIResultEvent[] results
}

View File

@@ -51,4 +51,23 @@ class ContentUploader extends Uploader {
} }
} }
@Override
public String getName() {
return file.getName();
}
@Override
public synchronized int getProgress() {
if (mapped == null)
return 0
int position = mapped.position()
int total = request.getRange().end - request.getRange().start
(int)(position * 100.0 / total)
}
@Override
public String getDownloader() {
request.downloader.getHumanReadableName()
}
} }

View File

@@ -6,6 +6,8 @@ import java.nio.charset.StandardCharsets
import com.muwire.core.InfoHash import com.muwire.core.InfoHash
import com.muwire.core.connection.Endpoint import com.muwire.core.connection.Endpoint
import net.i2p.data.Base64
class HashListUploader extends Uploader { class HashListUploader extends Uploader {
private final InfoHash infoHash private final InfoHash infoHash
private final HashListRequest request private final HashListRequest request
@@ -14,6 +16,7 @@ class HashListUploader extends Uploader {
super(endpoint) super(endpoint)
this.infoHash = infoHash this.infoHash = infoHash
mapped = ByteBuffer.wrap(infoHash.getHashList()) mapped = ByteBuffer.wrap(infoHash.getHashList())
this.request = request
} }
void respond() { void respond() {
@@ -32,4 +35,21 @@ class HashListUploader extends Uploader {
} }
endpoint.getOutputStream().flush() endpoint.getOutputStream().flush()
} }
@Override
public String getName() {
return "Hash list for " + Base64.encode(infoHash.getRoot());
}
@Override
public synchronized int getProgress() {
(int)(mapped.position() * 100.0 / mapped.capacity())
}
@Override
public String getDownloader() {
request.downloader.getHumanReadableName()
}
} }

View File

@@ -23,4 +23,13 @@ abstract class Uploader {
return -1 return -1
mapped.position() mapped.position()
} }
abstract String getName();
/**
* @return an integer between 0 and 100
*/
abstract int getProgress();
abstract String getDownloader();
} }

View File

@@ -25,4 +25,17 @@ public class SharedFile {
public int getPieceSize() { public int getPieceSize() {
return pieceSize; return pieceSize;
} }
@Override
public int hashCode() {
return file.hashCode() ^ infoHash.hashCode();
}
@Override
public boolean equals(Object o) {
if (!(o instanceof SharedFile))
return false;
SharedFile other = (SharedFile)o;
return file.equals(other.file) && infoHash.equals(other.infoHash);
}
} }

View File

@@ -1,5 +1,5 @@
group = com.muwire group = com.muwire
version = 0.2.1 version = 0.2.5
groovyVersion = 2.4.15 groovyVersion = 2.4.15
slf4jVersion = 1.7.25 slf4jVersion = 1.7.25
spockVersion = 1.1-groovy-2.4 spockVersion = 1.1-groovy-2.4

View File

@@ -62,7 +62,7 @@ class MainFrameController {
def searchEvent def searchEvent
if (hashSearch) { if (hashSearch) {
searchEvent = new SearchEvent(searchHash : root, uuid : uuid) searchEvent = new SearchEvent(searchHash : root, uuid : uuid, oobInfohash: true)
} else { } else {
// this can be improved a lot // this can be improved a lot
def replaced = search.toLowerCase().trim().replaceAll(Constants.SPLIT_PATTERN, " ") def replaced = search.toLowerCase().trim().replaceAll(Constants.SPLIT_PATTERN, " ")
@@ -118,8 +118,11 @@ class MainFrameController {
void download() { void download() {
def result = selectedResult() def result = selectedResult()
if (result == null) if (result == null)
return // TODO disable button return
if (!model.canDownload(result.infohash))
return
def file = new File(application.context.get("muwire-settings").downloadLocation, result.name) def file = new File(application.context.get("muwire-settings").downloadLocation, result.name)
def selected = builder.getVariable("result-tabs").getSelectedComponent() def selected = builder.getVariable("result-tabs").getSelectedComponent()
@@ -150,6 +153,7 @@ class MainFrameController {
void cancel() { void cancel() {
def downloader = model.downloads[selectedDownload()].downloader def downloader = model.downloads[selectedDownload()].downloader
downloader.cancel() downloader.cancel()
model.downloadInfoHashes.remove(downloader.getInfoHash())
core.eventBus.publish(new UIDownloadCancelledEvent(downloader : downloader)) core.eventBus.publish(new UIDownloadCancelledEvent(downloader : downloader))
} }

View File

@@ -20,6 +20,7 @@ import com.muwire.core.files.FileHashedEvent
import com.muwire.core.files.FileLoadedEvent import com.muwire.core.files.FileLoadedEvent
import com.muwire.core.files.FileSharedEvent import com.muwire.core.files.FileSharedEvent
import com.muwire.core.search.QueryEvent import com.muwire.core.search.QueryEvent
import com.muwire.core.search.UIResultBatchEvent
import com.muwire.core.search.UIResultEvent import com.muwire.core.search.UIResultEvent
import com.muwire.core.trust.TrustEvent import com.muwire.core.trust.TrustEvent
import com.muwire.core.trust.TrustService import com.muwire.core.trust.TrustService
@@ -59,11 +60,14 @@ class MainFrameModel {
@Observable int connections @Observable int connections
@Observable String me @Observable String me
@Observable boolean searchButtonsEnabled @Observable boolean downloadActionEnabled
@Observable boolean trustButtonsEnabled
@Observable boolean cancelButtonEnabled @Observable boolean cancelButtonEnabled
@Observable boolean retryButtonEnabled @Observable boolean retryButtonEnabled
private final Set<InfoHash> infoHashes = new HashSet<>() private final Set<InfoHash> infoHashes = new HashSet<>()
private final Set<InfoHash> downloadInfoHashes = new HashSet<>()
volatile Core core volatile Core core
@@ -115,6 +119,7 @@ class MainFrameModel {
core = e.getNewValue() core = e.getNewValue()
me = core.me.getHumanReadableName() me = core.me.getHumanReadableName()
core.eventBus.register(UIResultEvent.class, this) core.eventBus.register(UIResultEvent.class, this)
core.eventBus.register(UIResultBatchEvent.class, this)
core.eventBus.register(DownloadStartedEvent.class, this) core.eventBus.register(DownloadStartedEvent.class, this)
core.eventBus.register(ConnectionEvent.class, this) core.eventBus.register(ConnectionEvent.class, this)
core.eventBus.register(DisconnectionEvent.class, this) core.eventBus.register(DisconnectionEvent.class, this)
@@ -161,9 +166,15 @@ class MainFrameModel {
resultsGroup?.model.handleResult(e) resultsGroup?.model.handleResult(e)
} }
void onUIResultBatchEvent(UIResultBatchEvent e) {
MVCGroup resultsGroup = results.get(e.uuid)
resultsGroup?.model.handleResultBatch(e.results)
}
void onDownloadStartedEvent(DownloadStartedEvent e) { void onDownloadStartedEvent(DownloadStartedEvent e) {
runInsideUIAsync { runInsideUIAsync {
downloads << e downloads << e
downloadInfoHashes.add(e.downloader.infoHash)
} }
} }
@@ -333,4 +344,8 @@ class MainFrameModel {
return destination == other.destination return destination == other.destination
} }
} }
boolean canDownload(InfoHash hash) {
!downloadInfoHashes.contains(hash)
}
} }

View File

@@ -52,4 +52,22 @@ class SearchTabModel {
table.model.fireTableDataChanged() table.model.fireTableDataChanged()
} }
} }
void handleResultBatch(UIResultEvent[] batch) {
runInsideUIAsync {
batch.each {
if (uiSettings.excludeLocalResult && it.sender == core.me)
return
def bucket = hashBucket.get(it.infohash)
if (bucket == null) {
bucket = []
hashBucket[it.infohash] = bucket
}
bucket << it
results << it
}
JTable table = builder.getVariable("results-table")
table.model.fireTableDataChanged()
}
}
} }

View File

@@ -105,9 +105,9 @@ class MainFrameView {
borderLayout() borderLayout()
tabbedPane(id : "result-tabs", constraints: BorderLayout.CENTER) tabbedPane(id : "result-tabs", constraints: BorderLayout.CENTER)
panel(constraints : BorderLayout.SOUTH) { panel(constraints : BorderLayout.SOUTH) {
button(text : "Download", enabled : bind {model.searchButtonsEnabled}, downloadAction) button(text : "Download", enabled : bind {model.downloadActionEnabled}, downloadAction)
button(text : "Trust", enabled: bind {model.searchButtonsEnabled }, trustAction) button(text : "Trust", enabled: bind {model.trustButtonsEnabled }, trustAction)
button(text : "Distrust", enabled : bind {model.searchButtonsEnabled}, distrustAction) button(text : "Distrust", enabled : bind {model.trustButtonsEnabled}, distrustAction)
} }
} }
panel (constraints : JSplitPane.BOTTOM) { panel (constraints : JSplitPane.BOTTOM) {
@@ -120,7 +120,7 @@ class MainFrameView {
closureColumn(header: "Progress", preferredWidth: 20, type: String, read: { row -> closureColumn(header: "Progress", preferredWidth: 20, type: String, read: { row ->
int pieces = row.downloader.nPieces int pieces = row.downloader.nPieces
int done = row.downloader.donePieces() int done = row.downloader.donePieces()
"$done/$pieces pieces" "$done/$pieces pieces".toString()
}) })
closureColumn(header: "Sources", preferredWidth : 10, type: Integer, read : {row -> row.downloader.activeWorkers()}) closureColumn(header: "Sources", preferredWidth : 10, type: Integer, read : {row -> row.downloader.activeWorkers()})
closureColumn(header: "Speed", preferredWidth: 50, type:String, read :{row -> closureColumn(header: "Speed", preferredWidth: 50, type:String, read :{row ->
@@ -160,16 +160,13 @@ class MainFrameView {
scrollPane (constraints : BorderLayout.CENTER) { scrollPane (constraints : BorderLayout.CENTER) {
table(id : "uploads-table") { table(id : "uploads-table") {
tableModel(list : model.uploads) { tableModel(list : model.uploads) {
closureColumn(header : "Name", type : String, read : {row -> row.file.getName() }) closureColumn(header : "Name", type : String, read : {row -> row.getName() })
closureColumn(header : "Progress", type : String, read : { row -> closureColumn(header : "Progress", type : String, read : { row ->
int position = row.getPosition() int percent = row.getProgress()
def range = row.request.getRange()
int total = range.end - range.start
int percent = (int)((position * 100.0) / total)
"$percent%" "$percent%"
}) })
closureColumn(header : "Downloader", type : String, read : { row -> closureColumn(header : "Downloader", type : String, read : { row ->
row.request.downloader?.getHumanReadableName() row.getDownloader()
}) })
} }
} }

View File

@@ -66,7 +66,13 @@ class SearchTabView {
def selectionModel = resultsTable.getSelectionModel() def selectionModel = resultsTable.getSelectionModel()
selectionModel.setSelectionMode(ListSelectionModel.SINGLE_SELECTION) selectionModel.setSelectionMode(ListSelectionModel.SINGLE_SELECTION)
selectionModel.addListSelectionListener( { selectionModel.addListSelectionListener( {
mvcGroup.parentGroup.model.searchButtonsEnabled = true int row = resultsTable.getSelectedRow()
if (row < 0)
return
if (lastSortEvent != null)
row = resultsTable.rowSorter.convertRowIndexToModel(row)
mvcGroup.parentGroup.model.trustButtonsEnabled = true
mvcGroup.parentGroup.model.downloadActionEnabled = mvcGroup.parentGroup.model.canDownload(model.results[row].infohash)
}) })
} }
} }
@@ -105,25 +111,18 @@ class SearchTabView {
resultsTable.rowSorter.setSortsOnUpdates(true) resultsTable.rowSorter.setSortsOnUpdates(true)
JPopupMenu menu = new JPopupMenu()
JMenuItem download = new JMenuItem("Download")
download.addActionListener({mvcGroup.parentGroup.controller.download()})
menu.add(download)
JMenuItem copyHashToClipboard = new JMenuItem("Copy hash to clipboard")
copyHashToClipboard.addActionListener({mvcGroup.view.copyHashToClipboard()})
menu.add(copyHashToClipboard)
resultsTable.addMouseListener(new MouseAdapter() { resultsTable.addMouseListener(new MouseAdapter() {
@Override @Override
public void mouseClicked(MouseEvent e) { public void mouseClicked(MouseEvent e) {
if (e.button == MouseEvent.BUTTON3) if (e.button == MouseEvent.BUTTON3)
showPopupMenu(menu, e) showPopupMenu(e)
else if (e.button == MouseEvent.BUTTON1 && e.clickCount == 2) else if (e.button == MouseEvent.BUTTON1 && e.clickCount == 2)
mvcGroup.parentGroup.controller.download() mvcGroup.parentGroup.controller.download()
} }
@Override @Override
public void mouseReleased(MouseEvent e) { public void mouseReleased(MouseEvent e) {
if (e.button == MouseEvent.BUTTON3) if (e.button == MouseEvent.BUTTON3)
showPopupMenu(menu, e) showPopupMenu(e)
} }
}) })
} }
@@ -135,8 +134,16 @@ class SearchTabView {
mvcGroup.destroy() mvcGroup.destroy()
} }
def showPopupMenu(JPopupMenu menu, MouseEvent e) { def showPopupMenu(MouseEvent e) {
println "showing popup menu" JPopupMenu menu = new JPopupMenu()
if (mvcGroup.parentGroup.model.downloadActionEnabled) {
JMenuItem download = new JMenuItem("Download")
download.addActionListener({mvcGroup.parentGroup.controller.download()})
menu.add(download)
}
JMenuItem copyHashToClipboard = new JMenuItem("Copy hash to clipboard")
copyHashToClipboard.addActionListener({mvcGroup.view.copyHashToClipboard()})
menu.add(copyHashToClipboard)
menu.show(e.getComponent(), e.getX(), e.getY()) menu.show(e.getComponent(), e.getX(), e.getY())
} }