Compare commits

..

No commits in common. "master" and "0.6.1" have entirely different histories.

44 changed files with 573 additions and 1003 deletions

View File

@ -1,29 +0,0 @@
name: Publish package to GitHub Packages
on: [push]
jobs:
publishing:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-java@v1
with:
java-version: 11
- name: Rewrite version
run: |
branch="`echo "${{ github.ref }}" | grep -o "[^/]*$"`"
cat gradle.properties | sed -e "s/^library_version=\([0-9\.]*\)/library_version=\1-branch_$branch-build${{ github.run_number }}/" > gradle.properties.tmp
rm gradle.properties
mv gradle.properties.tmp gradle.properties
- name: Build
run: ./gradlew build
- name: Publish to Gitea
continue-on-error: true
run: ./gradlew publishAllPublicationsToGiteaRepository
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
- name: Publish
run: ./gradlew publishAllPublicationsToGithubPackagesRepository --no-parallel
continue-on-error: true
env:
GITHUBPACKAGES_USER: ${{ github.actor }}
GITHUBPACKAGES_PASSWORD: ${{ secrets.GITHUB_TOKEN }}

1
.gitignore vendored
View File

@ -8,4 +8,3 @@ settings.xml
.gradle/ .gradle/
build/ build/
out/ out/
kotlin-js-store/

View File

@ -1,182 +1,5 @@
# SauceNaoAPI Changelog # SauceNaoAPI Changelog
## 0.17.2
* Versions:
* `Coroutines`: `1.7.3`
* `Ktor`: `2.3.4`
* `MicroUtils`: `0.19.9`
## 0.17.1
* Versions:
* `Coroutines`: `1.7.2`
* `Ktor`: `2.3.2`
* `MicroUtils`: `0.19.7`
## 0.17.0
* Versions:
* `Kotlin`: `1.8.22`
* `Serialization`: `1.5.1`
* `Ktor`: `2.3.1`
* `MicroUtils`: `0.19.2`
* `Klock`: `4.0.3`
## 0.16.0
Add `MicroUtils` as used micro utils
* Versions:
* `Kotlin`: `1.8.21`
* `Ktor`: `2.3.0`
## 0.15.1
* Versions:
* `Ktor`: `2.2.4`
## 0.15.0
* Versions:
* `Kotlin`: `1.8.10`
* `Serialization`: `1.5.0`
* `Ktor`: `2.2.3`
## 0.14.0
* `LimitStatus` is `Comparable<LimitStatus>` since this update
* `Limits` is `Comparable<Limits>` since this update
* Main API has been changed
## 0.13.0
* Versions:
* `Kotlin`: `1.7.20` -> `1.7.22`
* `Serialization`: `1.4.0` -> `1.4.1`
* `Klock`: `3.2.0` -> `3.4.0`
* `Ktor`: `2.1.2` -> `2.2.1`
* Now it is possible to subscribe onto API limits changes
## 0.12.2
* Versions:
* `Kotlin`: `1.7.10` -> `1.7.20`
* `Serialization`: `1.4.0-RC` -> `1.4.0`
* `Klock`: `3.0.0` -> `3.2.0`
* `Ktor`: `2.1.0` -> `2.1.2`
## 0.12.1
* Versions:
* `Ktor`: `2.0.3` -> `2.1.0`
## 0.11.1
* Versions updates:
* `Ktor`: `2.0.1` -> `2.0.3`
* `Coroutines`: `1.6.1` -> `1.6.4`
## 0.11.0
* Versions updates:
* `Kotlin`: `1.6.10` -> `1.6.21`
* `Serialization`: `1.3.2` -> `1.3.3`
* `Klock`: `2.6.3` -> `2.7.0`
* `Ktor`: `1.6.8` -> `2.0.1`
## 0.10.1
* Versions updates:
* `Klock`: `2.6.2` -> `2.6.3`
* `Ktor`: `1.6.7` -> `1.6.8`
## 0.10.0
Migration onto libs versions toml
## 0.9.1
* Versions updates:
* `Kotlin`: `1.5.30` -> `1.5.31`
* `Klock`: `2.4.0` -> `2.4.2`
* `Coroutines`: `1.5.1` -> `1.5.2`
* Add several extensions to `ResultData`: `authors`, `froms`, `charactersList`, `titles`, `urls`
## 0.9.0
* Versions updates:
* `Kotlin`: `1.5.10` -> `1.5.30`
* `Klock`: `2.1.2` -> `2.4.0`
* `Ktor`: `1.5.4` -> `1.6.3`
* `Serialization`: `1.2.1` -> `1.2.2`
* `Coroutines`: `1.5.0` -> `1.5.1`
## 0.8.2
* Versions updates:
* `Kotlin`: `1.4.32` -> `1.5.10`
* `Klock`: `2.0.7` -> `2.1.2`
* `Ktor`: `1.5.3` -> `1.5.4`
* `Serialization`: `1.1.0` -> `1.2.1`
* `Coroutines`: `1.4.3` -> `1.5.0`
## 0.8.1
* Versions updates:
* `Kotlin`: `1.4.31` -> `1.4.32`
* `Klock`: `2.0.6` -> `2.0.7`
* `Ktor`: `1.5.2` -> `1.5.3`
## 0.8.0
* Versions updates:
* `Kotlin`: `1.4.21` -> `1.4.31`
* `Klock`: `2.0.4` -> `2.0.6`
* `Ktor`: `1.5.1` -> `1.5.2`
* `Kotlin Serialisation`: `1.0.1` -> `1.1.0`
* `Kotlin Coroutines`: `1.4.2` -> `1.4.3`
## 0.7.2
* Versions updates:
* `Klock`: `2.0.2` -> `2.0.4`
* `Ktor`: `1.5.0` -> `1.5.1`
## 0.7.1
* Versions updates:
* `Kotlin`: `1.4.20` -> `1.4.21`
* `Klock`: `2.0.0` -> `2.0.2`
* `Ktor`: `1.4.3` -> `1.5.0`
## 0.7.0
**BREAKING CHANGES: PACKAGE HAS BEEN CHANGED FROM `com.insanusmokrassar` to `dev.inmo`**
Migration:
* Packages in the whole project were changed `com.insanusmokrassar.SauceNaoAPI` -> `dev.inmo.saucenaoapi`
* Change implementation in your gradle files: `implementation "com.insanusmokrassar:SauceNaoAPI:*"` ->
`implementation "dev.inmo:saucenaoapi:*"`
## 0.6.2
* Versions updates:
* `Kotlin`: `1.4.10` -> `1.4.20`
* `Kotlin Serialisation`: `1.0.0-RC2` -> `1.0.1`
* `Kotlin Coroutines`: `1.3.9` -> `1.4.2`
* `Klock`: `1.12.1` -> `2.0.0`
* `Ktor`: `1.4.1` -> `1.4.3`
## 0.6.1
* Versions updates:
* `Kotlin`: `1.4.0` -> `1.4.10`
* `Kotlin Serialisation`: `1.0.0-RC` -> `1.0.0-RC2`
* `Klock`: `1.12.0` -> `1.12.1`
* `Ktor`: `1.4.0` -> `1.4.1`
## 0.6.0 ## 0.6.0
**MAIN PACKAGE WAS CHANGED: `com.github.insanusmokrassar` -> `com.insanusmokrassar`** **MAIN PACKAGE WAS CHANGED: `com.github.insanusmokrassar` -> `com.insanusmokrassar`**
@ -189,11 +12,31 @@ Migration:
* `Klock`: `1.11.14` -> `1.12.0` * `Klock`: `1.11.14` -> `1.12.0`
* `Ktor`: `1.3.2` -> `1.4.0` * `Ktor`: `1.3.2` -> `1.4.0`
### 0.6.1
* Versions updates:
* `Kotlin`: `1.4.0` -> `1.4.10`
* `Kotlin Serialisation`: `1.0.0-RC` -> `1.0.0-RC2`
* `Klock`: `1.12.0` -> `1.12.1`
* `Ktor`: `1.4.0` -> `1.4.1`
## 0.5.0 ## 0.5.0
* Versions updates * Versions updates
## 0.4.4 ## 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.4.4
* Uploading of file * Uploading of file
* Updates of versions * Updates of versions
@ -201,15 +44,15 @@ Migration:
* `SauceNaoAPI` instances now can return `limitsState` object, which will contains `LimitsState` with currently known * `SauceNaoAPI` instances now can return `limitsState` object, which will contains `LimitsState` with currently known
state of limits state of limits
## 0.4.3 ### 0.4.3
Hotfix for serializer of `SauceNaoAnswer` Hotfix for serializer of `SauceNaoAnswer`
## 0.4.2 ### 0.4.2
Hotfix for autostop for some time when there is no remaining quotas for requests Hotfix for autostop for some time when there is no remaining quotas for requests
## 0.4.1 Managers experiments and row format in answer ### 0.4.1 Managers experiments and row format in answer
* Add `TimeManager` - it will manage work with requests times * Add `TimeManager` - it will manage work with requests times
* Add `RequestQuotaMagager` - it will manage quota for requests and call suspend * Add `RequestQuotaMagager` - it will manage quota for requests and call suspend
@ -218,18 +61,6 @@ if they will be over
* Now `SauceNaoAnswer` have field `row` which contains `JsonObject` with * Now `SauceNaoAnswer` have field `row` which contains `JsonObject` with
all original answer fields all original answer fields
## 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 ## 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

