Merge pull request #221 from anthonyraymond/upload-ratio

Stop seeding when target ratio is reached
This commit is contained in:
Anthony Raymond 2023-11-01 11:10:24 +01:00 committed by GitHub
commit d4d1dce50f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 88 additions and 41 deletions

View file

@ -101,7 +101,8 @@ The application configuration belongs in `joal-conf/config.json`.
"maxUploadRate" : 160,
"simultaneousSeed" : 20,
"client" : "qbittorrent-3.3.16.client",
"keepTorrentWithZeroLeechers" : true
"keepTorrentWithZeroLeechers" : true,
"uploadRatioTarget": -1.0
}
```
- `minUploadRate` : The minimum uploadRate you want to fake (in kB/s) (**required**)
@ -109,6 +110,7 @@ The application configuration belongs in `joal-conf/config.json`.
- `simultaneousSeed` : How many torrents should be seeding at the same time (**required**)
- `client` : The name of the .client file to use in `joal-conf/clients/` (**required**)
- `keepTorrentWithZeroLeechers`: should JOAL keep torrent with no leechers or seeders. If yes, torrent with no peers will be seed at 0kB/s. If false torrents will be deleted on 0 peers reached. (**required**)
- `uploadRatioTarget`: when JOAL has uploaded X times the size of the torrent **in a single session**, the torrent is removed. If -1.0 torrents are never removed.

View file

@ -3,5 +3,6 @@
"maxUploadRate": 170,
"simultaneousSeed": 5,
"client": "utorrent-3.5.0_43916.client",
"keepTorrentWithZeroLeechers": true
"keepTorrentWithZeroLeechers": true,
"uploadRatioTarget": -1.0
}

View file

@ -132,7 +132,7 @@ public class SeedManager {
.withAppConfiguration(appConfig)
.withTorrentFileProvider(this.torrentFileProvider)
.withBandwidthDispatcher(this.bandwidthDispatcher)
.withAnnouncerFactory(new AnnouncerFactory(announceDataAccessor, httpClient))
.withAnnouncerFactory(new AnnouncerFactory(announceDataAccessor, httpClient, appConfig))
.withEventPublisher(this.appEventPublisher)
.withDelayQueue(new DelayQueue<>())
.build();

View file

@ -20,6 +20,7 @@ public class AppConfiguration {
private final int simultaneousSeed;
private final String client;
private final boolean keepTorrentWithZeroLeechers;
private final float uploadRatioTarget;
@JsonCreator
public AppConfiguration(
@ -27,13 +28,15 @@ public class AppConfiguration {
@JsonProperty(value = "maxUploadRate", required = true) final long maxUploadRate,
@JsonProperty(value = "simultaneousSeed", required = true) final int simultaneousSeed,
@JsonProperty(value = "client", required = true) final String client,
@JsonProperty(value = "keepTorrentWithZeroLeechers", required = true) final boolean keepTorrentWithZeroLeechers
@JsonProperty(value = "keepTorrentWithZeroLeechers", required = true) final boolean keepTorrentWithZeroLeechers,
@JsonProperty(value = "uploadRatioTarget", defaultValue = "-1.0", required = false) final float uploadRatioTarget
) {
this.minUploadRate = minUploadRate;
this.maxUploadRate = maxUploadRate;
this.simultaneousSeed = simultaneousSeed;
this.client = client;
this.keepTorrentWithZeroLeechers = keepTorrentWithZeroLeechers;
this.uploadRatioTarget = uploadRatioTarget;
validate();
}
@ -56,5 +59,9 @@ public class AppConfiguration {
if (StringUtils.isBlank(client)) {
throw new AppConfigurationIntegrityException("client is required, no file name given");
}
if (uploadRatioTarget < 0f && uploadRatioTarget != -1f){
throw new AppConfigurationIntegrityException("uploadRatioTarget must be greater than 0 (or equal to -1)");
}
}
}

View file

@ -3,6 +3,7 @@ package org.araymond.joal.core.ttorrent.client;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.turn.ttorrent.common.protocol.TrackerMessage.AnnounceRequestMessage.RequestEvent;
import lombok.extern.slf4j.Slf4j;
import org.araymond.joal.core.config.AppConfiguration;
import org.araymond.joal.core.events.torrent.files.TorrentFileAddedEvent;
import org.araymond.joal.core.events.torrent.files.TorrentFileDeletedEvent;
@ -36,6 +37,7 @@ import static java.util.stream.Collectors.toSet;
* <li>implements {@link TorrentFileChangeAware} to react to torrent file changes in filesystem</li>
* </ul>
*/
@Slf4j
public class Client implements TorrentFileChangeAware, ClientFacade {
private final AppConfiguration appConfig;
private final TorrentFileProvider torrentFileProvider;
@ -181,6 +183,11 @@ public class Client implements TorrentFileChangeAware, ClientFacade {
}
}
public void onUploadRatioLimitReached(final InfoHash infoHash) {
log.info("Deleting torrent [{}] since ratio has been met", infoHash);
this.torrentFileProvider.moveToArchiveFolder(infoHash);
}
public void onTorrentHasStopped(final Announcer stoppedAnnouncer) {
if (this.stop) {
this.currentlySeedingAnnouncers.remove(stoppedAnnouncer);
@ -242,4 +249,5 @@ public class Client implements TorrentFileChangeAware, ClientFacade {
lock.unlock();
}
}
}

View file

@ -35,11 +35,14 @@ public class Announcer implements AnnouncerFacade {
@Getter private final MockedTorrent torrent;
private TrackerClient trackerClient;
private final AnnounceDataAccessor announceDataAccessor;
private long reportedUploadBytes = 0L;
private final float uploadRatioTarget;
Announcer(final MockedTorrent torrent, final AnnounceDataAccessor announceDataAccessor, final HttpClient httpClient) {
Announcer(final MockedTorrent torrent, final AnnounceDataAccessor announceDataAccessor, final HttpClient httpClient, final float uploadRatioTarget) {
this.torrent = torrent;
this.trackerClient = this.buildTrackerClient(torrent, httpClient);
this.announceDataAccessor = announceDataAccessor;
this.uploadRatioTarget = uploadRatioTarget;
}
private TrackerClient buildTrackerClient(final MockedTorrent torrent, HttpClient httpClient) {
@ -67,6 +70,7 @@ public class Announcer implements AnnouncerFacade {
log.info("{} has announced successfully. Response: {} seeders, {} leechers, {}s interval",
this.torrent.getTorrentInfoHash().getHumanReadable(), responseMessage.getSeeders(), responseMessage.getLeechers(), responseMessage.getInterval());
this.reportedUploadBytes = announceDataAccessor.getUploaded(this.torrent.getTorrentInfoHash());
this.lastKnownInterval = responseMessage.getInterval();
this.lastKnownLeechers = responseMessage.getLeechers();
this.lastKnownSeeders = responseMessage.getSeeders();
@ -116,6 +120,14 @@ public class Announcer implements AnnouncerFacade {
return this.getTorrent().getTorrentInfoHash();
}
public boolean hasReachedUploadRatioLimit() {
if (uploadRatioTarget == -1f) {
return false;
}
final float bytesToUploadTarget = (uploadRatioTarget * (float) this.getTorrentSize());
return reportedUploadBytes >= bytesToUploadTarget;
}
/**
* Make sure to keep {@code torrentInfoHash} as the only input.
*/

View file

@ -2,6 +2,7 @@ package org.araymond.joal.core.ttorrent.client.announcer;
import lombok.RequiredArgsConstructor;
import org.apache.http.client.HttpClient;
import org.araymond.joal.core.config.AppConfiguration;
import org.araymond.joal.core.torrent.torrent.MockedTorrent;
import org.araymond.joal.core.ttorrent.client.announcer.request.AnnounceDataAccessor;
@ -9,8 +10,9 @@ import org.araymond.joal.core.ttorrent.client.announcer.request.AnnounceDataAcce
public class AnnouncerFactory {
private final AnnounceDataAccessor announceDataAccessor;
private final HttpClient httpClient;
private final AppConfiguration appConfiguration;
public Announcer create(final MockedTorrent torrent) {
return new Announcer(torrent, this.announceDataAccessor, httpClient);
return new Announcer(torrent, this.announceDataAccessor, httpClient, appConfiguration.getUploadRatioTarget());
}
}

View file

@ -24,4 +24,8 @@ public class AnnounceDataAccessor {
public Set<Map.Entry<String, String>> getHttpHeadersForTorrent() {
return this.bitTorrentClient.getHeaders();
}
public long getUploaded(final InfoHash infoHash) {
return this.bandwidthDispatcher.getSeedStatForTorrent(infoHash).getUploaded();
}
}

View file

@ -34,6 +34,10 @@ public class ClientNotifier implements AnnounceResponseHandler {
public void onAnnounceRegularSuccess(final Announcer announcer, final SuccessAnnounceResponse result) {
if (result.getSeeders() < 1 || result.getLeechers() < 1) {
this.client.onNoMorePeers(announcer.getTorrentInfoHash());
return;
}
if (announcer.hasReachedUploadRatioLimit()) {
this.client.onUploadRatioLimitReached(announcer.getTorrentInfoHash());
}
}

View file

@ -18,6 +18,7 @@ public class ConfigIncomingMessage {
private final Integer simultaneousSeed;
private final String client;
private final boolean keepTorrentWithZeroLeechers;
private final Float uploadRatioTarget;
@JsonCreator
ConfigIncomingMessage(
@ -25,16 +26,18 @@ public class ConfigIncomingMessage {
@JsonProperty(value = "maxUploadRate", required = true) final Long maxUploadRate,
@JsonProperty(value = "simultaneousSeed", required = true) final Integer simultaneousSeed,
@JsonProperty(value = "client", required = true) final String client,
@JsonProperty(value = "keepTorrentWithZeroLeechers", required = true) final boolean keepTorrentWithZeroLeechers
@JsonProperty(value = "keepTorrentWithZeroLeechers", required = true) final boolean keepTorrentWithZeroLeechers,
@JsonProperty(value = "uploadRatioTarget", defaultValue = "-1.0", required = false) final Float uploadRatioTarget
) {
this.minUploadRate = minUploadRate;
this.maxUploadRate = maxUploadRate;
this.simultaneousSeed = simultaneousSeed;
this.client = client;
this.keepTorrentWithZeroLeechers = keepTorrentWithZeroLeechers;
this.uploadRatioTarget = uploadRatioTarget;
}
public AppConfiguration toAppConfiguration() throws AppConfigurationIntegrityException {
return new AppConfiguration(this.minUploadRate, this.maxUploadRate, this.simultaneousSeed, this.client, keepTorrentWithZeroLeechers);
return new AppConfiguration(this.minUploadRate, this.maxUploadRate, this.simultaneousSeed, this.client, keepTorrentWithZeroLeechers, this.uploadRatioTarget);
}
}

View file

@ -54,8 +54,8 @@ public class AppConfigurationSerializationTest {
@Test
public void shouldSerialize() throws JsonProcessingException {
final AppConfiguration config = new AppConfiguration(180L, 190L, 2, "azureus.client", false);
assertThat(mapper.writeValueAsString(config)).isEqualTo("{\"minUploadRate\":180,\"maxUploadRate\":190,\"simultaneousSeed\":2,\"client\":\"azureus.client\",\"keepTorrentWithZeroLeechers\":false}");
final AppConfiguration config = new AppConfiguration(180L, 190L, 2, "azureus.client", false, 1f);
assertThat(mapper.writeValueAsString(config)).isEqualTo("{\"minUploadRate\":180,\"maxUploadRate\":190,\"simultaneousSeed\":2,\"client\":\"azureus.client\",\"keepTorrentWithZeroLeechers\":false,\"uploadRatioTarget\":1.0}");
}
@Test

View file

@ -11,40 +11,40 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy;
public class AppConfigurationTest {
public static AppConfiguration createOne() {
return new AppConfiguration(30L, 150L, 2, "azureus", true);
return new AppConfiguration(30L, 150L, 2, "azureus", true, 1f);
}
@Test
public void shouldNotBuildIfMinUploadRateIsLessThanZero() {
assertThatThrownBy(() -> new AppConfiguration(-1L, 190L, 2, "azureus.client", false))
assertThatThrownBy(() -> new AppConfiguration(-1L, 190L, 2, "azureus.client", false, 1f))
.isInstanceOf(AppConfigurationIntegrityException.class)
.hasMessageContaining("minUploadRate must be at least 0");
}
@Test
public void shouldBuildIfMinUploadRateEqualsZero() {
final AppConfiguration config = new AppConfiguration(0L, 190L, 2, "azureus.client", false);
final AppConfiguration config = new AppConfiguration(0L, 190L, 2, "azureus.client", false, 1f);
assertThat(config.getMinUploadRate()).isEqualTo(0);
}
@Test
public void shouldBuildIfMinUploadRateEqualsOne() {
final AppConfiguration config = new AppConfiguration(0L, 1L, 2, "azureus.client", false);
final AppConfiguration config = new AppConfiguration(0L, 1L, 2, "azureus.client", false, 1f);
assertThat(config.getMaxUploadRate()).isEqualTo(1);
}
@Test
public void shouldNotBuildIfMaxUploadRateIsLessThanZero() {
assertThatThrownBy(() -> new AppConfiguration(180L, -1L, 2, "azureus.client", false))
assertThatThrownBy(() -> new AppConfiguration(180L, -1L, 2, "azureus.client", false, 1f))
.isInstanceOf(AppConfigurationIntegrityException.class)
.hasMessageContaining("maxUploadRate must greater or equal to 0");
}
@Test
public void shouldBuildIfMinRateAndMaxRateEqualsZero() {
final AppConfiguration conf = new AppConfiguration(0L, 0L, 2, "azureus.client", false);
final AppConfiguration conf = new AppConfiguration(0L, 0L, 2, "azureus.client", false, 1f);
assertThat(conf.getMinUploadRate()).isEqualTo(0L);
assertThat(conf.getMaxUploadRate()).isEqualTo(0L);
@ -52,14 +52,14 @@ public class AppConfigurationTest {
@Test
public void shouldNotBuildIfMaxRateIsLesserThanMinRate() {
assertThatThrownBy(() -> new AppConfiguration(180L, 179L, 2, "azureus.client", false))
assertThatThrownBy(() -> new AppConfiguration(180L, 179L, 2, "azureus.client", false, 1f))
.isInstanceOf(AppConfigurationIntegrityException.class)
.hasMessageContaining("maxUploadRate must be greater or equal to minUploadRate");
}
@Test
public void shouldBuildIfMaxRateEqualsMinRate() {
final AppConfiguration conf = new AppConfiguration(180L, 180L, 2, "azureus.client", false);
final AppConfiguration conf = new AppConfiguration(180L, 180L, 2, "azureus.client", false, 1f);
assertThat(conf.getMinUploadRate()).isEqualTo(180L);
assertThat(conf.getMaxUploadRate()).isEqualTo(180L);
@ -67,35 +67,35 @@ public class AppConfigurationTest {
@Test
public void shouldNotBuildIfSimultaneousSeedIsLessThanOne() {
assertThatThrownBy(() -> new AppConfiguration(180L, 190L, 0, "azureus.client", false))
assertThatThrownBy(() -> new AppConfiguration(180L, 190L, 0, "azureus.client", false, 1f))
.isInstanceOf(AppConfigurationIntegrityException.class)
.hasMessageContaining("simultaneousSeed must be greater than 0");
}
@Test
public void shouldCreateIfSimultaneousSeedIsOne() {
final AppConfiguration config = new AppConfiguration(180L, 190L, 1, "azureus.client", false);
final AppConfiguration config = new AppConfiguration(180L, 190L, 1, "azureus.client", false, 1f);
assertThat(config.getSimultaneousSeed()).isEqualTo(1);
}
@Test
public void shouldNotBuildIfClientIsNull() {
assertThatThrownBy(() -> new AppConfiguration(180L, 190L, 2, null, false))
assertThatThrownBy(() -> new AppConfiguration(180L, 190L, 2, null, false, 1f))
.isInstanceOf(AppConfigurationIntegrityException.class)
.hasMessageContaining("client is required, no file name given");
}
@Test
public void shouldNotBuildIfClientIsEmpty() {
assertThatThrownBy(() -> new AppConfiguration(180L, 190L, 2, " ", false))
assertThatThrownBy(() -> new AppConfiguration(180L, 190L, 2, " ", false, 1f))
.isInstanceOf(AppConfigurationIntegrityException.class)
.hasMessageContaining("client is required, no file name given");
}
@Test
public void shouldBuild() {
final AppConfiguration config = new AppConfiguration(180L, 190L, 2, "azureus.client", false);
final AppConfiguration config = new AppConfiguration(180L, 190L, 2, "azureus.client", false, 1f);
assertThat(config.getMinUploadRate()).isEqualTo(180);
assertThat(config.getMaxUploadRate()).isEqualTo(190);
@ -105,15 +105,15 @@ public class AppConfigurationTest {
@Test
public void shouldBeEqualsByProperties() {
final AppConfiguration config = new AppConfiguration(180L, 190L, 2, "azureus.client", false);
final AppConfiguration config2 = new AppConfiguration(180L, 190L, 2, "azureus.client", false);
final AppConfiguration config = new AppConfiguration(180L, 190L, 2, "azureus.client", false, 1f);
final AppConfiguration config2 = new AppConfiguration(180L, 190L, 2, "azureus.client", false, 1f);
assertThat(config).isEqualTo(config2);
}
@Test
public void shouldHaveSameHashCodeWithSameProperties() {
final AppConfiguration config = new AppConfiguration(180L, 190L, 2, "azureus.client", false);
final AppConfiguration config2 = new AppConfiguration(180L, 190L, 2, "azureus.client", false);
final AppConfiguration config = new AppConfiguration(180L, 190L, 2, "azureus.client", false, 1f);
final AppConfiguration config2 = new AppConfiguration(180L, 190L, 2, "azureus.client", false, 1f);
assertThat(config.hashCode()).isEqualTo(config2.hashCode());
}

View file

@ -32,7 +32,8 @@ public class JoalConfigProviderTest {
190L,
5,
"azureus-5.7.5.0.client",
false
false,
1f
);
@Test
@ -108,7 +109,8 @@ public class JoalConfigProviderTest {
rand.longs(201, 400).findFirst().getAsLong(),
rand.ints(1, 5).findFirst().getAsInt(),
RandomStringUtils.random(60),
false
false,
1f
);
provider.saveNewConf(newConf);

View file

@ -1,6 +1,7 @@
package org.araymond.joal.core.ttorrent.client.announcer;
import org.apache.http.client.HttpClient;
import org.araymond.joal.core.config.AppConfiguration;
import org.araymond.joal.core.torrent.torrent.MockedTorrent;
import org.araymond.joal.core.ttorrent.client.announcer.request.AnnounceDataAccessor;
import org.araymond.joal.core.ttorrent.client.announcer.tracker.NoMoreUriAvailableException;
@ -20,7 +21,7 @@ public class AnnouncerFactoryTest {
@Test
public void shouldCreate() {
final AnnounceDataAccessor announceDataAccessor = mock(AnnounceDataAccessor.class);
final AnnouncerFactory announcerFactory = new AnnouncerFactory(announceDataAccessor, Mockito.mock(HttpClient.class));
final AnnouncerFactory announcerFactory = new AnnouncerFactory(announceDataAccessor, Mockito.mock(HttpClient.class), mock(AppConfiguration.class));
final MockedTorrent torrent = mock(MockedTorrent.class);
given(torrent.getAnnounceList()).willReturn(list(list(URI.create("http://localhost"))));
@ -32,7 +33,7 @@ public class AnnouncerFactoryTest {
@Test
public void createThrowsIfTorrentContainsNoValidURIs() {
final AnnounceDataAccessor announceDataAccessor = mock(AnnounceDataAccessor.class);
final AnnouncerFactory announcerFactory = new AnnouncerFactory(announceDataAccessor, Mockito.mock(HttpClient.class));
final AnnouncerFactory announcerFactory = new AnnouncerFactory(announceDataAccessor, Mockito.mock(HttpClient.class), mock(AppConfiguration.class));
assertThatThrownBy(() -> announcerFactory.create(mock(MockedTorrent.class)))
.isInstanceOf(NoMoreUriAvailableException.class);

View file

@ -28,7 +28,7 @@ public class AnnouncerTest {
@Test
public void shouldProvideRequiredInfoForAnnouncerFacade() {
final MockedTorrent torrent = MockedTorrentTest.createOneMock();
final AnnouncerFacade facade = new Announcer(torrent, null, Mockito.mock(HttpClient.class));
final AnnouncerFacade facade = new Announcer(torrent, null, Mockito.mock(HttpClient.class), 1f);
assertThat(facade.getConsecutiveFails()).isEqualTo(0);
assertThat(facade.getLastKnownInterval()).isEqualTo(5);
@ -47,7 +47,7 @@ public class AnnouncerTest {
doReturn("dd=ff&qq=d").when(dataAccessor).getHttpRequestQueryForTorrent(any(InfoHash.class), eq(RequestEvent.STARTED));
doReturn(new HashSet<>()).when(dataAccessor).getHttpHeadersForTorrent();
final Announcer announcer = new Announcer(torrent, dataAccessor, Mockito.mock(HttpClient.class));
final Announcer announcer = new Announcer(torrent, dataAccessor, Mockito.mock(HttpClient.class), 1f);
announcer.setTrackerClient(trackerClient);
//noinspection Duplicates
@ -80,7 +80,7 @@ public class AnnouncerTest {
doReturn("dd=ff&qq=d").when(dataAccessor).getHttpRequestQueryForTorrent(any(InfoHash.class), eq(RequestEvent.STARTED));
doReturn(new HashSet<>()).when(dataAccessor).getHttpHeadersForTorrent();
final Announcer announcer = new Announcer(torrent, dataAccessor, Mockito.mock(HttpClient.class));
final Announcer announcer = new Announcer(torrent, dataAccessor, Mockito.mock(HttpClient.class), 1f);
announcer.setTrackerClient(trackerClient);
//noinspection Duplicates
@ -116,7 +116,7 @@ public class AnnouncerTest {
doReturn("dd=ff&qq=d").when(dataAccessor).getHttpRequestQueryForTorrent(any(InfoHash.class), eq(RequestEvent.STARTED));
doReturn(new HashSet<>()).when(dataAccessor).getHttpHeadersForTorrent();
final Announcer announcer = new Announcer(torrent, dataAccessor, Mockito.mock(HttpClient.class));
final Announcer announcer = new Announcer(torrent, dataAccessor, Mockito.mock(HttpClient.class), 1f);
announcer.setTrackerClient(trackerClient);
assertThat(announcer.getLastAnnouncedAt()).isEmpty();
@ -155,7 +155,7 @@ public class AnnouncerTest {
doReturn("dd=ff&qq=d").when(dataAccessor).getHttpRequestQueryForTorrent(any(InfoHash.class), eq(RequestEvent.STARTED));
doReturn(new HashSet<>()).when(dataAccessor).getHttpHeadersForTorrent();
final Announcer announcer = new Announcer(torrent, dataAccessor, Mockito.mock(HttpClient.class));
final Announcer announcer = new Announcer(torrent, dataAccessor, Mockito.mock(HttpClient.class), 1f);
announcer.setTrackerClient(trackerClient);
assertThat(announcer.getLastKnownInterval()).isEqualTo(5);
@ -194,11 +194,11 @@ public class AnnouncerTest {
@Test
public void shouldBeEqualsByInfoHash() {
final MockedTorrent torrent1 = MockedTorrentTest.createOneMock("abcd");
final Announcer announcer1 = new Announcer(torrent1, null, Mockito.mock(HttpClient.class));
final Announcer announcer1 = new Announcer(torrent1, null, Mockito.mock(HttpClient.class), 1f);
final MockedTorrent torrent2 = MockedTorrentTest.createOneMock("abcd");
final Announcer announcer2 = new Announcer(torrent2, null, Mockito.mock(HttpClient.class));
final Announcer announcer2 = new Announcer(torrent2, null, Mockito.mock(HttpClient.class), 1f);
assertThat(announcer1).isEqualTo(announcer2);
}
@ -207,11 +207,11 @@ public class AnnouncerTest {
@Test
public void shouldNotBeEqualsWithDifferentInfoHash() {
final MockedTorrent torrent1 = MockedTorrentTest.createOneMock("abcd");
final Announcer announcer1 = new Announcer(torrent1, null, Mockito.mock(HttpClient.class));
final Announcer announcer1 = new Announcer(torrent1, null, Mockito.mock(HttpClient.class), 1f);
final MockedTorrent torrent2 = MockedTorrentTest.createOneMock("abcdefgh");
final Announcer announcer2 = new Announcer(torrent2, null, Mockito.mock(HttpClient.class));
final Announcer announcer2 = new Announcer(torrent2, null, Mockito.mock(HttpClient.class), 1f);
assertThat(announcer1).isNotEqualTo(announcer2);
}

View file

@ -3,5 +3,6 @@
"maxUploadRate": 190,
"simultaneousSeed": 5,
"client": "azureus-5.7.5.0.client",
"keepTorrentWithZeroLeechers": false
"keepTorrentWithZeroLeechers": false,
"uploadRatioTarget": 1.0
}