mirror of
https://github.com/InsanusMokrassar/SauceNaoAPI.git
synced 2024-12-22 20:57:12 +00:00
commit
f9a3176fec
@ -12,6 +12,15 @@
|
||||
* Now `SauceNaoAPI` working with synchronous queue
|
||||
* `SauceNaoAPI` now will wait for some time when one of limits will be achieved
|
||||
|
||||
### 0.4.1 Managers experiments and row format in answer
|
||||
|
||||
* Add `TimeManager` - it will manage work with requests times
|
||||
* Add `RequestQuotaMagager` - it will manage quota for requests and call suspend
|
||||
if they will be over
|
||||
* `SauceNaoAPI` now working (almost) asynchronously
|
||||
* Now `SauceNaoAnswer` have field `row` which contains `JsonObject` with
|
||||
all original answer fields
|
||||
|
||||
## 0.3.0
|
||||
|
||||
* Now `results` field of `SauceNaoAnswer` is optional and is empty list by default
|
||||
|
26
README.md
26
README.md
@ -1 +1,25 @@
|
||||
# SauceNaoAPI
|
||||
# SauceNaoAPI
|
||||
|
||||
It is wrapper for [SauceNAO](https://saucenao.com/) API. For now, library is
|
||||
in preview state. It can be fully used, but some of info can be unavailable from
|
||||
wrapper classes, but now you can access them via `SauceNaoAnswer#row` field.
|
||||
|
||||
## Requester
|
||||
|
||||
For the requests we are using `SauceNaoAPI` object. Unfortunately, for now it
|
||||
supports only url strings as source of request. For example:
|
||||
|
||||
```kotlin
|
||||
val key = // here must be your Sauce NAO API key
|
||||
val requestUrl = // here must be your link to some image
|
||||
|
||||
val api = SauceNaoAPI(key)
|
||||
api.use {
|
||||
println(
|
||||
it.request(requestUrl)
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
Most of others requests use the same etymology and meaning as in the
|
||||
`SauceNAO` API docs.
|
||||
|
@ -1,4 +1,4 @@
|
||||
project.version = "0.4.0"
|
||||
project.version = "0.4.1"
|
||||
project.group = "com.github.insanusmokrassar"
|
||||
|
||||
buildscript {
|
||||
@ -15,7 +15,6 @@ buildscript {
|
||||
}
|
||||
}
|
||||
|
||||
apply plugin: 'java-library'
|
||||
apply plugin: 'kotlin'
|
||||
apply plugin: 'kotlinx-serialization'
|
||||
|
||||
|
@ -2,15 +2,13 @@ package com.github.insanusmokrassar.SauceNaoAPI
|
||||
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
fun main(vararg args: String) {
|
||||
suspend fun main(vararg args: String) {
|
||||
val (key, requestUrl) = args
|
||||
|
||||
runBlocking {
|
||||
val api = SauceNaoAPI(key, scope = GlobalScope)
|
||||
api.use {
|
||||
println(
|
||||
it.request(requestUrl)
|
||||
)
|
||||
}
|
||||
val api = SauceNaoAPI(key, scope = GlobalScope)
|
||||
api.use {
|
||||
println(
|
||||
it.request(requestUrl)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,8 @@ import com.github.insanusmokrassar.SauceNaoAPI.additional.LONG_TIME_RECALCULATIN
|
||||
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.utils.*
|
||||
import com.github.insanusmokrassar.SauceNaoAPI.utils.calculateSleepTime
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.engine.okhttp.OkHttp
|
||||
import io.ktor.client.features.ClientRequestException
|
||||
@ -38,31 +40,24 @@ data class SauceNaoAPI(
|
||||
private val logger = Logger.getLogger("SauceNaoAPI")
|
||||
|
||||
private val requestsChannel = Channel<Pair<Continuation<SauceNaoAnswer>, HttpRequestBuilder>>(Channel.UNLIMITED)
|
||||
private val requestsSendTimes = mutableListOf<DateTime>()
|
||||
private val timeManager = TimeManager(scope)
|
||||
private val quotaManager = RequestQuotaManager(scope)
|
||||
|
||||
init {
|
||||
scope.launch {
|
||||
for ((callback, requestBuilder) in requestsChannel) {
|
||||
private val requestsJob = scope.launch {
|
||||
for ((callback, requestBuilder) in requestsChannel) {
|
||||
quotaManager.getQuota()
|
||||
launch {
|
||||
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)
|
||||
}
|
||||
quotaManager.updateQuota(answer.header, timeManager)
|
||||
} catch (e: Exception) {
|
||||
callback.resumeWith(Result.failure(e))
|
||||
try {
|
||||
callback.resumeWith(Result.failure(e))
|
||||
} catch (e: IllegalStateException) { // may happen when already resumed and api was closed
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -121,7 +116,7 @@ data class SauceNaoAPI(
|
||||
val call = client.execute(builder)
|
||||
val answerText = call.response.readText()
|
||||
logger.info(answerText)
|
||||
addRequestTimesAndClear()
|
||||
timeManager.addTimeAndClear()
|
||||
Json.nonstrict.parse(
|
||||
SauceNaoAnswer.serializer(),
|
||||
answerText
|
||||
@ -131,40 +126,6 @@ data class SauceNaoAPI(
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
@ -193,6 +154,8 @@ data class SauceNaoAPI(
|
||||
override fun close() {
|
||||
requestsChannel.close()
|
||||
client.close()
|
||||
requestsSendTimes.clear()
|
||||
requestsJob.cancel()
|
||||
timeManager.close()
|
||||
quotaManager.close()
|
||||
}
|
||||
}
|
@ -1,9 +1,40 @@
|
||||
package com.github.insanusmokrassar.SauceNaoAPI.models
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.*
|
||||
import kotlinx.serialization.internal.ArrayListSerializer
|
||||
import kotlinx.serialization.json.*
|
||||
|
||||
@Serializable
|
||||
@Serializable(SauceNaoAnswerSerializer::class)
|
||||
data class SauceNaoAnswer(
|
||||
val header: Header,
|
||||
val results: List<Result> = emptyList()
|
||||
val results: List<Result> = emptyList(),
|
||||
val raw: JsonObject
|
||||
)
|
||||
|
||||
@Serializer(SauceNaoAnswer::class)
|
||||
object SauceNaoAnswerSerializer : KSerializer<SauceNaoAnswer> {
|
||||
private val resultsSerializer = ArrayListSerializer(Result.serializer())
|
||||
private const val headerField = "header"
|
||||
private const val resultsField = "results"
|
||||
private val serializer = Json.nonstrict
|
||||
|
||||
override fun deserialize(decoder: Decoder): SauceNaoAnswer {
|
||||
val raw = JsonObjectSerializer.deserialize(decoder)
|
||||
val header = serializer.fromJson(Header.serializer(), raw.getObject(headerField))
|
||||
val results = serializer.fromJson(resultsSerializer, raw.getArray(resultsField))
|
||||
|
||||
return SauceNaoAnswer(header, results, raw)
|
||||
}
|
||||
|
||||
override fun serialize(encoder: Encoder, obj: SauceNaoAnswer) {
|
||||
val resultObject = JsonObject(
|
||||
obj.raw.content.let {
|
||||
it + mapOf(
|
||||
headerField to serializer.toJson(Header.serializer(), obj.header),
|
||||
resultsField to serializer.toJson(resultsSerializer, obj.results)
|
||||
)
|
||||
}
|
||||
)
|
||||
JsonObject.serializer().serialize(encoder, resultObject)
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,74 @@
|
||||
package com.github.insanusmokrassar.SauceNaoAPI.utils
|
||||
|
||||
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.models.Header
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.io.core.Closeable
|
||||
import org.joda.time.DateTime
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
import kotlin.math.min
|
||||
|
||||
class RequestQuotaManager (
|
||||
private val scope: CoroutineScope
|
||||
) : Closeable {
|
||||
private var longQuota = 1
|
||||
private var shortQuota = 1
|
||||
private var longMaxQuota = 1
|
||||
private var shortMaxQuota = 1
|
||||
|
||||
private val quotaActions = Channel<suspend () -> Unit>(Channel.UNLIMITED)
|
||||
|
||||
private val quotaJob = scope.launch {
|
||||
for (callback in quotaActions) {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun updateQuota(header: Header, timeManager: TimeManager) {
|
||||
quotaActions.send(
|
||||
suspend {
|
||||
longMaxQuota = header.longLimit
|
||||
shortMaxQuota = header.shortLimit
|
||||
|
||||
longQuota = min(header.longLimit, header.longRemaining)
|
||||
shortQuota = min(header.shortLimit, header.shortRemaining)
|
||||
|
||||
when {
|
||||
shortQuota < 1 -> timeManager.getMostOldestInShortPeriod() ?.millis ?.plus(SHORT_TIME_RECALCULATING_MILLIS) ?: let {
|
||||
shortQuota = 1
|
||||
null
|
||||
}
|
||||
longQuota < 1 -> timeManager.getMostOldestInLongPeriod() ?.millis ?.plus(LONG_TIME_RECALCULATING_MILLIS) ?: let {
|
||||
longQuota = 1
|
||||
null
|
||||
}
|
||||
else -> null
|
||||
} ?.let {
|
||||
delay(it - DateTime.now().millis)
|
||||
}
|
||||
Unit
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun getQuota() {
|
||||
return suspendCoroutine {
|
||||
lateinit var callback: suspend () -> Unit
|
||||
callback = suspend {
|
||||
if (longQuota > 0 && shortQuota > 0) {
|
||||
it.resumeWith(Result.success(Unit))
|
||||
} else {
|
||||
quotaActions.send(callback)
|
||||
}
|
||||
}
|
||||
quotaActions.offer(callback)
|
||||
}
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
quotaJob.cancel()
|
||||
quotaActions.close()
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
package com.github.insanusmokrassar.SauceNaoAPI.utils
|
||||
|
||||
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.models.Header
|
||||
import org.joda.time.DateTime
|
||||
|
||||
internal suspend fun calculateSleepTime(
|
||||
header: Header,
|
||||
mostOldestInShortPeriodGetter: suspend () -> DateTime?,
|
||||
mostOldestInLongPeriodGetter: suspend () -> DateTime?
|
||||
): DateTime? {
|
||||
return when {
|
||||
header.longRemaining < 1 -> mostOldestInLongPeriodGetter() ?.plusMillis(LONG_TIME_RECALCULATING_MILLIS)
|
||||
header.shortRemaining < 1 -> mostOldestInShortPeriodGetter() ?.plusMillis(SHORT_TIME_RECALCULATING_MILLIS)
|
||||
else -> null
|
||||
}
|
||||
}
|
@ -0,0 +1,104 @@
|
||||
package com.github.insanusmokrassar.SauceNaoAPI.utils
|
||||
|
||||
import com.github.insanusmokrassar.SauceNaoAPI.additional.LONG_TIME_RECALCULATING_MILLIS
|
||||
import com.github.insanusmokrassar.SauceNaoAPI.additional.SHORT_TIME_RECALCULATING_MILLIS
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.io.core.Closeable
|
||||
import org.joda.time.DateTime
|
||||
import kotlin.coroutines.Continuation
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
private fun MutableList<DateTime>.clearTooOldTimes(relatedTo: DateTime = DateTime.now()) {
|
||||
val limitValue = relatedTo.minusMillis(LONG_TIME_RECALCULATING_MILLIS)
|
||||
|
||||
removeAll {
|
||||
it < limitValue
|
||||
}
|
||||
}
|
||||
|
||||
private interface TimeManagerAction {
|
||||
suspend fun makeChangeWith(times: MutableList<DateTime>)
|
||||
suspend operator fun invoke(times: MutableList<DateTime>) = makeChangeWith(times)
|
||||
}
|
||||
|
||||
private data class TimeManagerClean(private val relatedTo: DateTime = DateTime.now()) : TimeManagerAction {
|
||||
override suspend fun makeChangeWith(times: MutableList<DateTime>) {
|
||||
times.clearTooOldTimes(relatedTo)
|
||||
}
|
||||
}
|
||||
|
||||
private data class TimeManagerTimeAdder(
|
||||
private val time: DateTime = DateTime.now()
|
||||
) : TimeManagerAction {
|
||||
override suspend fun makeChangeWith(times: MutableList<DateTime>) {
|
||||
times.add(time)
|
||||
times.clearTooOldTimes()
|
||||
}
|
||||
}
|
||||
|
||||
private data class TimeManagerMostOldestInLongGetter(
|
||||
private val continuation: Continuation<DateTime?>
|
||||
) : TimeManagerAction {
|
||||
override suspend fun makeChangeWith(times: MutableList<DateTime>) {
|
||||
times.clearTooOldTimes()
|
||||
continuation.resumeWith(Result.success(times.min()))
|
||||
}
|
||||
}
|
||||
|
||||
private data class TimeManagerMostOldestInShortGetter(
|
||||
private val continuation: Continuation<DateTime?>
|
||||
) : TimeManagerAction {
|
||||
override suspend fun makeChangeWith(times: MutableList<DateTime>) {
|
||||
times.clearTooOldTimes()
|
||||
|
||||
val now = DateTime.now()
|
||||
|
||||
val limitTime = now.minusMillis(SHORT_TIME_RECALCULATING_MILLIS)
|
||||
|
||||
continuation.resumeWith(
|
||||
Result.success(
|
||||
times.asSequence().filter {
|
||||
limitTime < it
|
||||
}.min()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class TimeManager(
|
||||
scope: CoroutineScope
|
||||
) : Closeable {
|
||||
private val actionsChannel = Channel<TimeManagerAction>(Channel.UNLIMITED)
|
||||
|
||||
private val timeUpdateJob = scope.launch {
|
||||
val times = mutableListOf<DateTime>()
|
||||
for (action in actionsChannel) {
|
||||
action(times)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun addTimeAndClear() {
|
||||
actionsChannel.send(TimeManagerTimeAdder())
|
||||
}
|
||||
|
||||
suspend fun getMostOldestInLongPeriod(): DateTime? {
|
||||
return suspendCoroutine {
|
||||
actionsChannel.offer(
|
||||
TimeManagerMostOldestInLongGetter(it)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getMostOldestInShortPeriod(): DateTime? {
|
||||
return suspendCoroutine {
|
||||
actionsChannel.offer(TimeManagerMostOldestInShortGetter(it))
|
||||
}
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
actionsChannel.close()
|
||||
timeUpdateJob.cancel()
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user