diff --git a/build.gradle b/build.gradle index 5657e594..1017cb40 100644 --- a/build.gradle +++ b/build.gradle @@ -3,7 +3,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:2.1.0' + classpath 'com.android.tools.build:gradle:2.2.3' classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8' } } diff --git a/connect/build.gradle b/connect/build.gradle new file mode 100644 index 00000000..530b406c --- /dev/null +++ b/connect/build.gradle @@ -0,0 +1,14 @@ +apply plugin: 'java' + +dependencies { + compile 'com.jakewharton.retrofit:retrofit2-rxjava2-adapter:1.0.0' + compile 'com.squareup.okhttp3:logging-interceptor:3.5.0' + compile 'com.github.erickok:retrofit-xmlrpc:master-SNAPSHOT' + compile 'com.burgstaller:okhttp-digest:1.10' + + testCompile 'junit:junit:4.12' + testCompile 'com.google.truth:truth:0.31' +} + +sourceCompatibility = "1.7" +targetCompatibility = "1.7" diff --git a/connect/src/main/java/org/transdroid/connect/Configuration.java b/connect/src/main/java/org/transdroid/connect/Configuration.java new file mode 100644 index 00000000..6ab67213 --- /dev/null +++ b/connect/src/main/java/org/transdroid/connect/Configuration.java @@ -0,0 +1,45 @@ +package org.transdroid.connect; + +import com.burgstaller.okhttp.digest.Credentials; + +import org.transdroid.connect.clients.Client; +import org.transdroid.connect.clients.ClientSpec; +import org.transdroid.connect.util.StringUtil; + +public final class Configuration { + + private final Client client; + private final String baseUrl; + private final String endpoint; + private final Credentials credentials; + private final boolean loggingEnabled; + + public Configuration(Client client, String baseUrl, String endpoint, String user, String password, boolean loggingEnabled) { + this.client = client; + this.baseUrl = baseUrl; + this.endpoint = endpoint; + this.credentials = (!StringUtil.isEmpty(user) && password != null) ? new Credentials(user, password) : null; + this.loggingEnabled = loggingEnabled; + } + + public String baseUrl() { + return baseUrl; + } + + public String endpoint() { + return endpoint; + } + + public boolean loggingEnabled() { + return loggingEnabled; + } + + public Credentials credentials() { + return credentials; + } + + public ClientSpec create() { + return client.create(this); + } + +} diff --git a/connect/src/main/java/org/transdroid/connect/clients/Client.java b/connect/src/main/java/org/transdroid/connect/clients/Client.java new file mode 100644 index 00000000..e00b6a5f --- /dev/null +++ b/connect/src/main/java/org/transdroid/connect/clients/Client.java @@ -0,0 +1,30 @@ +package org.transdroid.connect.clients; + +import org.transdroid.connect.Configuration; +import org.transdroid.connect.clients.rtorrent.Rtorrent; + +import java.util.Set; + +public enum Client { + + RTORRENT { + @Override + public ClientSpec create(Configuration configuration) { + return new Rtorrent(configuration); + } + + @Override + Set features() { + return Rtorrent.FEATURES; + } + }; + + public abstract ClientSpec create(Configuration configuration); + + abstract Set features(); + + public boolean supports(Feature feature) { + return features().contains(feature); + } + +} diff --git a/connect/src/main/java/org/transdroid/connect/clients/ClientSpec.java b/connect/src/main/java/org/transdroid/connect/clients/ClientSpec.java new file mode 100644 index 00000000..accf2bc1 --- /dev/null +++ b/connect/src/main/java/org/transdroid/connect/clients/ClientSpec.java @@ -0,0 +1,13 @@ +package org.transdroid.connect.clients; + +import org.transdroid.connect.model.Torrent; + +import io.reactivex.Flowable; + +public interface ClientSpec { + + Flowable clientVersion(); + + Flowable torrents(); + +} diff --git a/connect/src/main/java/org/transdroid/connect/clients/Feature.java b/connect/src/main/java/org/transdroid/connect/clients/Feature.java new file mode 100644 index 00000000..8b7f736e --- /dev/null +++ b/connect/src/main/java/org/transdroid/connect/clients/Feature.java @@ -0,0 +1,11 @@ +package org.transdroid.connect.clients; + +public enum Feature { + + VERSION, + STARTING, + STOPPING, + RESUMING, + PAUSING + +} 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 new file mode 100644 index 00000000..eb82cbdd --- /dev/null +++ b/connect/src/main/java/org/transdroid/connect/clients/rtorrent/Rtorrent.java @@ -0,0 +1,143 @@ +package org.transdroid.connect.clients.rtorrent; + +import com.jakewharton.retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory; + +import org.transdroid.connect.Configuration; +import org.transdroid.connect.clients.ClientSpec; +import org.transdroid.connect.clients.Feature; +import org.transdroid.connect.model.Torrent; +import org.transdroid.connect.model.TorrentStatus; +import org.transdroid.connect.util.OkHttpBuilder; +import org.transdroid.connect.util.RxUtil; + +import java.util.Date; +import java.util.HashSet; +import java.util.Set; + +import io.reactivex.Flowable; +import io.reactivex.functions.Function; +import nl.nl2312.xmlrpc.Nothing; +import nl.nl2312.xmlrpc.XmlRpcConverterFactory; +import retrofit2.Retrofit; + +public final class Rtorrent implements ClientSpec { + + public static final Set FEATURES = new HashSet<>(); + + { + FEATURES.add(Feature.VERSION); + FEATURES.add(Feature.STARTING); + FEATURES.add(Feature.STOPPING); + FEATURES.add(Feature.RESUMING); + FEATURES.add(Feature.PAUSING); + } + + private final Configuration configuration; + private final Service service; + + public Rtorrent(Configuration configuration) { + this.configuration = configuration; + Retrofit retrofit = new Retrofit.Builder() + .baseUrl(configuration.baseUrl()) + .client(new OkHttpBuilder(configuration).build()) + .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) + .addConverterFactory(XmlRpcConverterFactory.create()) + .build(); + this.service = retrofit.create(Service.class); + } + + @Override + public Flowable clientVersion() { + return service.clientVersion(configuration.endpoint(), Nothing.NOTHING); + } + + @Override + public Flowable torrents() { + return service.torrents( + configuration.endpoint(), + "", + "main", + "d.hash=", + "d.name=", + "d.state=", + "d.down.rate=", + "d.up.rate=", + "d.peers_connected=", + "d.peers_not_connected=", + "d.bytes_done=", + "d.up.total=", + "d.size_bytes=", + "d.left_bytes=", + "d.creation_date=", + "d.complete=", + "d.is_active=", + "d.is_hash_checking=", + "d.base_path=", + "d.base_filename=", + "d.message=", + "d.custom=addtime", + "d.custom=seedingtime", + "d.custom1=", + "d.peers_complete=", + "d.peers_accounted=") + .compose(RxUtil.asList()) + .map(new Function() { + @Override + public Torrent apply(TorrentSpec torrentSpec) throws Exception { + return new Torrent( + torrentSpec.hash.hashCode(), + torrentSpec.hash, + torrentSpec.name, + torrentStatus(torrentSpec.state, torrentSpec.isComplete, torrentSpec.isActive, torrentSpec.isHashChecking), + torrentSpec.basePath.substring(0, torrentSpec.basePath.indexOf(torrentSpec.baseFilename)), + (int) torrentSpec.downloadRate, + (int) torrentSpec.uploadRate, + (int) torrentSpec.seedersConnected, + (int) (torrentSpec.peersConnected + torrentSpec.peersNotConnected), + (int) torrentSpec.leechersConnected, + (int) (torrentSpec.peersConnected + torrentSpec.peersNotConnected), + torrentSpec.downloadRate > 0 ? (torrentSpec.bytesleft / torrentSpec.downloadRate) : Torrent.UNKNOWN, + torrentSpec.bytesDone, + torrentSpec.bytesUploaded, + torrentSpec.bytesTotal, + torrentSpec.bytesDone / torrentSpec.bytesTotal, + 0F, + torrentSpec.label, + torrentTimeAdded(torrentSpec.timeAdded, torrentSpec.timeCreated), + torrentTimeFinished(torrentSpec.timeFinished), + torrentSpec.errorMessage + ); + } + }); + } + + private TorrentStatus torrentStatus(long state, long complete, long active, long checking) { + if (state == 0) { + return TorrentStatus.QUEUED; + } else if (active == 1) { + if (complete == 1) { + return TorrentStatus.SEEDING; + } else { + return TorrentStatus.DOWNLOADING; + } + } else if (checking == 1) { + return TorrentStatus.CHECKING; + } else { + return TorrentStatus.PAUSED; + } + } + + private Date torrentTimeAdded(String timeAdded, long timeCreated) { + if (timeAdded != null || timeAdded.trim().length() != 0) { + return new Date(Long.parseLong(timeAdded.trim()) * 1000L); + } + return new Date(timeCreated * 1000L); + } + + private Date torrentTimeFinished(String timeFinished) { + if (timeFinished == null || timeFinished.trim().length() == 0) + return null; + return new Date(Long.parseLong(timeFinished.trim()) * 1000L); + } + +} 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 new file mode 100644 index 00000000..9b662cf2 --- /dev/null +++ b/connect/src/main/java/org/transdroid/connect/clients/rtorrent/Service.java @@ -0,0 +1,20 @@ +package org.transdroid.connect.clients.rtorrent; + +import io.reactivex.Flowable; +import nl.nl2312.xmlrpc.Nothing; +import nl.nl2312.xmlrpc.XmlRpc; +import retrofit2.http.Body; +import retrofit2.http.POST; +import retrofit2.http.Path; + +interface Service { + + @XmlRpc("system.client_version") + @POST("{endpoint}") + Flowable clientVersion(@Path("endpoint") String endpoint, @Body Nothing nothing); + + @XmlRpc("d.multicall2") + @POST("{endpoint}") + Flowable torrents(@Path("endpoint") String endpoint, @Body String... fields); + +} diff --git a/connect/src/main/java/org/transdroid/connect/clients/rtorrent/TorrentSpec.java b/connect/src/main/java/org/transdroid/connect/clients/rtorrent/TorrentSpec.java new file mode 100644 index 00000000..8cb08678 --- /dev/null +++ b/connect/src/main/java/org/transdroid/connect/clients/rtorrent/TorrentSpec.java @@ -0,0 +1,29 @@ +package org.transdroid.connect.clients.rtorrent; + +public final class TorrentSpec { + + public String hash; + public String name; + public long state; + public long downloadRate; + public long uploadRate; + public long peersConnected; + public long peersNotConnected; + public long bytesDone; + public long bytesUploaded; + public long bytesTotal; + public long bytesleft; + public long timeCreated; + public long isComplete; + public long isActive; + public long isHashChecking; + public String basePath; + public String baseFilename; + public String errorMessage; + public String timeAdded; + public String timeFinished; + public String label; + public long seedersConnected; + public long leechersConnected; + +} diff --git a/connect/src/main/java/org/transdroid/connect/model/Torrent.java b/connect/src/main/java/org/transdroid/connect/model/Torrent.java new file mode 100644 index 00000000..599996be --- /dev/null +++ b/connect/src/main/java/org/transdroid/connect/model/Torrent.java @@ -0,0 +1,100 @@ +package org.transdroid.connect.model; + +import java.util.Calendar; +import java.util.Date; + +public final class Torrent { + + public static final long UNKNOWN = -1L; + + private final long id; + private final String hash; + private final String name; + private final TorrentStatus statusCode; + private final String locationDir; + + private final int rateDownload; + private final int rateUpload; + private final int seedersConnected; + private final int seedersKnown; + private final int leechersConnected; + private final int leechersKnown; + private final long eta; + + private final long downloadedEver; + private final long uploadedEver; + private final long totalSize; + private final float partDone; + private final float available; + private final String label; + + private final Date dateAdded; + private final Date dateDone; + private final String error; + + public Torrent(long id, + String hash, + String name, + TorrentStatus statusCode, + String locationDir, + int rateDownload, + int rateUpload, + int seedersConnected, + int seedersKnown, + int leechersConnected, + int leechersKnown, + long eta, + long downloadedEver, + long uploadedEver, + long totalSize, + float partDone, + float available, + String label, + Date dateAdded, + Date realDateDone, + String error) { + + this.id = id; + this.hash = hash; + this.name = name; + this.statusCode = statusCode; + this.locationDir = locationDir; + + this.rateDownload = rateDownload; + this.rateUpload = rateUpload; + this.seedersConnected = seedersConnected; + this.seedersKnown = seedersKnown; + this.leechersConnected = leechersConnected; + this.leechersKnown = leechersKnown; + this.eta = eta; + + this.downloadedEver = downloadedEver; + this.uploadedEver = uploadedEver; + this.totalSize = totalSize; + this.partDone = partDone; + this.available = available; + this.label = label; + + this.dateAdded = dateAdded; + if (realDateDone != null) { + this.dateDone = realDateDone; + } else { + if (this.partDone == 1) { + // Finished but no finished date: set so move to bottom of list + Calendar cal = Calendar.getInstance(); + cal.clear(); + 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 + this.dateDone = new Date(Long.MAX_VALUE); + } else { + Calendar cal = Calendar.getInstance(); + cal.add(Calendar.SECOND, (int) eta); + this.dateDone = cal.getTime(); + } + } + this.error = error; + } + +} diff --git a/connect/src/main/java/org/transdroid/connect/model/TorrentStatus.java b/connect/src/main/java/org/transdroid/connect/model/TorrentStatus.java new file mode 100644 index 00000000..047478b3 --- /dev/null +++ b/connect/src/main/java/org/transdroid/connect/model/TorrentStatus.java @@ -0,0 +1,14 @@ +package org.transdroid.connect.model; + +public enum TorrentStatus { + + WAITING, + CHECKING, + DOWNLOADING, + SEEDING, + PAUSED, + QUEUED, + ERROR, + UNKNOWN; + +} diff --git a/connect/src/main/java/org/transdroid/connect/util/OkHttpBuilder.java b/connect/src/main/java/org/transdroid/connect/util/OkHttpBuilder.java new file mode 100644 index 00000000..79b2fae9 --- /dev/null +++ b/connect/src/main/java/org/transdroid/connect/util/OkHttpBuilder.java @@ -0,0 +1,50 @@ +package org.transdroid.connect.util; + +import com.burgstaller.okhttp.AuthenticationCacheInterceptor; +import com.burgstaller.okhttp.CachingAuthenticatorDecorator; +import com.burgstaller.okhttp.DispatchingAuthenticator; +import com.burgstaller.okhttp.basic.BasicAuthenticator; +import com.burgstaller.okhttp.digest.CachingAuthenticator; +import com.burgstaller.okhttp.digest.DigestAuthenticator; + +import org.transdroid.connect.Configuration; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import okhttp3.OkHttpClient; +import okhttp3.logging.HttpLoggingInterceptor; + +public final class OkHttpBuilder { + + private final Configuration configuration; + + public OkHttpBuilder(Configuration configuration) { + this.configuration = configuration; + } + + public OkHttpClient build() { + OkHttpClient.Builder okhttp = new OkHttpClient.Builder(); + + if (configuration.loggingEnabled()) { + HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor(); + loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY); + okhttp.addInterceptor(loggingInterceptor); + } + if (configuration.credentials() != null) { + BasicAuthenticator basicAuthenticator = new BasicAuthenticator(configuration.credentials()); + DigestAuthenticator digestAuthenticator = new DigestAuthenticator(configuration.credentials()); + DispatchingAuthenticator authenticator = new DispatchingAuthenticator.Builder() + .with("digest", digestAuthenticator) + .with("basic", basicAuthenticator) + .build(); + + Map authCache = new ConcurrentHashMap<>(); + okhttp.authenticator(new CachingAuthenticatorDecorator(authenticator, authCache)); + okhttp.addInterceptor(new AuthenticationCacheInterceptor(authCache)); + } + + return okhttp.build(); + } + +} diff --git a/connect/src/main/java/org/transdroid/connect/util/RxUtil.java b/connect/src/main/java/org/transdroid/connect/util/RxUtil.java new file mode 100644 index 00000000..85b4bd18 --- /dev/null +++ b/connect/src/main/java/org/transdroid/connect/util/RxUtil.java @@ -0,0 +1,29 @@ +package org.transdroid.connect.util; + +import org.reactivestreams.Publisher; + +import java.util.Arrays; + +import io.reactivex.Flowable; +import io.reactivex.FlowableTransformer; +import io.reactivex.functions.Function; + +public final class RxUtil { + + private RxUtil() {} + + public static FlowableTransformer asList() { + return new FlowableTransformer() { + @Override + public Publisher apply(Flowable upstream) { + return upstream.flatMapIterable(new Function>() { + @Override + public Iterable apply(T[] ts) throws Exception { + return Arrays.asList(ts); + } + }); + } + }; + } + +} diff --git a/connect/src/main/java/org/transdroid/connect/util/StringUtil.java b/connect/src/main/java/org/transdroid/connect/util/StringUtil.java new file mode 100644 index 00000000..0d52529a --- /dev/null +++ b/connect/src/main/java/org/transdroid/connect/util/StringUtil.java @@ -0,0 +1,11 @@ +package org.transdroid.connect.util; + +public final class StringUtil { + + private StringUtil() {} + + public static boolean isEmpty(String string) { + return string == null || string.equals(""); + } + +} 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 new file mode 100644 index 00000000..2cad4806 --- /dev/null +++ b/connect/src/test/java/org/transdroid/connect/clients/rtorrent/RtorrentTest.java @@ -0,0 +1,57 @@ +package org.transdroid.connect.clients.rtorrent; + +import org.junit.Before; +import org.junit.Test; +import org.transdroid.connect.Configuration; +import org.transdroid.connect.clients.Client; +import org.transdroid.connect.clients.ClientSpec; +import org.transdroid.connect.clients.Feature; +import org.transdroid.connect.model.Torrent; + +import java.io.IOException; +import java.util.List; + +import io.reactivex.functions.Predicate; + +import static com.google.common.truth.Truth.assertThat; + +public final class RtorrentTest { + + private ClientSpec rtorrent; + + @Before + public void setUp() { + Configuration configuration = new Configuration(Client.RTORRENT, "http://localhost:8008/", "RPC2", null, null, true); + rtorrent = configuration.create(); + } + + @Test + public void features() { + assertThat(Client.RTORRENT.supports(Feature.VERSION)).isTrue(); + assertThat(Client.RTORRENT.supports(Feature.STARTING)).isTrue(); + assertThat(Client.RTORRENT.supports(Feature.STOPPING)).isTrue(); + assertThat(Client.RTORRENT.supports(Feature.RESUMING)).isTrue(); + assertThat(Client.RTORRENT.supports(Feature.PAUSING)).isTrue(); + } + + @Test + public void clientVersion() throws IOException { + rtorrent.clientVersion() + .test() + .assertValue("0.9.6"); + } + + @Test + public void torrents() throws IOException { + rtorrent.torrents() + .toList() + .test() + .assertValue(new Predicate>() { + @Override + public boolean test(List torrents) throws Exception { + return torrents.size() > 0; + } + }); + } + +} diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 2890231d..54c78fce 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Wed Jan 20 12:20:00 CET 2016 +#Sat Jan 21 11:09:39 CET 2017 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.10-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip diff --git a/settings.gradle b/settings.gradle index e7b4def4..35b59efd 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1 @@ -include ':app' +include ':app', ':connect'