Eric Kok
8 years ago
17 changed files with 570 additions and 4 deletions
@ -0,0 +1,14 @@
@@ -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" |
@ -0,0 +1,45 @@
@@ -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); |
||||
} |
||||
|
||||
} |
@ -0,0 +1,30 @@
@@ -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<Feature> features() { |
||||
return Rtorrent.FEATURES; |
||||
} |
||||
}; |
||||
|
||||
public abstract ClientSpec create(Configuration configuration); |
||||
|
||||
abstract Set<Feature> features(); |
||||
|
||||
public boolean supports(Feature feature) { |
||||
return features().contains(feature); |
||||
} |
||||
|
||||
} |
@ -0,0 +1,13 @@
@@ -0,0 +1,13 @@
|
||||
package org.transdroid.connect.clients; |
||||
|
||||
import org.transdroid.connect.model.Torrent; |
||||
|
||||
import io.reactivex.Flowable; |
||||
|
||||
public interface ClientSpec { |
||||
|
||||
Flowable<String> clientVersion(); |
||||
|
||||
Flowable<Torrent> torrents(); |
||||
|
||||
} |
@ -0,0 +1,11 @@
@@ -0,0 +1,11 @@
|
||||
package org.transdroid.connect.clients; |
||||
|
||||
public enum Feature { |
||||
|
||||
VERSION, |
||||
STARTING, |
||||
STOPPING, |
||||
RESUMING, |
||||
PAUSING |
||||
|
||||
} |
@ -0,0 +1,143 @@
@@ -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<Feature> 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<String> clientVersion() { |
||||
return service.clientVersion(configuration.endpoint(), Nothing.NOTHING); |
||||
} |
||||
|
||||
@Override |
||||
public Flowable<Torrent> 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.<TorrentSpec>asList()) |
||||
.map(new Function<TorrentSpec, Torrent>() { |
||||
@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); |
||||
} |
||||
|
||||
} |
@ -0,0 +1,20 @@
@@ -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<String> clientVersion(@Path("endpoint") String endpoint, @Body Nothing nothing); |
||||
|
||||
@XmlRpc("d.multicall2") |
||||
@POST("{endpoint}") |
||||
Flowable<TorrentSpec[]> torrents(@Path("endpoint") String endpoint, @Body String... fields); |
||||
|
||||
} |
@ -0,0 +1,29 @@
@@ -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; |
||||
|
||||
} |
@ -0,0 +1,100 @@
@@ -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; |
||||
} |
||||
|
||||
} |
@ -0,0 +1,14 @@
@@ -0,0 +1,14 @@
|
||||
package org.transdroid.connect.model; |
||||
|
||||
public enum TorrentStatus { |
||||
|
||||
WAITING, |
||||
CHECKING, |
||||
DOWNLOADING, |
||||
SEEDING, |
||||
PAUSED, |
||||
QUEUED, |
||||
ERROR, |
||||
UNKNOWN; |
||||
|
||||
} |
@ -0,0 +1,50 @@
@@ -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<String, CachingAuthenticator> authCache = new ConcurrentHashMap<>(); |
||||
okhttp.authenticator(new CachingAuthenticatorDecorator(authenticator, authCache)); |
||||
okhttp.addInterceptor(new AuthenticationCacheInterceptor(authCache)); |
||||
} |
||||
|
||||
return okhttp.build(); |
||||
} |
||||
|
||||
} |
@ -0,0 +1,29 @@
@@ -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 <T> FlowableTransformer<T[], T> asList() { |
||||
return new FlowableTransformer<T[], T>() { |
||||
@Override |
||||
public Publisher<T> apply(Flowable<T[]> upstream) { |
||||
return upstream.flatMapIterable(new Function<T[], Iterable<T>>() { |
||||
@Override |
||||
public Iterable<T> apply(T[] ts) throws Exception { |
||||
return Arrays.asList(ts); |
||||
} |
||||
}); |
||||
} |
||||
}; |
||||
} |
||||
|
||||
} |
@ -0,0 +1,11 @@
@@ -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(""); |
||||
} |
||||
|
||||
} |
@ -0,0 +1,57 @@
@@ -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<List<Torrent>>() { |
||||
@Override |
||||
public boolean test(List<Torrent> torrents) throws Exception { |
||||
return torrents.size() > 0; |
||||
} |
||||
}); |
||||
} |
||||
|
||||
} |
@ -1,6 +1,6 @@
@@ -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 |
||||
|
Loading…
Reference in new issue