View File

@ -1,7 +1,6 @@
# SauceNaoAPI # SauceNaoAPI
[![Maven Central](https://maven-badges.herokuapp.com/maven-central/dev.inmo/saucenaoapi/badge.svg)](https://maven-badges.herokuapp.com/maven-central/dev.inmo/saucenaoapi) [![Download](https://api.bintray.com/packages/insanusmokrassar/InsanusMokrassar/SauceNaoAPI-mpp/images/download.svg)](https://bintray.com/insanusmokrassar/InsanusMokrassar/SauceNaoAPI-mpp/_latestVersion)
It is wrapper for [SauceNAO](https://saucenao.com/) API. For now, library is 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 in preview state. It can be fully used, but some of info can be unavailable from
@ -12,7 +11,7 @@ wrapper classes, but now you can access them via `SauceNaoAnswer#row` field.
### Gradle ### Gradle
```groovy ```groovy
implementation "dev.inmo:saucenaoapi:$saucenaoapi_version" implementation "com.insanusmokrassar:SauceNaoAPI:$saucenaoapi_version"
``` ```
## Requester ## Requester

View File

@ -1,46 +1,42 @@
buildscript { buildscript {
repositories { repositories {
mavenLocal() mavenLocal()
jcenter()
mavenCentral() mavenCentral()
} }
dependencies { dependencies {
classpath libs.buildscript.kt.gradle classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath libs.buildscript.kt.serialization classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
classpath libs.buildscript.gh.release classpath "com.jfrog.bintray.gradle:gradle-bintray-plugin:$gradle_bintray_plugin_version"
classpath "com.github.breadmoirai:github-release:$gradle_github_release_plugin_version"
} }
} }
plugins { plugins {
alias(libs.plugins.multiplatform) id "org.jetbrains.kotlin.multiplatform" version "$kotlin_version"
alias(libs.plugins.serialization) id "org.jetbrains.kotlin.plugin.serialization" version "$kotlin_version"
} }
project.version = "$library_version" project.version = "0.6.1"
project.group = "dev.inmo" project.group = "com.insanusmokrassar"
apply from: "publish.gradle" apply from: "publish.gradle"
apply from: "github_release.gradle" apply from: "github_release.gradle"
repositories { repositories {
mavenLocal() mavenLocal()
jcenter()
mavenCentral() mavenCentral()
maven { url "https://kotlin.bintray.com/kotlinx" }
} }
kotlin { kotlin {
jvm { jvm()
compilations.main { js(BOTH) {
kotlinOptions {
jvmTarget = "1.8"
}
}
}
js(IR) {
browser() browser()
nodejs() nodejs()
} }
linuxX64()
mingwX64()
sourceSets { sourceSets {
@ -48,13 +44,10 @@ kotlin {
dependencies { dependencies {
implementation kotlin('stdlib') implementation kotlin('stdlib')
api libs.kt.coroutines api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version"
api libs.kt.serialization api "org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlin_serialisation_runtime_version"
api libs.klock api "com.soywiz.korlibs.klock:klock:$klock_version"
api libs.ktor.client api "io.ktor:ktor-client-core:$ktor_version"
api libs.microutils.common
api libs.microutils.ktor.common
api libs.microutils.mimetypes
} }
} }
commonTest { commonTest {
@ -63,21 +56,21 @@ kotlin {
implementation kotlin('test-annotations-common') implementation kotlin('test-annotations-common')
} }
} }
jvmMain {
dependencies {
implementation kotlin('stdlib')
}
}
jvmTest { jvmTest {
dependencies { dependencies {
implementation kotlin('test-junit') implementation kotlin('test-junit')
implementation libs.ktor.client.okhttp implementation "io.ktor:ktor-client-okhttp:$ktor_version"
} }
} }
jsTest { jsMain {
dependencies { dependencies {
implementation kotlin('test-js') implementation kotlin('test-js')
} }
} }
} }
} }
java {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}

View File

@ -2,8 +2,7 @@ private String getCurrentVersionChangelog() {
OutputStream changelogDataOS = new ByteArrayOutputStream() OutputStream changelogDataOS = new ByteArrayOutputStream()
exec { exec {
standardOutput = changelogDataOS standardOutput = changelogDataOS
commandLine 'chmod', "+x", './changelog_parser.sh' commandLine './changelog_info_retriever', "$library_version", 'CHANGELOG.md'
commandLine './changelog_parser.sh', "$library_version", 'CHANGELOG.md'
} }
return changelogDataOS.toString().trim() return changelogDataOS.toString().trim()
@ -19,9 +18,9 @@ if (new File(projectDir, "secret.gradle").exists()) {
owner "InsanusMokrassar" owner "InsanusMokrassar"
repo "${rootProject.name}" repo "${rootProject.name}"
tagName "v${project.version}" tagName "$library_version"
releaseName "${project.version}" releaseName "$library_version"
targetCommitish "${project.version}" targetCommitish "$library_version"
body getCurrentVersionChangelog() body getCurrentVersionChangelog()
} }

View File

@ -1,3 +1,9 @@
kotlin.code.style=official kotlin.code.style=official
kotlin_version=1.4.10
kotlin_coroutines_version=1.3.9
kotlin_serialisation_runtime_version=1.0.0-RC2
klock_version=1.12.1
ktor_version=1.4.1
library_version=0.17.2 gradle_github_release_plugin_version=2.2.12
gradle_bintray_plugin_version=1.8.5

View File

@ -1,36 +0,0 @@
[versions]
kt = "1.8.22"
kt-serialization = "1.5.1"
kt-coroutines = "1.7.3"
klock = "4.0.3"
ktor = "2.3.4"
microutils = "0.19.9"
gh-release = "2.4.1"
[libraries]
kt-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kt" }
kt-serialization = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kt-serialization" }
kt-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kt-coroutines" }
klock = { module = "com.soywiz.korlibs.klock:klock", version.ref = "klock" }
ktor-client = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" }
microutils-common = { module = "dev.inmo:micro_utils.common", version.ref = "microutils" }
microutils-ktor-common = { module = "dev.inmo:micro_utils.ktor.common", version.ref = "microutils" }
microutils-mimetypes = { module = "dev.inmo:micro_utils.mime_types", version.ref = "microutils" }
buildscript-kt-gradle = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kt" }
buildscript-kt-serialization = { module = "org.jetbrains.kotlin:kotlin-serialization", version.ref = "kt" }
buildscript-gh-release = { module = "com.github.breadmoirai:github-release", version.ref = "gh-release" }
[plugins]
multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kt" }
serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kt" }

View File

@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-6.6.1-bin.zip

53
maven.publish.gradle Normal file
View File

@ -0,0 +1,53 @@
apply plugin: 'maven-publish'
task javadocsJar(type: Jar) {
classifier = 'javadoc'
}
afterEvaluate {
project.publishing.publications.all {
// rename artifacts
groupId "${project.group}"
if (it.name.contains('kotlinMultiplatform')) {
artifactId = "${project.name}"
} else {
artifactId = "${project.name}-$name"
}
}
}
publishing {
publications.all {
artifact javadocsJar
pom {
description = "SauceNao API library"
name = "SauceNao API"
url = "https://insanusmokrassar.github.io/${project.name}"
scm {
developerConnection = "scm:git:[fetch=]https://github.com/insanusmokrassar/${project.name}.git[push=]https://github.com/insanusmokrassar/${project.name}.git"
url = "https://github.com/insanusmokrassar/${project.name}.git"
}
developers {
developer {
id = "InsanusMokrassar"
name = "Ovsyannikov Alexey"
email = "ovsyannikov.alexey95@gmail.com"
}
}
licenses {
license {
name = "Apache Software License 2.0"
url = "https://github.com/InsanusMokrassar/TelegramBotAPI/blob/master/LICENSE"
}
}
}
}
}

1
publication.kpsb Normal file
View File

@ -0,0 +1 @@
{"bintrayConfig":{"repo":"InsanusMokrassar","packageName":"${project.name}-mpp","packageVcs":"https://github.com/InsanusMokrassar/${project.name}"},"licenses":[{"id":"Apache-2.0","title":"Apache Software License 2.0","url":"https://github.com/InsanusMokrassar/TelegramBotAPI/blob/master/LICENSE"}],"mavenConfig":{"name":"SauceNao API","description":"SauceNao API library","url":"https://insanusmokrassar.github.io/${project.name}","vcsUrl":"https://github.com/insanusmokrassar/${project.name}.git","developers":[{"id":"InsanusMokrassar","name":"Ovsyannikov Alexey","eMail":"ovsyannikov.alexey95@gmail.com"}]},"type":"Multiplatform"}

View File

@ -1,99 +1,55 @@
apply plugin: 'maven-publish' apply plugin: 'com.jfrog.bintray'
task javadocsJar(type: Jar) { apply from: "maven.publish.gradle"
classifier = 'javadoc'
}
publishing { bintray {
publications.all { user = project.hasProperty('BINTRAY_USER') ? project.property('BINTRAY_USER') : System.getenv('BINTRAY_USER')
artifact javadocsJar key = project.hasProperty('BINTRAY_KEY') ? project.property('BINTRAY_KEY') : System.getenv('BINTRAY_KEY')
filesSpec {
pom { from "${buildDir}/publications/"
description = "SauceNao API library" eachFile {
name = "SauceNao API" String directorySubname = it.getFile().parentFile.name
url = "https://insanusmokrassar.github.io/${project.name}" if (it.getName() == "module.json") {
if (directorySubname == "kotlinMultiplatform") {
scm { it.setPath("${project.name}/${project.version}/${project.name}-${project.version}.module")
developerConnection = "scm:git:[fetch=]https://github.com/insanusmokrassar/${project.name}.git[push=]https://github.com/insanusmokrassar/${project.name}.git" } else {
url = "https://github.com/insanusmokrassar/${project.name}.git" it.setPath("${project.name}-${directorySubname}/${project.version}/${project.name}-${directorySubname}-${project.version}.module")
} }
} else {
developers { if (directorySubname == "kotlinMultiplatform" && it.getName() == "pom-default.xml") {
it.setPath("${project.name}/${project.version}/${project.name}-${project.version}.pom")
developer { } else {
id = "InsanusMokrassar" it.exclude()
name = "Ovsyannikov Alexey" }
email = "ovsyannikov.alexey95@gmail.com"
}
}
licenses {
license {
name = "Apache Software License 2.0"
url = "https://github.com/InsanusMokrassar/SauceNaoAPI/blob/master/LICENSE"
}
} }
} }
repositories { into "${project.group}".replace(".", "/")
if ((project.hasProperty('GITHUBPACKAGES_USER') || System.getenv('GITHUBPACKAGES_USER') != null) && (project.hasProperty('GITHUBPACKAGES_PASSWORD') || System.getenv('GITHUBPACKAGES_PASSWORD') != null)) { }
maven { pkg {
name = "GithubPackages" repo = "InsanusMokrassar"
url = uri("https://maven.pkg.github.com/InsanusMokrassar/SauceNaoAPI") name = "${project.name}-mpp"
vcsUrl = "https://github.com/InsanusMokrassar/${project.name}"
credentials { licenses = ["Apache-2.0"]
username = project.hasProperty('GITHUBPACKAGES_USER') ? project.property('GITHUBPACKAGES_USER') : System.getenv('GITHUBPACKAGES_USER') version {
password = project.hasProperty('GITHUBPACKAGES_PASSWORD') ? project.property('GITHUBPACKAGES_PASSWORD') : System.getenv('GITHUBPACKAGES_PASSWORD') name = "${project.version}"
} released = new Date()
vcsTag = "${project.version}"
} gpg {
} sign = true
if (project.hasProperty('GITEA_TOKEN') || System.getenv('GITEA_TOKEN') != null) { passphrase = project.hasProperty('signing.gnupg.passphrase') ? project.property('signing.gnupg.passphrase') : System.getenv('signing.gnupg.passphrase')
maven {
name = "Gitea"
url = uri("https://git.inmo.dev/api/packages/InsanusMokrassar/maven")
credentials(HttpHeaderCredentials) {
name = "Authorization"
value = project.hasProperty('GITEA_TOKEN') ? project.property('GITEA_TOKEN') : System.getenv('GITEA_TOKEN')
}
authentication {
header(HttpHeaderAuthentication)
}
}
}
if ((project.hasProperty('SONATYPE_USER') || System.getenv('SONATYPE_USER') != null) && (project.hasProperty('SONATYPE_PASSWORD') || System.getenv('SONATYPE_PASSWORD') != null)) {
maven {
name = "sonatype"
url = uri("https://oss.sonatype.org/service/local/staging/deploy/maven2/")
credentials {
username = project.hasProperty('SONATYPE_USER') ? project.property('SONATYPE_USER') : System.getenv('SONATYPE_USER')
password = project.hasProperty('SONATYPE_PASSWORD') ? project.property('SONATYPE_PASSWORD') : System.getenv('SONATYPE_PASSWORD')
}
}
} }
} }
} }
} }
if (project.hasProperty("signing.gnupg.keyName")) { bintrayUpload.doFirst {
apply plugin: 'signing' publications = publishing.publications.collect {
if (it.name.contains('kotlinMultiplatform')) {
signing { null
useGpgCmd() } else {
it.name
sign publishing.publications
}
task signAll {
tasks.withType(Sign).forEach {
dependsOn(it)
} }
} } - null
} }
bintrayUpload.dependsOn publishToMavenLocal

View File

@ -1 +0,0 @@
{"licenses":[{"id":"Apache-2.0","title":"Apache Software License 2.0","url":"https://github.com/InsanusMokrassar/SauceNaoAPI/blob/master/LICENSE"}],"mavenConfig":{"name":"SauceNao API","description":"SauceNao API library","url":"https://insanusmokrassar.github.io/${project.name}","vcsUrl":"https://github.com/insanusmokrassar/${project.name}.git","developers":[{"id":"InsanusMokrassar","name":"Ovsyannikov Alexey","eMail":"ovsyannikov.alexey95@gmail.com"}],"repositories":[{"name":"GithubPackages","url":"https://maven.pkg.github.com/InsanusMokrassar/SauceNaoAPI"},{"name":"Gitea","url":"https://git.inmo.dev/api/packages/InsanusMokrassar/maven","credsType":{"type":"dev.inmo.kmppscriptbuilder.core.models.MavenPublishingRepository.CredentialsType.HttpHeaderCredentials","headerName":"Authorization","headerValueProperty":"GITEA_TOKEN"}},{"name":"sonatype","url":"https://oss.sonatype.org/service/local/staging/deploy/maven2/"}],"gpgSigning":{"type":"dev.inmo.kmppscriptbuilder.core.models.GpgSigning.Optional"}}}

View File

@ -1,3 +1 @@
rootProject.name = 'saucenaoapi' rootProject.name = 'SauceNaoAPI'
enableFeaturePreview("VERSION_CATALOGS")

View File

@ -0,0 +1,17 @@
package com.insanusmokrassar.SauceNaoAPI
sealed class OutputType {
abstract val typeCode: Int
}
object HtmlOutputType : OutputType() {
override val typeCode: Int = 0
}
object XmlOutputType : OutputType() {
override val typeCode: Int = 1
}
object JsonOutputType : OutputType() {
override val typeCode: Int = 2
}

View File

@ -0,0 +1,215 @@
package com.insanusmokrassar.SauceNaoAPI
import com.insanusmokrassar.SauceNaoAPI.exceptions.TooManyRequestsException
import com.insanusmokrassar.SauceNaoAPI.exceptions.sauceNaoAPIException
import com.insanusmokrassar.SauceNaoAPI.models.*
import com.insanusmokrassar.SauceNaoAPI.utils.*
import io.ktor.client.HttpClient
import io.ktor.client.features.ClientRequestException
import io.ktor.client.request.*
import io.ktor.client.request.forms.MultiPartFormDataContent
import io.ktor.client.request.forms.formData
import io.ktor.client.statement.HttpResponse
import io.ktor.client.statement.readText
import io.ktor.http.*
import io.ktor.utils.io.core.Input
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import kotlinx.serialization.json.Json
import kotlin.coroutines.*
private const val API_TOKEN_FIELD = "api_key"
private const val OUTPUT_TYPE_FIELD = "output_type"
private const val URL_FIELD = "url"
private const val FILE_FIELD = "file"
private const val FILENAME_FIELD = "filename"
private const val DB_FIELD = "db"
private const val DBMASK_FIELD = "dbmask"
private const val DBMASKI_FIELD = "dbmaski"
private const val RESULTS_COUNT_FIELD = "numres"
private const val MINIMAL_SIMILARITY_FIELD = "minsim"
private const val SEARCH_URL = "https://saucenao.com/search.php"
val defaultSauceNaoParser = Json {
allowSpecialFloatingPointValues = true
allowStructuredMapKeys = true
ignoreUnknownKeys = true
useArrayPolymorphism = true
}
data class SauceNaoAPI(
private val apiToken: String? = null,
private val outputType: OutputType = JsonOutputType,
private val client: HttpClient = HttpClient(),
private val searchUrl: String = SEARCH_URL,
private val scope: CoroutineScope = CoroutineScope(Dispatchers.Default),
private val parser: Json = defaultSauceNaoParser
) : SauceCloseable {
private val requestsChannel = Channel<Pair<Continuation<SauceNaoAnswer>, HttpRequestBuilder>>(Channel.UNLIMITED)
private val timeManager = TimeManager(scope)
private val quotaManager = RequestQuotaManager(scope)
val limitsState: LimitsState
get() = quotaManager.limitsState
private val requestsJob = scope.launch {
for ((callback, requestBuilder) in requestsChannel) {
quotaManager.getQuota()
launch {
try {
val answer = makeRequest(requestBuilder)
callback.resume(answer)
quotaManager.updateQuota(answer.header, timeManager)
} catch (e: TooManyRequestsException) {
quotaManager.happenTooManyRequests(timeManager, e)
requestsChannel.send(callback to requestBuilder)
} catch (e: Exception) {
try {
callback.resumeWithException(e)
} catch (e: IllegalStateException) { // may happen when already resumed and api was closed
// do nothing
}
}
}
}
}
suspend fun request(
url: String,
resultsCount: Int? = null,
minSimilarity: Float? = null
): SauceNaoAnswer? = makeRequest(
url.asSauceRequestSubject,
resultsCount = resultsCount,
minSimilarity = minSimilarity
)
suspend fun request(
mediaInput: Input,
mimeType: ContentType,
resultsCount: Int? = null,
minSimilarity: Float? = null
): SauceNaoAnswer? = makeRequest(
mediaInput.asSauceRequestSubject(mimeType),
resultsCount = resultsCount,
minSimilarity = minSimilarity
)
suspend fun requestByDb(
url: String,
db: Int,
resultsCount: Int? = null,
minSimilarity: Float? = null
): SauceNaoAnswer? = makeRequest(
url.asSauceRequestSubject,
db = db,
resultsCount = resultsCount,
minSimilarity = minSimilarity
)
suspend fun requestByMask(
url: String,
dbmask: Int,
resultsCount: Int? = null,
minSimilarity: Float? = null
): SauceNaoAnswer? = makeRequest(
url.asSauceRequestSubject,
dbmask = dbmask,
resultsCount = resultsCount,
minSimilarity = minSimilarity
)
suspend fun requestByMaskI(
url: String,
dbmaski: Int,
resultsCount: Int? = null,
minSimilarity: Float? = null
): SauceNaoAnswer? = makeRequest(
url.asSauceRequestSubject,
dbmaski = dbmaski,
resultsCount = resultsCount,
minSimilarity = minSimilarity
)
private suspend fun makeRequest(
builder: HttpRequestBuilder
): SauceNaoAnswer {
return try {
val call = client.request<HttpResponse>(builder)
val answerText = call.readText()
timeManager.addTimeAndClear()
parser.decodeFromString(
SauceNaoAnswerSerializer,
answerText
)
} catch (e: ClientRequestException) {
throw e.sauceNaoAPIException()
}
}
private suspend fun makeRequest(
request: SauceRequestSubject,
db: Int? = null,
dbmask: Int? = null,
dbmaski: Int? = null,
resultsCount: Int? = null,
minSimilarity: Float? = null
): SauceNaoAnswer? {
return suspendCoroutine<SauceNaoAnswer> {
requestsChannel.offer(
it to HttpRequestBuilder().apply {
url(searchUrl)
apiToken ?.also { parameter(API_TOKEN_FIELD, it) }
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) }
when (request) {
is UrlSauceRequestSubject -> {
parameter(URL_FIELD, request.url)
}
is InputRequestSubject -> {
val mimeType = request.mimeType
method = HttpMethod.Post
body = MultiPartFormDataContent(formData {
appendInput(
FILE_FIELD,
Headers.build {
append(HttpHeaders.ContentType, mimeType.toString())
val fakeFilename = "filename=file" + when (mimeType) {
ContentType.Image.GIF -> ".gif"
ContentType.Image.JPEG -> ".jpeg"
ContentType.Image.PNG -> ".png"
ContentType.Image.SVG -> ".svg"
else -> throw IllegalArgumentException(
"Currently supported formats for uploading in sauce: gif, jpeg, png, svg"
)
}
append(HttpHeaders.ContentDisposition, "filename=$fakeFilename")
},
block = request::input
)
})
}
}
}
)
}
}
override fun close() {
requestsChannel.close()
client.close()
requestsJob.cancel()
timeManager.close()
quotaManager.close()
}
}

