diff --git a/lib/ratelimit/build.gradle b/lib/ratelimit/build.gradle new file mode 100644 index 000000000..814d8e947 --- /dev/null +++ b/lib/ratelimit/build.gradle @@ -0,0 +1,29 @@ +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' + +android { + compileSdkVersion 27 + buildToolsVersion '28.0.3' + + defaultConfig { + minSdkVersion 16 + targetSdkVersion 27 + versionCode 1 + versionName '1.0.0' + } + + buildTypes { + release { + minifyEnabled false + } + } +} + +repositories { + mavenCentral() +} + +dependencies { + compileOnly "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" + compileOnly 'com.squareup.okhttp3:okhttp:3.10.0' +} diff --git a/lib/ratelimit/src/main/AndroidManifest.xml b/lib/ratelimit/src/main/AndroidManifest.xml new file mode 100644 index 000000000..a08d6eaff --- /dev/null +++ b/lib/ratelimit/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + diff --git a/lib/ratelimit/src/main/java/eu/kanade/tachiyomi/lib/ratelimit/RateLimitInterceptor.kt b/lib/ratelimit/src/main/java/eu/kanade/tachiyomi/lib/ratelimit/RateLimitInterceptor.kt new file mode 100644 index 000000000..32bcbcb8d --- /dev/null +++ b/lib/ratelimit/src/main/java/eu/kanade/tachiyomi/lib/ratelimit/RateLimitInterceptor.kt @@ -0,0 +1,58 @@ +package eu.kanade.tachiyomi.lib.ratelimit + +import android.os.SystemClock +import okhttp3.Interceptor +import okhttp3.Response +import java.util.concurrent.TimeUnit + +/** + * An OkHttp interceptor that handles rate limiting. + * + * Examples: + * + * permits = 5, period = 1, unit = seconds => 5 requests per second + * permits = 10, period = 2, unit = minutes => 10 requests per 2 minutes + * + * @param permits {Int} Number of requests allowed within a period of units. + * @param period {Long} The limiting duration. Defaults to 1. + * @param unit {TimeUnit} The unit of time for the period. Defaults to seconds. + */ +class RateLimitInterceptor( + private val permits: Int, + private val period: Long = 1, + private val unit: TimeUnit = TimeUnit.SECONDS) : Interceptor { + + private val requestQueue = ArrayList(permits) + private val rateLimitMillis = unit.toMillis(period) + + override fun intercept(chain: Interceptor.Chain): Response { + synchronized(requestQueue) { + val now = SystemClock.elapsedRealtime() + val waitTime = if (requestQueue.size < permits) { + 0 + } else { + val oldestReq = requestQueue[0] + val newestReq = requestQueue[permits - 1] + + if (newestReq - oldestReq > rateLimitMillis) { + 0 + } else { + oldestReq + rateLimitMillis - now // Remaining time + } + } + + if (requestQueue.size == permits) { + requestQueue.removeAt(0) + } + if (waitTime > 0) { + requestQueue.add(now + waitTime) + Thread.sleep(waitTime) // Sleep inside synchronized to pause queued requests + } else { + requestQueue.add(now) + } + } + + return chain.proceed(chain.request()) + } + +} diff --git a/settings.gradle b/settings.gradle index 4619bfc30..6ef57c2a2 100644 --- a/settings.gradle +++ b/settings.gradle @@ -7,5 +7,7 @@ new File(rootDir, "src").eachDir { dir -> } include ':duktape-stub' include ':preference-stub' +include ':lib-ratelimit' project(':duktape-stub').projectDir = new File("lib/duktape-stub") -project(':preference-stub').projectDir = new File("lib/preference-stub") \ No newline at end of file +project(':preference-stub').projectDir = new File("lib/preference-stub") +project(':lib-ratelimit').projectDir = new File("lib/ratelimit") diff --git a/src/all/mangadex/build.gradle b/src/all/mangadex/build.gradle index 808c02713..b66737c16 100644 --- a/src/all/mangadex/build.gradle +++ b/src/all/mangadex/build.gradle @@ -5,11 +5,12 @@ ext { appName = 'Tachiyomi: MangaDex' pkgNameSuffix = 'all.mangadex' extClass = '.MangadexFactory' - extVersionCode = 56 + extVersionCode = 57 libVersion = '1.2' } dependencies { + implementation project(':lib-ratelimit') compileOnly project(':preference-stub') compileOnly 'com.google.code.gson:gson:2.8.2' compileOnly 'com.github.salomonbrys.kotson:kotson:2.5.0' diff --git a/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/Mangadex.kt b/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/Mangadex.kt index f3bd51892..37b954592 100644 --- a/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/Mangadex.kt +++ b/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/Mangadex.kt @@ -2,7 +2,6 @@ package eu.kanade.tachiyomi.extension.all.mangadex import android.app.Application import android.content.SharedPreferences -import android.os.SystemClock import android.support.v7.preference.ListPreference import android.support.v7.preference.PreferenceScreen import com.github.salomonbrys.kotson.forEach @@ -16,6 +15,7 @@ import com.github.salomonbrys.kotson.string import com.google.gson.JsonElement import com.google.gson.JsonObject import com.google.gson.JsonParser +import eu.kanade.tachiyomi.lib.ratelimit.RateLimitInterceptor import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.asObservableSuccess import eu.kanade.tachiyomi.source.ConfigurableSource @@ -28,7 +28,6 @@ import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.online.ParsedHttpSource import okhttp3.Headers import okhttp3.HttpUrl -import okhttp3.Interceptor import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.Response @@ -57,37 +56,7 @@ open class Mangadex(override val lang: String, private val internalLang: String, Injekt.get().getSharedPreferences("source_$id", 0x0000) } - private val requestsPerSecond = 4 - private val lastRequests = ArrayList(requestsPerSecond) - private val rateLimitInterceptor = Interceptor { - synchronized(this) { - val now = SystemClock.elapsedRealtime() - val waitTime = if (lastRequests.size < requestsPerSecond) { - 0 - } else { - val oldestReq = lastRequests[0] - val newestReq = lastRequests[requestsPerSecond - 1] - - if (newestReq - oldestReq > 1000) { - 0 - } else { - oldestReq + 1000 - now // Remaining time for the next second - } - } - - if (lastRequests.size == requestsPerSecond) { - lastRequests.removeAt(0) - } - if (waitTime > 0) { - lastRequests.add(now + waitTime) - Thread.sleep(waitTime) // Sleep inside synchronized to pause queued requests - } else { - lastRequests.add(now) - } - } - - it.proceed(it.request()) - } + private val rateLimitInterceptor = RateLimitInterceptor(4) override val client: OkHttpClient = network.cloudflareClient.newBuilder() .addNetworkInterceptor(rateLimitInterceptor)