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 * 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 * 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" project.group = "com.github.insanusmokrassar"
buildscript { buildscript {

View File

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

View File

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

View File

@ -1,13 +1,21 @@
package com.github.insanusmokrassar.SauceNaoAPI 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 com.github.insanusmokrassar.SauceNaoAPI.models.SauceNaoAnswer
import io.ktor.client.HttpClient import io.ktor.client.HttpClient
import io.ktor.client.call.call
import io.ktor.client.engine.okhttp.OkHttp import io.ktor.client.engine.okhttp.OkHttp
import io.ktor.client.request.parameter import io.ktor.client.features.ClientRequestException
import io.ktor.client.request.url import io.ktor.client.request.*
import io.ktor.client.response.readText 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 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 API_TOKEN_FIELD = "api_key"
private const val OUTPUT_TYPE_FIELD = "output_type" private const val OUTPUT_TYPE_FIELD = "output_type"
@ -24,8 +32,42 @@ data class SauceNaoAPI(
private val apiToken: String, private val apiToken: String,
private val outputType: OutputType = JsonOutputType, private val outputType: OutputType = JsonOutputType,
private val client: HttpClient = HttpClient(OkHttp), 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( suspend fun request(
url: String, url: String,
resultsCount: Int? = null, resultsCount: Int? = null,
@ -72,6 +114,57 @@ data class SauceNaoAPI(
minSimilarity = minSimilarity 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( private suspend fun makeRequest(
url: String, url: String,
db: Int? = null, db: Int? = null,
@ -80,21 +173,26 @@ data class SauceNaoAPI(
resultsCount: Int? = null, resultsCount: Int? = null,
minSimilarity: Float? = null minSimilarity: Float? = null
): SauceNaoAnswer? { ): SauceNaoAnswer? {
return client.call { return suspendCoroutine<SauceNaoAnswer> {
url(searchUrl) requestsChannel.offer(
parameter(URL_FIELD, url) it to HttpRequestBuilder().apply {
parameter(API_TOKEN_FIELD, apiToken) url(searchUrl)
parameter(OUTPUT_TYPE_FIELD, outputType.typeCode) parameter(URL_FIELD, url)
db ?.also { parameter(DB_FIELD, it) } parameter(API_TOKEN_FIELD, apiToken)
dbmask ?.also { parameter(DBMASK_FIELD, it) } parameter(OUTPUT_TYPE_FIELD, outputType.typeCode)
dbmaski ?.also { parameter(DBMASKI_FIELD, it) } db ?.also { parameter(DB_FIELD, it) }
resultsCount ?.also { parameter(RESULTS_COUNT_FIELD, it) } dbmask ?.also { parameter(DBMASK_FIELD, it) }
minSimilarity ?.also { parameter(MINIMAL_SIMILARITY_FIELD, it) } dbmaski ?.also { parameter(DBMASKI_FIELD, it) }
}.response.readText().let { resultsCount ?.also { parameter(RESULTS_COUNT_FIELD, it) }
Json.nonstrict.parse( minSimilarity ?.also { parameter(MINIMAL_SIMILARITY_FIELD, it) }
SauceNaoAnswer.serializer(), }
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 val Header.shortLimitStatus: LimitStatus
get() = LimitStatus( get() = LimitStatus(
shortRemaining ?: Int.MAX_VALUE, shortRemaining,
shortLimit ?: Int.MAX_VALUE shortLimit
) )
val Header.longLimitStatus: LimitStatus val Header.longLimitStatus: LimitStatus
get() = LimitStatus( get() = LimitStatus(
longRemaining ?: Int.MAX_VALUE, longRemaining,
longLimit ?: Int.MAX_VALUE longLimit
) )
val Header.limits 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") @SerialName("query_image")
val queryImage: String? = null, // something like "uuid.jpg" val queryImage: String? = null, // something like "uuid.jpg"
@SerialName("short_remaining") @SerialName("short_remaining")
val shortRemaining: Int? = null, val shortRemaining: Int = Int.MAX_VALUE,
@SerialName("long_remaining") @SerialName("long_remaining")
val longRemaining: Int? = null, val longRemaining: Int = Int.MAX_VALUE,
@SerialName("short_limit") @SerialName("short_limit")
val shortLimit: Int? = null, val shortLimit: Int = Int.MAX_VALUE,
@SerialName("long_limit") @SerialName("long_limit")
val longLimit: Int? = null, val longLimit: Int = Int.MAX_VALUE,
@SerialName("account_type") @SerialName("account_type")
val accountType: Int? = null, val accountType: Int? = null,
@SerialName("user_id") @SerialName("user_id")