View File

@ -1,4 +1,4 @@
package dev.inmo.saucenaoapi package com.insanusmokrassar.SauceNaoAPI
import io.ktor.http.ContentType import io.ktor.http.ContentType
import io.ktor.utils.io.core.Input import io.ktor.utils.io.core.Input

View File

@ -0,0 +1,23 @@
package com.insanusmokrassar.SauceNaoAPI.additional
import com.insanusmokrassar.SauceNaoAPI.additional.header.ResultMetaInfo
import com.insanusmokrassar.SauceNaoAPI.additional.header.adapted
import com.insanusmokrassar.SauceNaoAPI.additional.results.AdaptedResult
import com.insanusmokrassar.SauceNaoAPI.additional.results.adapted
import com.insanusmokrassar.SauceNaoAPI.models.SauceNaoAnswer
val SauceNaoAnswer.adapted: AdaptedAnswer
get() = header.adapted.let { resultMetainfo ->
val adaptedResults = results.map {
it.adapted(resultMetainfo)
}
AdaptedAnswer(
resultMetainfo,
adaptedResults
)
}
data class AdaptedAnswer(
val resultMetaInfo: ResultMetaInfo,
val results: List<AdaptedResult>
)

View File

@ -1,6 +1,6 @@
package dev.inmo.saucenaoapi.additional package com.insanusmokrassar.SauceNaoAPI.additional
import korlibs.time.TimeSpan import com.soywiz.klock.TimeSpan
typealias AccountType = Int typealias AccountType = Int
const val defaultAccountType: AccountType = 1 // "basic" const val defaultAccountType: AccountType = 1 // "basic"

