started 0.4.0

This commit is contained in:
InsanusMokrassar 2019-08-19 14:13:43 +06:00
parent 9b9bbefaf0
commit 555e1da355
9 changed files with 160 additions and 38 deletions

View File

@ -1,4 +1,8 @@
# 0.3.0 # SauceNaoAPI Changelog
## 0.4.0
## 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.41
kotlin_coroutines_version=1.2.1 kotlin_coroutines_version=1.2.2
kotlin_serialisation_runtime_version=0.11.0 kotlin_serialisation_runtime_version=0.11.1
joda_time_version=2.10.1 joda_time_version=2.10.1
ktor_version=1.1.4 ktor_version=1.2.3
project_public_name=SauceNao API project_public_name=SauceNao API
project_public_description=SauceNao API library project_public_description=SauceNao API library

View File

@ -0,0 +1,4 @@
package com.github.insanusmokrassar.SauceNaoAPI
const val LONG_TIME_LIMIT_MILLIS: Int = 1 * 24 * 60 * 60 * 1000
const val SHORT_TIME_LIMIT_MILLIS: Int = 30 * 1000

View File

@ -1,16 +1,22 @@
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 = args.first()
val api = SauceNaoAPI(key) val api = SauceNaoAPI(key)
runBlocking { val launch = GlobalScope.launch {
api.request( api.use {
args[1] it.request(
).also { args[1]
println(it) ).also {
println(it)
}
} }
} }
runBlocking {
launch.join()
}
} }

View File

@ -1,13 +1,19 @@
package com.github.insanusmokrassar.SauceNaoAPI package com.github.insanusmokrassar.SauceNaoAPI
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"
@ -25,7 +31,40 @@ data class SauceNaoAPI(
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
) { ) : Closeable {
private val logger = Logger.getLogger("SauceNaoAPI")
private val requestsChannel = Channel<Pair<Continuation<SauceNaoAnswer>, HttpRequestBuilder>>(Channel.UNLIMITED)
private val requestsSendTimes = mutableListOf<DateTime>()
init {
CoroutineScope(Dispatchers.Default).launch {
for ((callback, requestBuilder) in requestsChannel) {
try {
val answer = makeRequest(requestBuilder)
callback.resumeWith(Result.success(answer))
val sleepUntil = if (answer.header.longRemaining == 0) {
getMostOldestInLongPeriod() ?.plusMillis(LONG_TIME_LIMIT_MILLIS)
} else {
if (answer.header.shortRemaining == 0) {
getMostOldestInShortPeriod() ?.plusMillis(SHORT_TIME_LIMIT_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 +111,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_LIMIT_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_LIMIT_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 +170,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()
requestsSendTimes.clear()
client.close()
}
} }

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")