Skip to content

Commit

Permalink
Merge pull request #221 from anthonyraymond/upload-ratio
Browse files Browse the repository at this point in the history
Stop seeding when target ratio is reached
  • Loading branch information
anthonyraymond authored Nov 1, 2023
2 parents 67c03d2 + a61797f commit d4d1dce
Show file tree
Hide file tree
Showing 16 changed files with 88 additions and 41 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,14 +101,16 @@ 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**)
- `maxUploadRate` : The maximum uploadRate you want to fake (in kB/s) (**required**)
- `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.



Expand Down
3 changes: 2 additions & 1 deletion resources/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@
"maxUploadRate": 170,
"simultaneousSeed": 5,
"client": "utorrent-3.5.0_43916.client",
"keepTorrentWithZeroLeechers": true
"keepTorrentWithZeroLeechers": true,
"uploadRatioTarget": -1.0
}
2 changes: 1 addition & 1 deletion src/main/java/org/araymond/joal/core/SeedManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ public void startSeeding() throws IOException {
.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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,20 +20,23 @@ public class AppConfiguration {
private final int simultaneousSeed;
private final String client;
private final boolean keepTorrentWithZeroLeechers;
private final float uploadRatioTarget;

@JsonCreator
public AppConfiguration(
@JsonProperty(value = "minUploadRate", required = true) final long minUploadRate,
@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();
}
Expand All @@ -56,5 +59,9 @@ private void validate() {
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)");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
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;
Expand Down Expand Up @@ -36,6 +37,7 @@
* <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;
Expand Down Expand Up @@ -181,6 +183,11 @@ public void onNoMorePeers(final InfoHash infoHash) {
}
}

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);
Expand Down Expand Up @@ -242,4 +249,5 @@ public List<AnnouncerFacade> getCurrentlySeedingAnnouncers() {
lock.unlock();
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -67,6 +70,7 @@ public SuccessAnnounceResponse announce(final RequestEvent event) throws Announc
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();
Expand Down Expand Up @@ -116,6 +120,14 @@ public InfoHash getTorrentInfoHash() {
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.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,17 @@

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;

@RequiredArgsConstructor
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());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,8 @@ public String getHttpRequestQueryForTorrent(final InfoHash infoHash, final Reque
public Set<Map.Entry<String, String>> getHttpHeadersForTorrent() {
return this.bitTorrentClient.getHeaders();
}

public long getUploaded(final InfoHash infoHash) {
return this.bandwidthDispatcher.getSeedStatForTorrent(infoHash).getUploaded();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ public void onAnnounceStartFails(final Announcer announcer, final Throwable thro
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());
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,23 +18,26 @@ public class ConfigIncomingMessage {
private final Integer simultaneousSeed;
private final String client;
private final boolean keepTorrentWithZeroLeechers;
private final Float uploadRatioTarget;

@JsonCreator
ConfigIncomingMessage(
@JsonProperty(value = "minUploadRate", required = true) final Long minUploadRate,
@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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@ public void shouldFailToDeserializeIfKeepTorrentWithZeroLeechersIsNotDefined() t

@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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,91 +11,91 @@
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);
}

@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);
}

@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);
Expand All @@ -105,15 +105,15 @@ public void shouldBuild() {

@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());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ public class JoalConfigProviderTest {
190L,
5,
"azureus-5.7.5.0.client",
false
false,
1f
);

@Test
Expand Down Expand Up @@ -108,7 +109,8 @@ public void shouldWriteConfigurationFile() throws IOException {
rand.longs(201, 400).findFirst().getAsLong(),
rand.ints(1, 5).findFirst().getAsInt(),
RandomStringUtils.random(60),
false
false,
1f
);

provider.saveNewConf(newConf);
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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"))));
Expand All @@ -32,7 +33,7 @@ public void shouldCreate() {
@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);
Expand Down
Loading

0 comments on commit d4d1dce

Please sign in to comment.