View File

@ -1,7 +1,7 @@
package dev.inmo.saucenaoapi.additional.header package com.insanusmokrassar.SauceNaoAPI.additional.header
import dev.inmo.saucenaoapi.additional.* import com.insanusmokrassar.SauceNaoAPI.additional.*
import dev.inmo.saucenaoapi.models.Header import com.insanusmokrassar.SauceNaoAPI.models.Header
val Header.shortLimitStatus: LimitStatus val Header.shortLimitStatus: LimitStatus
get() = LimitStatus( get() = LimitStatus(
@ -29,27 +29,15 @@ val Header.accountInfo
data class LimitStatus( data class LimitStatus(
val remain: Int = Int.MAX_VALUE, val remain: Int = Int.MAX_VALUE,
val limit: Int = Int.MAX_VALUE val limit: Int = Int.MAX_VALUE
) : Comparable<LimitStatus> { )
override fun compareTo(other: LimitStatus): Int = when {
limit == other.limit && remain == other.remain -> 0
else -> remain.compareTo(other.remain)
}
}
data class Limits( data class Limits(
val short: LimitStatus = LimitStatus(), val short: LimitStatus = LimitStatus(),
val long: LimitStatus = LimitStatus() val long: LimitStatus = LimitStatus()
) : Comparable<Limits> { )
override fun compareTo(other: Limits): Int = when {
long == other.long && short == other.short -> 0
else -> short.remain.compareTo(other.short.remain)
}
}
data class AccountInfo( data class AccountInfo(
val accountType: AccountType = defaultAccountType, val accountType: AccountType = defaultAccountType,
val userId: UserId? = null, val userId: UserId? = null,
val limits: Limits = Limits() val limits: Limits = Limits()
) : Comparable<AccountInfo> { )
override fun compareTo(other: AccountInfo): Int = limits.compareTo(other.limits)
}

View File

@ -1,6 +1,6 @@
package dev.inmo.saucenaoapi.additional.header package com.insanusmokrassar.SauceNaoAPI.additional.header
import dev.inmo.saucenaoapi.models.Header import com.insanusmokrassar.SauceNaoAPI.models.Header
data class IndexInfo( data class IndexInfo(
val id: Int, val id: Int,

View File

@ -1,6 +1,6 @@
package dev.inmo.saucenaoapi.additional.header package com.insanusmokrassar.SauceNaoAPI.additional.header
import dev.inmo.saucenaoapi.models.Header import com.insanusmokrassar.SauceNaoAPI.models.Header
val Header.queryPreview val Header.queryPreview
get() = QueryResultPreview( get() = QueryResultPreview(

View File

@ -1,6 +1,6 @@
package dev.inmo.saucenaoapi.additional.header package com.insanusmokrassar.SauceNaoAPI.additional.header
import dev.inmo.saucenaoapi.models.Header import com.insanusmokrassar.SauceNaoAPI.models.Header
data class ResultMetaInfo( data class ResultMetaInfo(
val accountInfo: AccountInfo = AccountInfo(), val accountInfo: AccountInfo = AccountInfo(),

View File

@ -1,9 +1,9 @@
package dev.inmo.saucenaoapi.additional.results package com.insanusmokrassar.SauceNaoAPI.additional.results
import dev.inmo.saucenaoapi.additional.header.IndexInfo import com.insanusmokrassar.SauceNaoAPI.additional.header.IndexInfo
import dev.inmo.saucenaoapi.additional.header.ResultMetaInfo import com.insanusmokrassar.SauceNaoAPI.additional.header.ResultMetaInfo
import dev.inmo.saucenaoapi.models.Result import com.insanusmokrassar.SauceNaoAPI.models.Result
import dev.inmo.saucenaoapi.models.ResultData import com.insanusmokrassar.SauceNaoAPI.models.ResultData
fun Result.adapted( fun Result.adapted(
resultMetaInfo: ResultMetaInfo resultMetaInfo: ResultMetaInfo

View File

@ -0,0 +1,9 @@
package com.insanusmokrassar.SauceNaoAPI.additional.results
import com.insanusmokrassar.SauceNaoAPI.additional.header.IndexInfo
data class ResultHeader(
val similarity: Float,
val thumbnail: String,
val index: IndexInfo
)

View File

@ -1,25 +1,26 @@
package dev.inmo.saucenaoapi.exceptions package com.insanusmokrassar.SauceNaoAPI.exceptions
import korlibs.time.TimeSpan import com.insanusmokrassar.SauceNaoAPI.additional.LONG_TIME_RECALCULATING_MILLIS
import dev.inmo.saucenaoapi.additional.LONG_TIME_RECALCULATING_MILLIS import com.insanusmokrassar.SauceNaoAPI.additional.SHORT_TIME_RECALCULATING_MILLIS
import dev.inmo.saucenaoapi.additional.SHORT_TIME_RECALCULATING_MILLIS import com.soywiz.klock.TimeSpan
import io.ktor.client.plugins.ClientRequestException import io.ktor.client.features.ClientRequestException
import io.ktor.client.statement.bodyAsText import io.ktor.client.statement.readText
import io.ktor.http.HttpStatusCode.Companion.TooManyRequests import io.ktor.http.HttpStatusCode.Companion.TooManyRequests
import io.ktor.utils.io.errors.IOException import io.ktor.utils.io.errors.IOException
internal suspend fun ClientRequestException.sauceNaoAPIException(): Exception { internal suspend fun ClientRequestException.sauceNaoAPIException(): Exception {
return when (response.status) { val response = response ?: return this
TooManyRequests -> { return when (response.status) {
val answerContent = response.bodyAsText() TooManyRequests -> {
when { val answerContent = response.readText()
answerContent.contains("daily limit") -> TooManyRequestsLongException(answerContent) when {
else -> TooManyRequestsShortException(answerContent) answerContent.contains("daily limit") -> TooManyRequestsLongException(answerContent)
else -> TooManyRequestsShortException(answerContent)
}
} }
else -> this
} }
else -> this
} }
}
sealed class TooManyRequestsException(message: String, cause: Throwable? = null) : IOException(message, cause) { sealed class TooManyRequestsException(message: String, cause: Throwable? = null) : IOException(message, cause) {
abstract val answerContent: String abstract val answerContent: String

View File

@ -1,6 +1,6 @@
package dev.inmo.saucenaoapi.models package com.insanusmokrassar.SauceNaoAPI.models
import dev.inmo.saucenaoapi.defaultSauceNaoParser import com.insanusmokrassar.SauceNaoAPI.defaultSauceNaoParser
import kotlinx.serialization.* import kotlinx.serialization.*
import kotlinx.serialization.builtins.serializer import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.descriptors.SerialDescriptor

View File

@ -1,4 +1,4 @@
package dev.inmo.saucenaoapi.models package com.insanusmokrassar.SauceNaoAPI.models
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable

View File

@ -1,4 +1,4 @@
package dev.inmo.saucenaoapi.models package com.insanusmokrassar.SauceNaoAPI.models
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@ -8,6 +8,4 @@ data class LimitsState(
val maxLongQuota: Int, val maxLongQuota: Int,
val knownShortQuota: Int, val knownShortQuota: Int,
val knownLongQuota: Int val knownLongQuota: Int
) : Comparable<LimitsState> { )
override fun compareTo(other: LimitsState): Int = knownShortQuota.compareTo(other.knownShortQuota)
}

View File

@ -1,4 +1,4 @@
package dev.inmo.saucenaoapi.models package com.insanusmokrassar.SauceNaoAPI.models
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable

View File

@ -1,6 +1,6 @@
package dev.inmo.saucenaoapi.models package com.insanusmokrassar.SauceNaoAPI.models
import dev.inmo.saucenaoapi.utils.CommonMultivariantStringSerializer import com.insanusmokrassar.SauceNaoAPI.utils.CommonMultivariantStringSerializer
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@ -159,18 +159,3 @@ data class ResultData(
@SerialName("ext_urls") @SerialName("ext_urls")
val extUrls: List<String> = emptyList() val extUrls: List<String> = emptyList()
) )
val ResultData.froms: List<String>
get() = material ?.split(", ") ?: emptyList()
val ResultData.authors: List<String>
get() = (creator ?.split(", ") ?: emptyList()) + (memberName ?.split(", ") ?: emptyList())
val ResultData.charactersList: List<String>
get() = characters ?.split(", ") ?: emptyList()
val ResultData.titles: List<String>
get() = title ?.split(", ") ?: emptyList()
val ResultData.urls: List<String>
get() = extUrls + (url ?.split(", ") ?: emptyList())

View File

@ -1,4 +1,4 @@
package dev.inmo.saucenaoapi.models package com.insanusmokrassar.SauceNaoAPI.models
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable

View File

@ -1,6 +1,6 @@
package dev.inmo.saucenaoapi.models package com.insanusmokrassar.SauceNaoAPI.models
import dev.inmo.saucenaoapi.defaultSauceNaoParser import com.insanusmokrassar.SauceNaoAPI.defaultSauceNaoParser
import kotlinx.serialization.* import kotlinx.serialization.*
import kotlinx.serialization.builtins.ListSerializer import kotlinx.serialization.builtins.ListSerializer
import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Decoder

View File

@ -1,13 +1,17 @@
package dev.inmo.saucenaoapi.utils package com.insanusmokrassar.SauceNaoAPI.utils
import kotlinx.serialization.KSerializer import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializer import kotlinx.serialization.Serializer
import kotlinx.serialization.builtins.ListSerializer
import kotlinx.serialization.builtins.serializer import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.json.* import kotlinx.serialization.json.*
@Serializer(String::class) @Serializer(String::class)
object CommonMultivariantStringSerializer : KSerializer<String> by String.serializer() { object CommonMultivariantStringSerializer : KSerializer<String> by String.serializer() {
private val stringArraySerializer = ListSerializer(String.serializer())
override fun deserialize(decoder: Decoder): String { override fun deserialize(decoder: Decoder): String {
return when (val parsed = JsonElement.serializer().deserialize(decoder)) { return when (val parsed = JsonElement.serializer().deserialize(decoder)) {
is JsonPrimitive -> parsed.content is JsonPrimitive -> parsed.content

View File

@ -1,47 +1,26 @@
package dev.inmo.saucenaoapi.utils package com.insanusmokrassar.SauceNaoAPI.utils
import korlibs.time.DateTime import com.insanusmokrassar.SauceNaoAPI.additional.LONG_TIME_RECALCULATING_MILLIS
import dev.inmo.saucenaoapi.additional.LONG_TIME_RECALCULATING_MILLIS import com.insanusmokrassar.SauceNaoAPI.additional.SHORT_TIME_RECALCULATING_MILLIS
import dev.inmo.saucenaoapi.additional.SHORT_TIME_RECALCULATING_MILLIS import com.insanusmokrassar.SauceNaoAPI.exceptions.TooManyRequestsException
import dev.inmo.saucenaoapi.exceptions.TooManyRequestsException import com.insanusmokrassar.SauceNaoAPI.exceptions.TooManyRequestsLongException
import dev.inmo.saucenaoapi.exceptions.TooManyRequestsLongException import com.insanusmokrassar.SauceNaoAPI.models.Header
import dev.inmo.saucenaoapi.models.Header import com.insanusmokrassar.SauceNaoAPI.models.LimitsState
import dev.inmo.saucenaoapi.models.LimitsState import com.soywiz.klock.DateTime
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.MutableStateFlow import kotlin.coroutines.suspendCoroutine
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge
import kotlin.math.max import kotlin.math.max
import kotlin.math.min import kotlin.math.min
internal class RequestQuotaManager ( internal class RequestQuotaManager (
scope: CoroutineScope scope: CoroutineScope
) { ) : SauceCloseable {
private val _longQuotaFlow = MutableStateFlow(1) private var longQuota = 1
private val _shortQuotaFlow = MutableStateFlow(1) private var shortQuota = 1
private val _longMaxQuotaFlow = MutableStateFlow(1) private var longMaxQuota = 1
private val _shortMaxQuotaFlow = MutableStateFlow(1) private var shortMaxQuota = 1
private var longQuota by _longQuotaFlow::value
private var shortQuota by _shortQuotaFlow::value
private var longMaxQuota by _longMaxQuotaFlow::value
private var shortMaxQuota by _shortMaxQuotaFlow::value
val longQuotaFlow = _longQuotaFlow.asStateFlow()
val shortQuotaFlow = _shortQuotaFlow.asStateFlow()
val longMaxQuotaFlow = _longMaxQuotaFlow.asStateFlow()
val shortMaxQuotaFlow = _shortMaxQuotaFlow.asStateFlow()
val limitsStateFlow = merge(
longQuotaFlow, shortQuotaFlow, longMaxQuotaFlow, shortMaxQuotaFlow
).map { _ ->
LimitsState(
shortMaxQuota,
longMaxQuota,
shortQuota,
longQuota
)
}
val limitsState: LimitsState val limitsState: LimitsState
get() = LimitsState( get() = LimitsState(
shortMaxQuota, shortMaxQuota,
@ -56,10 +35,6 @@ internal class RequestQuotaManager (
for (callback in quotaActions) { for (callback in quotaActions) {
callback() callback()
} }
}.also {
it.invokeOnCompletion {
quotaActions.close(it)
}
} }
private suspend fun updateQuota( private suspend fun updateQuota(
@ -108,16 +83,21 @@ internal class RequestQuotaManager (
) )
suspend fun getQuota() { suspend fun getQuota() {
val job = Job() return suspendCoroutine {
lateinit var callback: suspend () -> Unit lateinit var callback: suspend () -> Unit
callback = suspend { callback = suspend {
if (longQuota > 0 && shortQuota > 0) { if (longQuota > 0 && shortQuota > 0) {
job.complete() it.resumeWith(Result.success(Unit))
} else { } else {
quotaActions.send(callback) quotaActions.send(callback)
}
} }
quotaActions.offer(callback)
} }
quotaActions.trySend(callback) }
return job.join()
override fun close() {
quotaJob.cancel()
quotaActions.close()
} }
} }

View File

@ -1,4 +1,4 @@
package dev.inmo.saucenaoapi.utils package com.insanusmokrassar.SauceNaoAPI.utils
import kotlinx.coroutines.supervisorScope import kotlinx.coroutines.supervisorScope
@ -6,13 +6,12 @@ interface SauceCloseable {
fun close() fun close()
} }
inline fun <T> SauceCloseable.use(block: (SauceCloseable) -> T): T = try { fun <T> SauceCloseable.use(block: (SauceCloseable) -> T): T = try {
block(this) block(this)
} finally { } finally {
close() close()
} }
@Deprecated("Useless")
suspend fun <T> SauceCloseable.useSafe(block: suspend (SauceCloseable) -> T): T = try { suspend fun <T> SauceCloseable.useSafe(block: suspend (SauceCloseable) -> T): T = try {
supervisorScope { supervisorScope {
block(this@useSafe) block(this@useSafe)

View File

@ -0,0 +1,18 @@
package com.insanusmokrassar.SauceNaoAPI.utils
import com.insanusmokrassar.SauceNaoAPI.additional.LONG_TIME_RECALCULATING_MILLIS
import com.insanusmokrassar.SauceNaoAPI.additional.SHORT_TIME_RECALCULATING_MILLIS
import com.insanusmokrassar.SauceNaoAPI.models.Header
import com.soywiz.klock.DateTime
internal suspend fun calculateSleepTime(
header: Header,
mostOldestInShortPeriodGetter: suspend () -> DateTime?,
mostOldestInLongPeriodGetter: suspend () -> DateTime?
): DateTime? {
return when {
header.longRemaining < 1 -> mostOldestInLongPeriodGetter() ?.plus(LONG_TIME_RECALCULATING_MILLIS)
header.shortRemaining < 1 -> mostOldestInShortPeriodGetter() ?.plus(SHORT_TIME_RECALCULATING_MILLIS)
else -> null
}
}

View File

@ -1,10 +1,13 @@
package dev.inmo.saucenaoapi.utils package com.insanusmokrassar.SauceNaoAPI.utils
import korlibs.time.DateTime import com.insanusmokrassar.SauceNaoAPI.additional.LONG_TIME_RECALCULATING_MILLIS
import dev.inmo.saucenaoapi.additional.LONG_TIME_RECALCULATING_MILLIS import com.insanusmokrassar.SauceNaoAPI.additional.SHORT_TIME_RECALCULATING_MILLIS
import dev.inmo.saucenaoapi.additional.SHORT_TIME_RECALCULATING_MILLIS import com.soywiz.klock.DateTime
import kotlinx.coroutines.* import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.launch
import kotlin.coroutines.Continuation
import kotlin.coroutines.suspendCoroutine
private fun MutableList<DateTime>.clearTooOldTimes(relatedTo: DateTime = DateTime.now()) { private fun MutableList<DateTime>.clearTooOldTimes(relatedTo: DateTime = DateTime.now()) {
val limitValue = relatedTo - LONG_TIME_RECALCULATING_MILLIS val limitValue = relatedTo - LONG_TIME_RECALCULATING_MILLIS
@ -35,16 +38,16 @@ private data class TimeManagerTimeAdder(
} }
private data class TimeManagerMostOldestInLongGetter( private data class TimeManagerMostOldestInLongGetter(
private val deferred: CompletableDeferred<DateTime?> private val continuation: Continuation<DateTime?>
) : TimeManagerAction { ) : TimeManagerAction {
override suspend fun makeChangeWith(times: MutableList<DateTime>) { override suspend fun makeChangeWith(times: MutableList<DateTime>) {
times.clearTooOldTimes() times.clearTooOldTimes()
deferred.complete(times.minOrNull()) continuation.resumeWith(Result.success(times.minOrNull()))
} }
} }
private data class TimeManagerMostOldestInShortGetter( private data class TimeManagerMostOldestInShortGetter(
private val deferred: CompletableDeferred<DateTime?> private val continuation: Continuation<DateTime?>
) : TimeManagerAction { ) : TimeManagerAction {
override suspend fun makeChangeWith(times: MutableList<DateTime>) { override suspend fun makeChangeWith(times: MutableList<DateTime>) {
times.clearTooOldTimes() times.clearTooOldTimes()
@ -53,17 +56,19 @@ private data class TimeManagerMostOldestInShortGetter(
val limitTime = now - SHORT_TIME_RECALCULATING_MILLIS val limitTime = now - SHORT_TIME_RECALCULATING_MILLIS
deferred.complete( continuation.resumeWith(
times.asSequence().filter { Result.success(
limitTime < it times.asSequence().filter {
}.minOrNull() limitTime < it
}.minOrNull()
)
) )
} }
} }
internal class TimeManager( internal class TimeManager(
scope: CoroutineScope scope: CoroutineScope
) { ) : SauceCloseable {
private val actionsChannel = Channel<TimeManagerAction>(Channel.UNLIMITED) private val actionsChannel = Channel<TimeManagerAction>(Channel.UNLIMITED)
private val timeUpdateJob = scope.launch { private val timeUpdateJob = scope.launch {
@ -71,10 +76,6 @@ internal class TimeManager(
for (action in actionsChannel) { for (action in actionsChannel) {
action(times) action(times)
} }
}.also {
it.invokeOnCompletion {
actionsChannel.close(it)
}
} }
suspend fun addTimeAndClear() { suspend fun addTimeAndClear() {
@ -82,20 +83,21 @@ internal class TimeManager(
} }
suspend fun getMostOldestInLongPeriod(): DateTime? { suspend fun getMostOldestInLongPeriod(): DateTime? {
val deferred = CompletableDeferred<DateTime?>() return suspendCoroutine {
return if (actionsChannel.trySend(TimeManagerMostOldestInLongGetter(deferred)).isSuccess) { actionsChannel.offer(
deferred.await() TimeManagerMostOldestInLongGetter(it)
} else { )
null
} }
} }
suspend fun getMostOldestInShortPeriod(): DateTime? { suspend fun getMostOldestInShortPeriod(): DateTime? {
val deferred = CompletableDeferred<DateTime?>() return suspendCoroutine {
return if (actionsChannel.trySend(TimeManagerMostOldestInShortGetter(deferred)).isSuccess) { actionsChannel.offer(TimeManagerMostOldestInShortGetter(it))
deferred.await()
} else {
null
} }
} }
override fun close() {
actionsChannel.close()
timeUpdateJob.cancel()
}
} }

View File

@ -1,17 +0,0 @@
package dev.inmo.saucenaoapi
sealed class OutputType {
abstract val typeCode: Int
}
object HtmlOutputType : dev.inmo.saucenaoapi.OutputType() {
override val typeCode: Int = 0
}
object XmlOutputType : dev.inmo.saucenaoapi.OutputType() {
override val typeCode: Int = 1
}
object JsonOutputType : dev.inmo.saucenaoapi.OutputType() {
override val typeCode: Int = 2
}

View File

@ -1,366 +0,0 @@
package dev.inmo.saucenaoapi
import dev.inmo.micro_utils.common.MPPFile
import dev.inmo.micro_utils.ktor.common.input
import dev.inmo.saucenaoapi.exceptions.TooManyRequestsException
import dev.inmo.saucenaoapi.exceptions.sauceNaoAPIException
import dev.inmo.saucenaoapi.models.*
import dev.inmo.saucenaoapi.utils.*
import io.ktor.client.HttpClient
import io.ktor.client.plugins.ClientRequestException
import io.ktor.client.request.*
import io.ktor.client.request.forms.MultiPartFormDataContent
import io.ktor.client.request.forms.formData
import io.ktor.client.statement.bodyAsText
import io.ktor.http.*
import io.ktor.utils.io.core.Input
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import kotlinx.serialization.json.Json
private const val API_TOKEN_FIELD = "api_key"
private const val OUTPUT_TYPE_FIELD = "output_type"
private const val URL_FIELD = "url"
private const val FILE_FIELD = "file"
private const val FILENAME_FIELD = "filename"
private const val DB_FIELD = "db"
private const val DBS_FIELD = "dbs[]"
private const val DBMASK_FIELD = "dbmask"
private const val DBMASKI_FIELD = "dbmaski"
private const val RESULTS_COUNT_FIELD = "numres"
private const val MINIMAL_SIMILARITY_FIELD = "minsim"
private const val SEARCH_URL = "https://saucenao.com/search.php"
val defaultSauceNaoParser = Json {
allowSpecialFloatingPointValues = true
allowStructuredMapKeys = true
ignoreUnknownKeys = true
useArrayPolymorphism = true
}
data class SauceNaoAPI(
private val apiToken: String? = null,
private val client: HttpClient = HttpClient(),
private val searchUrl: String = SEARCH_URL,
private val scope: CoroutineScope = CoroutineScope(Dispatchers.Default),
private val parser: Json = defaultSauceNaoParser
) : SauceCloseable {
private val requestsChannel = Channel<Pair<CompletableDeferred<SauceNaoAnswer>, HttpRequestBuilder>>(Channel.UNLIMITED)
private val subscope = CoroutineScope(scope.coroutineContext + SupervisorJob(scope.coroutineContext.job)).also {
it.coroutineContext.job.invokeOnCompletion {
requestsChannel.close(it)
}
}
private val timeManager = TimeManager(subscope)
private val quotaManager = RequestQuotaManager(subscope)
val limitsState: LimitsState by quotaManager::limitsState
val longQuotaFlow by quotaManager::longQuotaFlow
val shortQuotaFlow by quotaManager::shortQuotaFlow
val longMaxQuotaFlow by quotaManager::longMaxQuotaFlow
val shortMaxQuotaFlow by quotaManager::shortMaxQuotaFlow
val limitsStateFlow by quotaManager::limitsStateFlow
private val requestsJob = subscope.launch {
for ((callback, requestBuilder) in requestsChannel) {
quotaManager.getQuota()
launch {
try {
val answer = makeRequest(requestBuilder)
callback.complete(answer)
quotaManager.updateQuota(answer.header, timeManager)
} catch (e: TooManyRequestsException) {
quotaManager.happenTooManyRequests(timeManager, e)
requestsChannel.send(callback to requestBuilder)
} catch (e: Exception) {
try {
callback.completeExceptionally(e)
} catch (e: IllegalStateException) { // may happen when already resumed and api was closed
// do nothing
}
}
}
}
}
suspend fun request(
url: String,
resultsCount: Int? = null,
minSimilarity: Float? = null
): SauceNaoAnswer = makeRequest(
url.asSauceRequestSubject,
resultsCount = resultsCount,
minSimilarity = minSimilarity
)
/**
* @param db search a specific index number or all without needing to generate a bitmask.
* @param dbs search one or more specific index number, set more than once to search multiple.
*/
suspend fun requestByDBs(
url: String,
db: Int? = null,
dbs: Array<Int>? = null,
resultsCount: Int? = null,
minSimilarity: Float? = null
): SauceNaoAnswer = makeRequest(
url.asSauceRequestSubject,
db = db,
dbs = dbs,
resultsCount = resultsCount,
minSimilarity = minSimilarity
)
/**
* @param mask Mask for selecting specific indexes to ENABLE. dbmask=8191 will search all of the first 14 indexes. If intending to search all databases, the db=999 option is more appropriate.
* @param excludedMask Mask for selecting specific indexes to DISABLE. dbmaski=8191 would search only indexes higher than the first 14. This is ideal when attempting to disable only certain indexes, while allowing future indexes to be included by default.
*
* Bitmask Note: Index numbers start with 0. Even though pixiv is labeled as index 5, it would be controlled with the 6th bit position, which has a decimal value of 32 when set.
* db=<index num or 999 for all>
*/
suspend fun requestByMasks(
url: String,
mask: Int?,
excludedMask: Int? = null,
resultsCount: Int? = null,
minSimilarity: Float? = null
): SauceNaoAnswer = makeRequest(
url.asSauceRequestSubject,
dbmask = mask,
dbmaski = excludedMask,
resultsCount = resultsCount,
minSimilarity = minSimilarity
)
suspend fun request(
mediaInput: Input,
mimeType: ContentType,
resultsCount: Int? = null,
minSimilarity: Float? = null
): SauceNaoAnswer = makeRequest(
mediaInput.asSauceRequestSubject(mimeType),
resultsCount = resultsCount,
minSimilarity = minSimilarity
)
/**
* @param db search a specific index number or all without needing to generate a bitmask.
* @param dbs search one or more specific index number, set more than once to search multiple.
*/
suspend fun requestByDBs(
mediaInput: Input,
mimeType: ContentType,
db: Int? = null,
dbs: Array<Int>? = null,
resultsCount: Int? = null,
minSimilarity: Float? = null
): SauceNaoAnswer = makeRequest(
mediaInput.asSauceRequestSubject(mimeType),
db = db,
dbs = dbs,
resultsCount = resultsCount,
minSimilarity = minSimilarity
)
/**
* @param mask Mask for selecting specific indexes to ENABLE. dbmask=8191 will search all of the first 14 indexes. If intending to search all databases, the db=999 option is more appropriate.
* @param excludedMask Mask for selecting specific indexes to DISABLE. dbmaski=8191 would search only indexes higher than the first 14. This is ideal when attempting to disable only certain indexes, while allowing future indexes to be included by default.
*
* Bitmask Note: Index numbers start with 0. Even though pixiv is labeled as index 5, it would be controlled with the 6th bit position, which has a decimal value of 32 when set.
* db=<index num or 999 for all>
*/
suspend fun requestByMasks(
mediaInput: Input,
mimeType: ContentType,
mask: Int?,
excludedMask: Int? = null,
resultsCount: Int? = null,
minSimilarity: Float? = null
): SauceNaoAnswer = makeRequest(
mediaInput.asSauceRequestSubject(mimeType),
dbmask = mask,
dbmaski = excludedMask,
resultsCount = resultsCount,
minSimilarity = minSimilarity
)
suspend fun request(
file: MPPFile,
resultsCount: Int? = null,
minSimilarity: Float? = null
): SauceNaoAnswer = request(
file.input(),
file.contentType,
resultsCount,
minSimilarity
)
/**
* @param db search a specific index number or all without needing to generate a bitmask.
* @param dbs search one or more specific index number, set more than once to search multiple.
*/
suspend fun requestByDBs(
file: MPPFile,
db: Int? = null,
dbs: Array<Int>? = null,
resultsCount: Int? = null,
minSimilarity: Float? = null
): SauceNaoAnswer = requestByDBs(
file.input(),
file.contentType,
db,
dbs,
resultsCount,
minSimilarity
)
/**
* @param mask Mask for selecting specific indexes to ENABLE. dbmask=8191 will search all of the first 14 indexes. If intending to search all databases, the db=999 option is more appropriate.
* @param excludedMask Mask for selecting specific indexes to DISABLE. dbmaski=8191 would search only indexes higher than the first 14. This is ideal when attempting to disable only certain indexes, while allowing future indexes to be included by default.
*
* Bitmask Note: Index numbers start with 0. Even though pixiv is labeled as index 5, it would be controlled with the 6th bit position, which has a decimal value of 32 when set.
* db=<index num or 999 for all>
*/
suspend fun requestByMasks(
file: MPPFile,
mask: Int?,
excludedMask: Int? = null,
resultsCount: Int? = null,
minSimilarity: Float? = null
): SauceNaoAnswer = requestByMasks(
file.input(),
file.contentType,
mask,
excludedMask,
resultsCount,
minSimilarity
)
@Deprecated("Renamed", ReplaceWith("requestByDBs(url, db, null, resultsCount, minSimilarity)"))
suspend fun requestByDb(
url: String,
db: Int,
resultsCount: Int? = null,
minSimilarity: Float? = null
): SauceNaoAnswer = requestByDBs(
url,
db,
null,
resultsCount,
minSimilarity
)
@Deprecated("Renamed", ReplaceWith("requestByMasks(url, dbmask, null, resultsCount, minSimilarity)"))
suspend fun request(
url: String,
dbmask: Int,
resultsCount: Int? = null,
minSimilarity: Float? = null
): SauceNaoAnswer = requestByMasks(
url,
dbmask,
null,
resultsCount,
minSimilarity
)
@Deprecated("Renamed", ReplaceWith("requestByMasks(url, null, dbmaski, resultsCount, minSimilarity)"))
suspend fun requestByMaskI(
url: String,
dbmaski: Int,
resultsCount: Int? = null,
minSimilarity: Float? = null
): SauceNaoAnswer = requestByMasks(
url,
null,
dbmaski,
resultsCount,
minSimilarity
)
private suspend fun makeRequest(
builder: HttpRequestBuilder
): SauceNaoAnswer {
return try {
val call = client.request(builder)
val answerText = call.bodyAsText()
timeManager.addTimeAndClear()
parser.decodeFromString(
SauceNaoAnswerSerializer,
answerText
)
} catch (e: ClientRequestException) {
throw e.sauceNaoAPIException()
}
}
private suspend fun makeRequest(
request: SauceRequestSubject,
db: Int? = null,
dbs: Array<Int>? = null,
dbmask: Int? = null,
dbmaski: Int? = null,
resultsCount: Int? = null,
minSimilarity: Float? = null
): SauceNaoAnswer {
val deferred = CompletableDeferred<SauceNaoAnswer>()
requestsChannel.trySend(
deferred to HttpRequestBuilder().apply {
url(searchUrl)
apiToken ?.also { parameter(API_TOKEN_FIELD, it) }
parameter(OUTPUT_TYPE_FIELD, JsonOutputType.typeCode)
db ?.also { parameter(DB_FIELD, it) }
dbs ?.forEach { parameter(DBS_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) }
when (request) {
is UrlSauceRequestSubject -> {
parameter(URL_FIELD, request.url)
}
is InputRequestSubject -> {
val mimeType = request.mimeType
method = HttpMethod.Post
setBody(
MultiPartFormDataContent(
formData {
appendInput(
FILE_FIELD,
Headers.build {
append(HttpHeaders.ContentType, mimeType.toString())
val fakeFilename = "filename=file" + when (mimeType) {
ContentType.Image.GIF -> ".gif"
ContentType.Image.JPEG -> ".jpeg"
ContentType.Image.PNG -> ".png"
ContentType.Image.SVG -> ".svg"
else -> throw IllegalArgumentException(
"Currently supported formats for uploading in sauce: gif, jpeg, png, svg"
)
}
append(HttpHeaders.ContentDisposition, "filename=$fakeFilename")
},
block = request::input
)
}
)
)
}
}
}
)
return deferred.await()
}
override fun close() {
subscope.cancel()
}
}

View File

@ -1,23 +0,0 @@
package dev.inmo.saucenaoapi.additional
import dev.inmo.saucenaoapi.additional.header.ResultMetaInfo
import dev.inmo.saucenaoapi.additional.header.adapted
import dev.inmo.saucenaoapi.additional.results.AdaptedResult
import dev.inmo.saucenaoapi.additional.results.adapted
import dev.inmo.saucenaoapi.models.SauceNaoAnswer
val SauceNaoAnswer.adapted: AdaptedAnswer
get() = header.adapted.let { resultMetainfo ->
val adaptedResults = results.map {
it.adapted(resultMetainfo)
}
AdaptedAnswer(
resultMetainfo,
adaptedResults
)
}
data class AdaptedAnswer(
val resultMetaInfo: ResultMetaInfo,
val results: List<AdaptedResult>
)

View File

@ -1,9 +0,0 @@
package dev.inmo.saucenaoapi.additional.results
import dev.inmo.saucenaoapi.additional.header.IndexInfo
data class ResultHeader(
val similarity: Float,
val thumbnail: String,
val index: IndexInfo
)

View File

@ -1,26 +0,0 @@
package dev.inmo.saucenaoapi.utils
import dev.inmo.micro_utils.common.MPPFile
import dev.inmo.micro_utils.common.filename
import dev.inmo.micro_utils.ktor.common.input
import dev.inmo.micro_utils.mime_types.KnownMimeTypes
import dev.inmo.micro_utils.mime_types.getMimeType
import io.ktor.http.ContentType
import io.ktor.utils.io.core.Input
@Deprecated(
"MPPFile from microutils is preferable since 0.16.0",
ReplaceWith("MPPFile", "dev.inmo.micro_utils.common.MPPFile")
)
typealias MPPFile = MPPFile
@Deprecated(
"input() from microutils is preferable since 0.16.0",
ReplaceWith("this.input()", "dev.inmo.micro_utils.ktor.common.input")
)
val MPPFile.input: Input
get() = input()
val MPPFile.contentType: ContentType
get() = ContentType.parse(
getMimeType(stringWithExtension = filename.extension) ?.raw ?: KnownMimeTypes.Any.raw
)

View File

@ -1,27 +1,30 @@
import dev.inmo.saucenaoapi.SauceNaoAPI import com.insanusmokrassar.SauceNaoAPI.SauceNaoAPI
import io.ktor.client.HttpClient import com.insanusmokrassar.SauceNaoAPI.utils.useSafe
import io.ktor.http.ContentType
import io.ktor.utils.io.streams.asInput
import kotlinx.coroutines.* import kotlinx.coroutines.*
import java.io.File import java.io.File
import java.nio.file.Files
suspend fun main(vararg args: String) { suspend fun main(vararg args: String) {
val (key, requestSubject) = args val (key, requestSubject) = args
val client = HttpClient() val scope = CoroutineScope(Dispatchers.Default)
val scope = CoroutineScope(Dispatchers.IO).also {
it.coroutineContext.job.invokeOnCompletion {
client.close()
}
}
val api = SauceNaoAPI(key, client, scope = scope) val api = SauceNaoAPI(key, scope = scope)
println( api.useSafe { _ ->
when { println(
requestSubject.startsWith("/") -> File(requestSubject).let { when {
api.request(it) requestSubject.startsWith("/") -> File(requestSubject).let {
api.request(
it.inputStream().asInput(),
ContentType.parse(Files.probeContentType(it.toPath()))
)
}
else -> api.request(requestSubject)
} }
else -> api.request(requestSubject) )
} }
)
scope.cancel() scope.cancel()
} }