mirror of
https://github.com/InsanusMokrassar/SauceNaoAPI.git
synced 2024-12-23 05:07:12 +00:00
commit
f0f911e17b
16
CHANGELOG.md
16
CHANGELOG.md
@ -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
|
||||||
|
@ -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 {
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
@ -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
|
||||||
|
@ -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()
|
@ -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")
|
||||||
|
Loading…
Reference in New Issue
Block a user