Merge pull request #3 from InsanusMokrassar/0.4.0

0.4.0
This commit is contained in:
InsanusMokrassar 2019-10-11 00:23:57 +06:00 committed by GitHub
commit f0f911e17b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 166 additions and 41 deletions

View File

@ -1,4 +1,18 @@
# 0.3.0
# SauceNaoAPI Changelog
## 0.4.0
* Update libraries versions
* Kotlin `1.3.31` -> `1.3.50`
* Coroutines `1.2.1` -> `1.3.2`
* Serialization `0.11.0` -> `0.13.0`
* Joda Time `2.10.1` -> `2.10.4`
* Ktor `1.1.4` -> `1.2.5`
* Now `SauceNaoAPI` is `Closeable`
* Now `SauceNaoAPI` working with synchronous queue
* `SauceNaoAPI` now will wait for some time when one of limits will be achieved
## 0.3.0
* Now `results` field of `SauceNaoAnswer` is optional and is empty list by default
* Adapted structure almost completed and now can be used with raw results

View File

@ -1,4 +1,4 @@
project.version = "0.3.0"
project.version = "0.4.0"
project.group = "com.github.insanusmokrassar"
buildscript {

View File

@ -1,9 +1,9 @@
kotlin.code.style=official
kotlin_version=1.3.31
kotlin_coroutines_version=1.2.1
kotlin_serialisation_runtime_version=0.11.0
joda_time_version=2.10.1
ktor_version=1.1.4
kotlin_version=1.3.50
kotlin_coroutines_version=1.3.2
kotlin_serialisation_runtime_version=0.13.0
joda_time_version=2.10.4
ktor_version=1.2.5
project_public_name=SauceNao API
project_public_description=SauceNao API library

View File

@ -1,16 +1,16 @@
package com.github.insanusmokrassar.SauceNaoAPI
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.*
fun main(vararg args: String) {
val key = args.first()
val api = SauceNaoAPI(key)
val (key, requestUrl) = args
runBlocking {
api.request(
args[1]
).also {
println(it)
val api = SauceNaoAPI(key, scope = GlobalScope)
api.use {
println(
it.request(requestUrl)
)
}
}
}

View File

@ -1,13 +1,21 @@
package com.github.insanusmokrassar.SauceNaoAPI
import com.github.insanusmokrassar.SauceNaoAPI.additional.LONG_TIME_RECALCULATING_MILLIS
import com.github.insanusmokrassar.SauceNaoAPI.additional.SHORT_TIME_RECALCULATING_MILLIS
import com.github.insanusmokrassar.SauceNaoAPI.exceptions.sauceNaoAPIException
import com.github.insanusmokrassar.SauceNaoAPI.models.SauceNaoAnswer
import io.ktor.client.HttpClient
import io.ktor.client.call.call
import io.ktor.client.engine.okhttp.OkHttp
import io.ktor.client.request.parameter
import io.ktor.client.request.url
import io.ktor.client.features.ClientRequestException
import io.ktor.client.request.*
import io.ktor.client.response.readText
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import kotlinx.io.core.Closeable
import kotlinx.serialization.json.Json
import org.joda.time.DateTime
import java.util.logging.Logger
import kotlin.coroutines.*
private const val API_TOKEN_FIELD = "api_key"
private const val OUTPUT_TYPE_FIELD = "output_type"
@ -24,8 +32,42 @@ data class SauceNaoAPI(
private val apiToken: String,
private val outputType: OutputType = JsonOutputType,
private val client: HttpClient = HttpClient(OkHttp),
private val searchUrl: String = SEARCH_URL
) {
private val searchUrl: String = SEARCH_URL,
private val scope: CoroutineScope = CoroutineScope(Dispatchers.Default)
) : Closeable {
private val logger = Logger.getLogger("SauceNaoAPI")
private val requestsChannel = Channel<Pair<Continuation<SauceNaoAnswer>, HttpRequestBuilder>>(Channel.UNLIMITED)
private val requestsSendTimes = mutableListOf<DateTime>()
init {
scope.launch {
for ((callback, requestBuilder) in requestsChannel) {
try {
val answer = makeRequest(requestBuilder)
callback.resumeWith(Result.success(answer))
val sleepUntil = if (answer.header.longRemaining < 1) {
getMostOldestInLongPeriod() ?.plusMillis(LONG_TIME_RECALCULATING_MILLIS)
} else {
if (answer.header.shortRemaining < 1) {
getMostOldestInShortPeriod() ?.plusMillis(SHORT_TIME_RECALCULATING_MILLIS)
} else {
null
}
}
sleepUntil ?.also { _ ->
logger.warning("LONG LIMIT REACHED, SLEEP UNTIL $sleepUntil")
delay(sleepUntil.millis - DateTime.now().millis)
}
} catch (e: Exception) {
callback.resumeWith(Result.failure(e))
}
}
}
}
suspend fun request(
url: String,
resultsCount: Int? = null,
@ -72,6 +114,57 @@ data class SauceNaoAPI(
minSimilarity = minSimilarity
)
private suspend fun makeRequest(
builder: HttpRequestBuilder
): SauceNaoAnswer {
return try {
val call = client.execute(builder)
val answerText = call.response.readText()
logger.info(answerText)
addRequestTimesAndClear()
Json.nonstrict.parse(
SauceNaoAnswer.serializer(),
answerText
)
} catch (e: ClientRequestException) {
throw e.sauceNaoAPIException
}
}
private fun addRequestTimesAndClear() {
val newDateTime = DateTime.now()
clearRequestTimes(newDateTime)
requestsSendTimes.add(newDateTime)
}
private fun clearRequestTimes(relatedTo: DateTime = DateTime.now()) {
val limitValue = relatedTo.minusMillis(LONG_TIME_RECALCULATING_MILLIS)
requestsSendTimes.removeAll {
it < limitValue
}
}
private fun getMostOldestInLongPeriod(): DateTime? {
clearRequestTimes()
return requestsSendTimes.min()
}
private fun getMostOldestInShortPeriod(): DateTime? {
val now = DateTime.now()
val limitTime = now.minusMillis(SHORT_TIME_RECALCULATING_MILLIS)
clearRequestTimes(now)
return requestsSendTimes.asSequence().filter {
limitTime < it
}.min()
}
private suspend fun makeRequest(
url: String,
db: Int? = null,
@ -80,21 +173,26 @@ data class SauceNaoAPI(
resultsCount: Int? = null,
minSimilarity: Float? = null
): SauceNaoAnswer? {
return client.call {
url(searchUrl)
parameter(URL_FIELD, url)
parameter(API_TOKEN_FIELD, apiToken)
parameter(OUTPUT_TYPE_FIELD, outputType.typeCode)
db ?.also { parameter(DB_FIELD, it) }
dbmask ?.also { parameter(DBMASK_FIELD, it) }
dbmaski ?.also { parameter(DBMASKI_FIELD, it) }
resultsCount ?.also { parameter(RESULTS_COUNT_FIELD, it) }
minSimilarity ?.also { parameter(MINIMAL_SIMILARITY_FIELD, it) }
}.response.readText().let {
Json.nonstrict.parse(
SauceNaoAnswer.serializer(),
it
return suspendCoroutine<SauceNaoAnswer> {
requestsChannel.offer(
it to HttpRequestBuilder().apply {
url(searchUrl)
parameter(URL_FIELD, url)
parameter(API_TOKEN_FIELD, apiToken)
parameter(OUTPUT_TYPE_FIELD, outputType.typeCode)
db ?.also { parameter(DB_FIELD, it) }
dbmask ?.also { parameter(DBMASK_FIELD, it) }
dbmaski ?.also { parameter(DBMASKI_FIELD, it) }
resultsCount ?.also { parameter(RESULTS_COUNT_FIELD, it) }
minSimilarity ?.also { parameter(MINIMAL_SIMILARITY_FIELD, it) }
}
)
}
}
override fun close() {
requestsChannel.close()
client.close()
requestsSendTimes.clear()
}
}

View File

@ -5,14 +5,14 @@ import com.github.insanusmokrassar.SauceNaoAPI.models.Header
val Header.shortLimitStatus: LimitStatus
get() = LimitStatus(
shortRemaining ?: Int.MAX_VALUE,
shortLimit ?: Int.MAX_VALUE
shortRemaining,
shortLimit
)
val Header.longLimitStatus: LimitStatus
get() = LimitStatus(
longRemaining ?: Int.MAX_VALUE,
longLimit ?: Int.MAX_VALUE
longRemaining,
longLimit
)
val Header.limits

View File

@ -0,0 +1,13 @@
package com.github.insanusmokrassar.SauceNaoAPI.exceptions
import io.ktor.client.features.ClientRequestException
import io.ktor.http.HttpStatusCode.Companion.TooManyRequests
import kotlinx.io.IOException
val ClientRequestException.sauceNaoAPIException: Exception
get() = when (response.status) {
TooManyRequests -> TooManyRequestsException()
else -> this
}
class TooManyRequestsException : IOException()

View File

@ -25,13 +25,13 @@ data class Header(
@SerialName("query_image")
val queryImage: String? = null, // something like "uuid.jpg"
@SerialName("short_remaining")
val shortRemaining: Int? = null,
val shortRemaining: Int = Int.MAX_VALUE,
@SerialName("long_remaining")
val longRemaining: Int? = null,
val longRemaining: Int = Int.MAX_VALUE,
@SerialName("short_limit")
val shortLimit: Int? = null,
val shortLimit: Int = Int.MAX_VALUE,
@SerialName("long_limit")
val longLimit: Int? = null,
val longLimit: Int = Int.MAX_VALUE,
@SerialName("account_type")
val accountType: Int? = null,
@SerialName("user_id")