diff --git a/connect/src/main/java/org/transdroid/connect/Configuration.java b/connect/src/main/java/org/transdroid/connect/Configuration.java index 93ce18bb..2af13409 100644 --- a/connect/src/main/java/org/transdroid/connect/Configuration.java +++ b/connect/src/main/java/org/transdroid/connect/Configuration.java @@ -52,6 +52,7 @@ public final class Configuration { Configuration configuration = new Configuration(client, baseUrl); configuration.endpoint = this.endpoint; configuration.credentials = this.credentials; + configuration.loggingEnabled = this.loggingEnabled; return configuration; } diff --git a/connect/src/main/java/org/transdroid/connect/clients/ClientDelegate.java b/connect/src/main/java/org/transdroid/connect/clients/ClientDelegate.java index 71b1690c..dde37139 100644 --- a/connect/src/main/java/org/transdroid/connect/clients/ClientDelegate.java +++ b/connect/src/main/java/org/transdroid/connect/clients/ClientDelegate.java @@ -2,6 +2,8 @@ package org.transdroid.connect.clients; import org.transdroid.connect.model.Torrent; +import java.io.InputStream; + import io.reactivex.Flowable; /** @@ -33,10 +35,45 @@ final class ClientDelegate implements ClientSpec { } @Override - public Flowable forceStartTorrent() { + public Flowable start(Torrent torrent) { + if (client.supports(Feature.STARTING_STOPPING)) + return ((Feature.StartingStopping) actual).start(torrent); + throw new UnsupportedFeatureException(client, Feature.STARTING_STOPPING); + } + + @Override + public Flowable stop(Torrent torrent) { + if (client.supports(Feature.STARTING_STOPPING)) + return ((Feature.StartingStopping) actual).stop(torrent); + throw new UnsupportedFeatureException(client, Feature.STARTING_STOPPING); + } + + @Override + public Flowable forceStart(Torrent torrent) { if (client.supports(Feature.FORCE_STARTING)) - return ((Feature.ForceStarting) actual).forceStartTorrent(); + return ((Feature.ForceStarting) actual).forceStart(torrent); throw new UnsupportedFeatureException(client, Feature.FORCE_STARTING); } + @Override + public Flowable addByFile(InputStream file) { + if (client.supports(Feature.ADD_BY_FILE)) + return ((Feature.AddByFile) actual).addByFile(file); + throw new UnsupportedFeatureException(client, Feature.ADD_BY_FILE); + } + + @Override + public Flowable addByUrl(String url) { + if (client.supports(Feature.ADD_BY_URL)) + return ((Feature.AddByUrl) actual).addByUrl(url); + throw new UnsupportedFeatureException(client, Feature.ADD_BY_URL); + } + + @Override + public Flowable addByMagnet(String magnet) { + if (client.supports(Feature.ADD_BY_MAGNET)) + return ((Feature.AddByMagnet) actual).addByMagnet(magnet); + throw new UnsupportedFeatureException(client, Feature.ADD_BY_MAGNET); + } + } diff --git a/connect/src/main/java/org/transdroid/connect/clients/ClientSpec.java b/connect/src/main/java/org/transdroid/connect/clients/ClientSpec.java index 0d020b9f..204ba201 100644 --- a/connect/src/main/java/org/transdroid/connect/clients/ClientSpec.java +++ b/connect/src/main/java/org/transdroid/connect/clients/ClientSpec.java @@ -5,6 +5,9 @@ public interface ClientSpec extends Feature.Listing, Feature.StartingStopping, Feature.ResumingPausing, - Feature.ForceStarting { + Feature.ForceStarting, + Feature.AddByFile, + Feature.AddByUrl, + Feature.AddByMagnet { } diff --git a/connect/src/main/java/org/transdroid/connect/clients/Feature.java b/connect/src/main/java/org/transdroid/connect/clients/Feature.java index 580cd0dd..9607470b 100644 --- a/connect/src/main/java/org/transdroid/connect/clients/Feature.java +++ b/connect/src/main/java/org/transdroid/connect/clients/Feature.java @@ -2,6 +2,8 @@ package org.transdroid.connect.clients; import org.transdroid.connect.model.Torrent; +import java.io.InputStream; + import io.reactivex.Flowable; /** @@ -14,7 +16,10 @@ public enum Feature { LISTING(Listing.class), STARTING_STOPPING(StartingStopping.class), RESUMING_PAUSING(ResumingPausing.class), - FORCE_STARTING(ForceStarting.class); + FORCE_STARTING(ForceStarting.class), + ADD_BY_FILE(AddByFile.class), + ADD_BY_URL(AddByUrl.class), + ADD_BY_MAGNET(AddByMagnet.class); private final Class type; @@ -40,6 +45,10 @@ public enum Feature { public interface StartingStopping { + Flowable start(Torrent torrent); + + Flowable stop(Torrent torrent); + } public interface ResumingPausing { @@ -48,7 +57,25 @@ public enum Feature { public interface ForceStarting { - Flowable forceStartTorrent(); + Flowable forceStart(Torrent torrent); + + } + + public interface AddByFile { + + Flowable addByFile(InputStream file); + + } + + public interface AddByUrl { + + Flowable addByUrl(String url); + + } + + public interface AddByMagnet { + + Flowable addByMagnet(String magnet); } diff --git a/connect/src/main/java/org/transdroid/connect/clients/rtorrent/Rtorrent.java b/connect/src/main/java/org/transdroid/connect/clients/rtorrent/Rtorrent.java index 71b6f139..439c96fe 100644 --- a/connect/src/main/java/org/transdroid/connect/clients/rtorrent/Rtorrent.java +++ b/connect/src/main/java/org/transdroid/connect/clients/rtorrent/Rtorrent.java @@ -2,6 +2,7 @@ package org.transdroid.connect.clients.rtorrent; import com.jakewharton.retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory; +import org.reactivestreams.Publisher; import org.transdroid.connect.Configuration; import org.transdroid.connect.clients.Feature; import org.transdroid.connect.model.Torrent; @@ -9,9 +10,11 @@ import org.transdroid.connect.model.TorrentStatus; import org.transdroid.connect.util.OkHttpBuilder; import org.transdroid.connect.util.RxUtil; +import java.io.InputStream; import java.util.Date; import io.reactivex.Flowable; +import io.reactivex.FlowableTransformer; import io.reactivex.functions.Function; import nl.nl2312.xmlrpc.Nothing; import nl.nl2312.xmlrpc.XmlRpcConverterFactory; @@ -21,7 +24,10 @@ public final class Rtorrent implements Feature.Version, Feature.Listing, Feature.StartingStopping, - Feature.ResumingPausing { + Feature.ResumingPausing, + Feature.AddByFile, + Feature.AddByUrl, + Feature.AddByMagnet { private final Configuration configuration; private final Service service; @@ -39,7 +45,8 @@ public final class Rtorrent implements @Override public Flowable clientVersion() { - return service.clientVersion(configuration.endpoint(), Nothing.NOTHING); + return service.clientVersion(configuration.endpoint(), Nothing.NOTHING) + .cache(); // Cached, as it is often used but 'never' changes } @Override @@ -102,6 +109,104 @@ public final class Rtorrent implements }); } + @Override + public Flowable start(final Torrent torrent) { + return service.start( + configuration.endpoint(), + torrent.hash()).map(new Function() { + @Override + public Torrent apply(Void result) throws Exception { + return torrent.mimicStart(); + } + }); + } + + @Override + public Flowable stop(final Torrent torrent) { + return service.stop( + configuration.endpoint(), + torrent.hash()).map(new Function() { + @Override + public Torrent apply(Void result) throws Exception { + return torrent.mimicStart(); + } + }); + } + + @Override + public Flowable addByFile(InputStream file) { + // TODO + return null; + } + + @Override + public Flowable addByUrl(final String url) { + return clientVersion().compose(clientVersionAsInt).flatMap(new Function>() { + @Override + public Publisher apply(Integer integer) throws Exception { + if (integer > 904) { + return service.loadStart( + configuration.endpoint(), + "", + url); + } else { + return service.loadStart( + configuration.endpoint(), + url); + } + } + }).map(new Function() { + @Override + public Void apply(Integer integer) throws Exception { + return null; + } + }); + } + + @Override + public Flowable addByMagnet(final String magnet) { + return clientVersion().compose(clientVersionAsInt).flatMap(new Function>() { + @Override + public Publisher apply(Integer integer) throws Exception { + if (integer > 904) { + return service.loadStart( + configuration.endpoint(), + "", + magnet); + } else { + return service.loadStart( + configuration.endpoint(), + magnet); + } + } + }).map(new Function() { + @Override + public Void apply(Integer integer) throws Exception { + return null; + } + }); + } + + private FlowableTransformer clientVersionAsInt = new FlowableTransformer() { + @Override + public Publisher apply(Flowable version) { + return version.map(new Function() { + @Override + public Integer apply(String version) throws Exception { + if (version == null) + return 10000; + try { + String[] versionParts = version.split("\\."); + return (Integer.parseInt(versionParts[0]) * 10000) + (Integer.parseInt(versionParts[1]) * 100) + Integer.parseInt + (versionParts[2]); + } catch (NumberFormatException e) { + return 10000; + } + } + }); + } + }; + private TorrentStatus torrentStatus(long state, long complete, long active, long checking) { if (state == 0) { return TorrentStatus.QUEUED; diff --git a/connect/src/main/java/org/transdroid/connect/clients/rtorrent/Service.java b/connect/src/main/java/org/transdroid/connect/clients/rtorrent/Service.java index 9b662cf2..2e1c5d6b 100644 --- a/connect/src/main/java/org/transdroid/connect/clients/rtorrent/Service.java +++ b/connect/src/main/java/org/transdroid/connect/clients/rtorrent/Service.java @@ -15,6 +15,18 @@ interface Service { @XmlRpc("d.multicall2") @POST("{endpoint}") - Flowable torrents(@Path("endpoint") String endpoint, @Body String... fields); + Flowable torrents(@Path("endpoint") String endpoint, @Body String... args); + + @XmlRpc("d.start") + @POST("{endpoint}") + Flowable start(@Path("endpoint") String endpoint, @Body String hash); + + @XmlRpc("d.stop") + @POST("{endpoint}") + Flowable stop(@Path("endpoint") String endpoint, @Body String hash); + + @XmlRpc("load.start") + @POST("{endpoint}") + Flowable loadStart(@Path("endpoint") String endpoint, @Body String... args); } diff --git a/connect/src/main/java/org/transdroid/connect/model/Torrent.java b/connect/src/main/java/org/transdroid/connect/model/Torrent.java index 599996be..75a6a238 100644 --- a/connect/src/main/java/org/transdroid/connect/model/Torrent.java +++ b/connect/src/main/java/org/transdroid/connect/model/Torrent.java @@ -86,7 +86,7 @@ public final class Torrent { cal.set(1900, Calendar.DECEMBER, 31); this.dateDone = cal.getTime(); } else if (eta == -1 || eta == -2) { - // UNknown eta: move to the top of the list + // Unknown eta: move to the top of the list this.dateDone = new Date(Long.MAX_VALUE); } else { Calendar cal = Calendar.getInstance(); @@ -97,4 +97,212 @@ public final class Torrent { this.error = error; } + public long id() { + return id; + } + + public String hash() { + return hash; + } + + public String name() { + return name; + } + + public TorrentStatus statusCode() { + return statusCode; + } + + public String locationDir() { + return locationDir; + } + + public int rateDownload() { + return rateDownload; + } + + public int rateUpload() { + return rateUpload; + } + + public int seedersConnected() { + return seedersConnected; + } + + public int seedersKnown() { + return seedersKnown; + } + + public int leechersConnected() { + return leechersConnected; + } + + public int leechersKnown() { + return leechersKnown; + } + + public long eta() { + return eta; + } + + public long downloadedEver() { + return downloadedEver; + } + + public long uploadedEver() { + return uploadedEver; + } + + public long totalSize() { + return totalSize; + } + + public float partDone() { + return partDone; + } + + public float available() { + return available; + } + + public String label() { + return label; + } + + public Date dateAdded() { + return dateAdded; + } + + public Date dateDone() { + return dateDone; + } + + public String error() { + return error; + } + + /** + * Returns the unique torrent-specific id, which is the torrent's hash or (if not available) the local index number + * @return The torrent's (session-transient) unique id + */ + public String uniqueId() { + if (this.hash == null) { + return Long.toString(this.id); + } else { + return this.hash; + } + } + + /** + * Gives the upload/download seed ratio. + * @return The ratio in range [0,r] + */ + public double ratio() { + return ((double) uploadedEver) / ((double) downloadedEver); + } + + /** + * Gives the percentage of the download that is completed + * @return The downloaded percentage in range [0,1] + */ + public float downloadedPercentage() { + return partDone; + } + + /** + * Returns whether this torrents is actively downloading or not. + * @param dormantAsInactive If true, dormant (0KB/s, so no data transfer) torrents are not considered actively downloading + * @return True if this torrent is to be treated as being in a downloading state, that is, it is trying to finish a download + */ + public boolean isDownloading(boolean dormantAsInactive) { + return statusCode == TorrentStatus.DOWNLOADING && (!dormantAsInactive || rateDownload > 0); + } + + /** + * Returns whether this torrents is actively seeding or not. + * @param dormantAsInactive If true, dormant (0KB/s, so no data transfer) torrents are not considered actively seeding + * @return True if this torrent is to be treated as being in a seeding state, that is, it is sending data to leechers + */ + public boolean isSeeding(boolean dormantAsInactive) { + return statusCode == TorrentStatus.SEEDING && (!dormantAsInactive || rateUpload > 0); + } + + /** + * Indicates if the torrent can be paused at this moment + * @return If it can be paused + */ + public boolean canPause() { + // Can pause when it is downloading or seeding + return statusCode == TorrentStatus.DOWNLOADING || statusCode == TorrentStatus.SEEDING; + } + + /** + * Indicates whether the torrent can be resumed + * @return If it can be resumed + */ + public boolean canResume() { + // Can resume when it is paused + return statusCode == TorrentStatus.PAUSED; + } + + /** + * Indicates if the torrent can be started at this moment + * @return If it can be started + */ + public boolean canStart() { + // Can start when it is queued + return statusCode == TorrentStatus.QUEUED; + } + + /** + * Indicates whether the torrent can be stopped + * @return If it can be stopped + */ + public boolean canStop() { + // Can stop when it is downloading or seeding or paused + return statusCode == TorrentStatus.DOWNLOADING || statusCode == TorrentStatus.SEEDING + || statusCode == TorrentStatus.PAUSED; + } + + public Torrent mimicResume() { + return mimicStatus(downloadedPercentage() >= 1 ? TorrentStatus.SEEDING : TorrentStatus.DOWNLOADING); + } + + public Torrent mimicPause() { + return mimicStatus(TorrentStatus.PAUSED); + } + + public Torrent mimicStart() { + return mimicStatus(downloadedPercentage() >= 1 ? TorrentStatus.SEEDING : TorrentStatus.DOWNLOADING); + } + + public Torrent mimicStop() { + return mimicStatus(TorrentStatus.QUEUED); + } + + public Torrent mimicNewLabel(String newLabel) { + return new Torrent(id, hash, name, statusCode, locationDir, rateDownload, rateUpload, seedersConnected, seedersKnown, leechersConnected, + leechersKnown, eta, downloadedEver, uploadedEver, totalSize, partDone, available, newLabel, dateAdded, dateDone, error); + } + + public Torrent mimicChecking() { + return mimicStatus(TorrentStatus.CHECKING); + } + + public Torrent mimicNewLocation(String newLocation) { + return new Torrent(id, hash, name, statusCode, newLocation, rateDownload, rateUpload, seedersConnected, seedersKnown, leechersConnected, + leechersKnown, eta, downloadedEver, uploadedEver, totalSize, partDone, available, label, dateAdded, dateDone, error); + } + + @Override + public String toString() { + // (HASH_OR_ID) NAME + return "(" + uniqueId() + ") " + name; + } + + private Torrent mimicStatus(TorrentStatus newStatus) { + return new Torrent(id, hash, name, newStatus, locationDir, rateDownload, rateUpload, seedersConnected, seedersKnown, leechersConnected, + leechersKnown, eta, downloadedEver, uploadedEver, totalSize, partDone, available, label, dateAdded, dateDone, error); + } + } diff --git a/connect/src/test/java/org/transdroid/connect/clients/rtorrent/RtorrentTest.java b/connect/src/test/java/org/transdroid/connect/clients/rtorrent/RtorrentTest.java index 7a04b756..3df7d981 100644 --- a/connect/src/test/java/org/transdroid/connect/clients/rtorrent/RtorrentTest.java +++ b/connect/src/test/java/org/transdroid/connect/clients/rtorrent/RtorrentTest.java @@ -24,7 +24,8 @@ public final class RtorrentTest { public void setUp() { rtorrent = new Configuration.Builder(Client.RTORRENT) .baseUrl("http://localhost:8008/") - .endpoint("/RPC2") + .endpoint("RPC2") + .loggingEnabled(true) .build() .createClient(); } @@ -32,9 +33,13 @@ public final class RtorrentTest { @Test public void features() { assertThat(Client.RTORRENT.supports(Feature.VERSION)).isTrue(); + assertThat(Client.RTORRENT.supports(Feature.LISTING)).isTrue(); assertThat(Client.RTORRENT.supports(Feature.STARTING_STOPPING)).isTrue(); assertThat(Client.RTORRENT.supports(Feature.RESUMING_PAUSING)).isTrue(); assertThat(Client.RTORRENT.supports(Feature.FORCE_STARTING)).isFalse(); + assertThat(Client.RTORRENT.supports(Feature.ADD_BY_FILE)).isTrue(); + assertThat(Client.RTORRENT.supports(Feature.ADD_BY_URL)).isTrue(); + assertThat(Client.RTORRENT.supports(Feature.ADD_BY_MAGNET)).isTrue(); } @Test @@ -57,9 +62,15 @@ public final class RtorrentTest { }); } + @Test + public void addByMagnet() throws IOException { + rtorrent.addByMagnet("http://torrent.ubuntu.com:6969/file?info_hash=%04%03%FBG%28%BDx%8F%BC%B6%7E%87%D6%FE%B2A%EF8%C7Z") + .test(); + } + @Test(expected = UnsupportedFeatureException.class) public void forceStart() throws IOException { - rtorrent.forceStartTorrent() + rtorrent.forceStart(null) .test(); }