Compare commits

...

108 Commits

Author SHA1 Message Date
renovate[bot]
3c9ebc0d62 Update kt to v2.2.21 2025-10-23 12:52:26 +00:00
a97c117288 Merge pull request #199 from InsanusMokrassar/0.19.0
0.19.0
2025-10-04 23:47:44 +06:00
d5f09c6028 update github workflow java version 2025-10-04 23:19:11 +06:00
7cd2744a64 update repositories order 2025-10-04 23:15:08 +06:00
a2e2f48d53 add publication via nmcp 2025-10-04 23:09:08 +06:00
75f56f0aee update dependencies 2025-10-04 23:08:58 +06:00
b810fa40c2 start 0.19.0 2025-10-04 22:44:30 +06:00
081d567ba1 Update github_release.gradle 2024-12-09 10:15:32 +06:00
5a3b0362fa Merge pull request #191 from InsanusMokrassar/0.18.0
0.18.0
2024-12-09 10:11:54 +06:00
1fdbd9b8e6 remove gitea from publishing targets 2024-12-09 10:11:20 +06:00
2fe0d5c170 update dependencies 2024-12-09 10:01:03 +06:00
c2c8ffe19b start 0.18.0 2024-12-09 09:42:12 +06:00
a61211e24a Merge pull request #185 from InsanusMokrassar/0.17.2
0.17.2
2023-09-10 14:28:43 +06:00
caa2fb9b58 update dependencies 2023-09-10 14:28:04 +06:00
915cb09e37 start 0.17.2 2023-09-10 14:24:04 +06:00
930523f69a Merge pull request #180 from InsanusMokrassar/0.17.1
0.17.1
2023-07-16 21:45:27 +06:00
773415f579 update dependencies 2023-07-16 21:42:13 +06:00
21803cb321 start 0.17.1 2023-07-16 21:38:18 +06:00
c6643dbdbc Merge pull request #174 from InsanusMokrassar/0.17.0
0.17.0
2023-06-18 13:43:03 +06:00
58beceebf1 update dependencies 2023-06-18 13:42:17 +06:00
0acf4a88f4 start 0.17.0 2023-06-18 13:39:16 +06:00
ed48b3e4aa Merge pull request #165 from InsanusMokrassar/0.16.0
0.16.0 microutils
2023-05-11 16:26:04 +06:00
1ed123596c add microutils and support of native platforms 2023-05-11 16:24:38 +06:00
76a0dbdf27 Merge pull request #164 from InsanusMokrassar/0.16.0
0.16.0
2023-05-11 15:57:00 +06:00
33775f3304 start 0.16.0 2023-05-11 15:19:14 +06:00
f7ca2bda89 Merge pull request #160 from InsanusMokrassar/0.15.1
0.15.1
2023-03-03 00:05:46 +06:00
9efade9daf Update gradle-wrapper.properties 2023-03-01 18:28:55 +06:00
e4d1ff7fa9 Update CHANGELOG.md 2023-03-01 18:28:25 +06:00
e0b5f5d410 Update libs.versions.toml 2023-03-01 18:27:59 +06:00
adec37b9e5 0.15.1 2023-03-01 18:25:16 +06:00
8e042c6399 Merge pull request #158 from InsanusMokrassar/0.15.0
0.15.0
2023-02-28 12:46:37 +06:00
0564ed1d75 gitignore kotlin-js-store 2023-02-28 12:44:51 +06:00
f9d98ec7d7 remove kotlin js yarn 2023-02-28 12:44:16 +06:00
67519da545 update dependencies 2023-02-28 12:18:32 +06:00
6028ec7774 upgrade up to 0.15.0 2023-02-28 12:17:35 +06:00
e29cad7bae Update CHANGELOG.md 2023-02-23 23:45:24 +06:00
473a5a735a Update libs.versions.toml 2023-02-21 13:09:10 +06:00
0229697dd3 start 0.14.1 2023-02-21 13:08:39 +06:00
47161eb4ff Merge pull request #153 from InsanusMokrassar/0.14.0
0.14.0
2022-12-23 13:08:04 +06:00
e6024b223c update SauceNaoAPI structure 2022-12-22 15:55:32 +06:00
5f8e410531 LimitsState take comparison operator 2022-12-22 13:55:07 +06:00
7727d2400e make LimitStatus and Limits to be comparable 2022-12-22 13:26:06 +06:00
eec32de472 start 0.14.0 2022-12-22 13:19:27 +06:00
ae9a7baf50 Merge pull request #152 from InsanusMokrassar/0.13.0
0.13.0
2022-12-11 11:40:49 +06:00
bcc36dbfb7 provide opportunity to get public properties from sauce nao api 2022-12-11 11:19:08 +06:00
cf8ec46513 fill changelog and improve RequestQoutaManager API 2022-12-11 11:17:28 +06:00
9c25b08296 add opportunity to subscribe on quota changes 2022-12-11 11:13:38 +06:00
962d079f46 Update build_and_publish.yml 2022-12-11 09:17:08 +06:00
a84f31f888 Update build_and_publish.yml 2022-12-11 09:10:12 +06:00
14006652ee Add files via upload 2022-12-11 09:08:39 +06:00
29aa1271a6 Update libs.versions.toml 2022-12-11 09:04:51 +06:00
d2edd43a09 Update gradle.properties 2022-12-11 09:03:34 +06:00
c31793ab38 Merge pull request #146 from InsanusMokrassar/0.12.2
0.12.2
2022-10-02 22:27:28 +06:00
d19bb48384 Update CHANGELOG.md 2022-10-02 22:24:25 +06:00
b21c3f1ac9 Update libs.versions.toml 2022-10-02 11:16:53 +06:00
5ddc13eca0 start 0.12.2 2022-10-02 11:15:31 +06:00
8b180850da Merge pull request #141 from InsanusMokrassar/0.12.1
0.12.1
2022-08-12 00:47:12 +06:00
e65a171e65 Update CHANGELOG.md 2022-08-12 00:39:29 +06:00
31f32a7069 Update libs.versions.toml 2022-08-12 00:38:22 +06:00
5b299fb1c1 start 0.12.1 2022-08-12 00:37:47 +06:00
5038fb4240 Merge pull request #139 from InsanusMokrassar/0.12.0
0.12.0
2022-08-06 10:00:46 +06:00
96f520700f Update libs.versions.toml 2022-08-06 08:48:50 +06:00
2a4296ff7d Update gradle-wrapper.properties 2022-08-06 08:47:55 +06:00
2f34ab7ad1 start 0.12.0 2022-08-06 08:47:18 +06:00
798c4097ed Update github_release.gradle 2022-07-22 20:04:50 +06:00
c1df04fd09 Merge pull request #136 from InsanusMokrassar/0.11.1
0.11.1
2022-07-22 19:44:20 +06:00
8cc0426dc5 Update CHANGELOG.md 2022-07-18 17:08:54 +06:00
9cc929adf6 Update dependencies 2022-07-18 17:06:31 +06:00
85567956ed Update gradle.properties 2022-07-18 17:05:41 +06:00
db154f4202 Merge pull request #131 from InsanusMokrassar/0.11.0
0.11.0
2022-05-14 13:47:39 +06:00
bfdbee356a optimize imports 2022-05-14 13:44:44 +06:00
62580f6649 recreate yarn.lock 2022-05-14 13:44:21 +06:00
0b79f27b38 improvements and actualization 2022-05-14 13:43:28 +06:00
8543917da7 Update libs.versions.toml 2022-05-10 10:34:58 +06:00
be858d3e60 Update gradle.properties 2022-05-10 10:33:45 +06:00
90e090ca78 Update gradle-wrapper.properties 2022-04-01 09:38:01 +06:00
66414ff8df Merge pull request #117 from InsanusMokrassar/0.10.1
0.10.1
2022-03-17 13:45:04 +06:00
d0c143dcd6 update dependencies 2022-03-16 12:50:57 +06:00
3ee0d0de7d start 0.10.1 2022-03-16 12:49:32 +06:00
d04ee03043 Merge pull request #116 from InsanusMokrassar/0.10.0
0.10.0
2022-03-11 13:59:37 +06:00
e173bd71e0 migration onto toml and actualizing of publish scripts 2022-03-11 13:54:01 +06:00
11aac1b581 Update build_and_publish.yml 2022-03-10 01:21:59 +06:00
4319c41e44 Update gradle-wrapper.properties 2022-03-10 01:21:19 +06:00
dfd1ba807f start 0.10.0 2022-03-10 01:12:41 +06:00
e926a5422d Update build_and_publish.yml 2021-09-22 20:16:31 +06:00
d631d63490 Merge pull request #108 from InsanusMokrassar/0.9.1
0.9.1
2021-09-21 15:28:38 +06:00
60ae527dd0 small fix 2021-09-21 15:18:48 +06:00
05b8d7738f extension ResultData#urls 2021-09-21 15:10:33 +06:00
b5ceff32bd add several extensions to ResultData: authors, froms, charactersList, titles 2021-09-21 14:47:40 +06:00
64109735f8 update dependencies 2021-09-21 14:44:21 +06:00
d4f902aefd start 0.9.1 2021-09-21 14:42:52 +06:00
2de15a54a1 Merge pull request #104 from InsanusMokrassar/0.9.0
0.9.0
2021-08-31 11:47:54 +06:00
036deeb318 Update README.md 2021-08-31 11:46:59 +06:00
4cc0223fbc update dependencies 2021-08-31 11:29:46 +06:00
be292102aa start 0.9.0 2021-08-31 11:21:41 +06:00
9d473857be Merge pull request #94 from InsanusMokrassar/0.8.2
0.8.2
2021-05-26 22:37:24 +06:00
6ff3f76295 fixes of publish scripts 2021-05-26 22:36:00 +06:00
0ed1a8702b fix deprecations 2021-05-26 22:16:08 +06:00
c800a81a41 update publish scripts 2021-05-26 22:10:53 +06:00
62ceae0066 update klock 2021-05-26 22:02:16 +06:00
8619d020f6 Merge branch 'master' into 0.8.2 2021-05-26 21:58:29 +06:00
affa06905e Create build_and_publish.yml 2021-05-26 21:57:39 +06:00
cbe76b3d95 update gradle wrapper 2021-05-26 21:42:51 +06:00
9f4feeccfc update dependencies 2021-05-26 21:39:42 +06:00
037074cef6 start 0.8.2 2021-05-26 21:37:34 +06:00
dfe29296ab 0.8.1 changelog 2021-04-07 20:09:11 +06:00
ce1d185eff 0.8.1 + dependencies update 2021-04-07 20:00:20 +06:00
a78611a27c Merge pull request #75 from InsanusMokrassar/0.8.0
0.8.0
2021-03-08 00:39:19 +06:00
22 changed files with 710 additions and 200 deletions

24
.github/workflows/build_and_publish.yml vendored Normal file
View File

@@ -0,0 +1,24 @@
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: 17
- 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
run: ./gradlew publishAllPublicationsToGithubPackagesRepository --no-parallel
continue-on-error: true
env:
GITHUBPACKAGES_USER: ${{ github.actor }}
GITHUBPACKAGES_PASSWORD: ${{ secrets.GITHUB_TOKEN }}

2
.gitignore vendored
View File

@@ -1,4 +1,5 @@
.idea .idea
.kotlin
out/* out/*
*.iml *.iml
target target
@@ -8,3 +9,4 @@ settings.xml
.gradle/ .gradle/
build/ build/
out/ out/
kotlin-js-store/

View File

@@ -1,5 +1,152 @@
# SauceNaoAPI Changelog # SauceNaoAPI Changelog
## 0.19.0
* Versions:
* `Kotlin`: `2.2.20`
* `Coroutines`: `1.10.2`
* `Serialization`: `1.9.0`
* `Ktor`: `3.3.0`
* `MicroUtils`: `0.26.5`
## 0.18.0
* Versions:
* `Kotlin`: `2.1.0`
* `Serialization`: `1.7.3`
* `Coroutines`: `1.9.0`
* `Ktor`: `3.0.2`
* `MicroUtils`: `0.23.2`
* `Klock`: `5.4.0`
## 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 ## 0.8.0
* Versions updates: * Versions updates:

View File

@@ -1,6 +1,7 @@
# SauceNaoAPI # SauceNaoAPI
[ ![Download](https://api.bintray.com/packages/insanusmokrassar/SauceNaoAPI/saucenaoapi/images/download.svg) ](https://bintray.com/insanusmokrassar/SauceNaoAPI/saucenaoapi/_latestVersion) [![Maven Central](https://maven-badges.herokuapp.com/maven-central/dev.inmo/saucenaoapi/badge.svg)](https://maven-badges.herokuapp.com/maven-central/dev.inmo/saucenaoapi)
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

View File

@@ -1,20 +1,20 @@
buildscript { buildscript {
repositories { repositories {
mavenLocal()
jcenter()
mavenCentral() mavenCentral()
mavenLocal()
} }
dependencies { dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath libs.buildscript.kt.gradle
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" classpath libs.buildscript.kt.serialization
classpath "com.github.breadmoirai:github-release:$gradle_github_release_plugin_version" classpath libs.buildscript.gh.release
} }
} }
plugins { plugins {
id "org.jetbrains.kotlin.multiplatform" version "$kotlin_version" alias(libs.plugins.multiplatform)
id "org.jetbrains.kotlin.plugin.serialization" version "$kotlin_version" alias(libs.plugins.serialization)
alias(libs.plugins.nmcp.aggregation)
} }
project.version = "$library_version" project.version = "$library_version"
@@ -24,18 +24,25 @@ apply from: "publish.gradle"
apply from: "github_release.gradle" apply from: "github_release.gradle"
repositories { repositories {
mavenLocal()
jcenter()
mavenCentral() mavenCentral()
maven { url "https://kotlin.bintray.com/kotlinx" } maven { url "https://nexus.inmo.dev/repository/maven-releases/" }
mavenLocal()
} }
kotlin { kotlin {
jvm() jvm {
js(BOTH) { compilations.main {
kotlinOptions {
jvmTarget = "17"
}
}
}
js(IR) {
browser() browser()
nodejs() nodejs()
} }
linuxX64()
mingwX64()
sourceSets { sourceSets {
@@ -43,10 +50,13 @@ kotlin {
dependencies { dependencies {
implementation kotlin('stdlib') implementation kotlin('stdlib')
api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version" api libs.kt.coroutines
api "org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlin_serialisation_version" api libs.kt.serialization
api "com.soywiz.korlibs.klock:klock:$klock_version" api libs.klock
api "io.ktor:ktor-client-core:$ktor_version" api libs.ktor.client
api libs.microutils.common
api libs.microutils.ktor.common
api libs.microutils.mimetypes
} }
} }
commonTest { commonTest {
@@ -58,7 +68,7 @@ kotlin {
jvmTest { jvmTest {
dependencies { dependencies {
implementation kotlin('test-junit') implementation kotlin('test-junit')
implementation "io.ktor:ktor-client-okhttp:$ktor_version" implementation libs.ktor.client.okhttp
} }
} }
jsTest { jsTest {
@@ -68,3 +78,21 @@ kotlin {
} }
} }
} }
if ((project.hasProperty('SONATYPE_USER') || System.getenv('SONATYPE_USER') != null) && (project.hasProperty('SONATYPE_PASSWORD') || System.getenv('SONATYPE_PASSWORD') != null)) {
nmcpAggregation {
centralPortal {
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')
validationTimeout = Duration.ofHours(4)
publishingType = System.getenv('PUBLISHING_TYPE') != "" ? System.getenv('PUBLISHING_TYPE') : "USER_MANAGED"
}
publishAllProjectsProbablyBreakingProjectIsolation()
}
}
java {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}

View File

@@ -16,13 +16,13 @@ if (new File(projectDir, "secret.gradle").exists()) {
githubRelease { githubRelease {
token "${project.property('GITHUB_RELEASE_TOKEN')}" token "${project.property('GITHUB_RELEASE_TOKEN')}"
owner "InsanusMokrassar" owner = "InsanusMokrassar"
repo "${rootProject.name}" repo = "${rootProject.name}"
tagName "${project.version}" tagName = "v${project.version}"
releaseName "${project.version}" releaseName = "${project.version}"
targetCommitish "${project.version}" targetCommitish = "${project.version}"
body getCurrentVersionChangelog() body = getCurrentVersionChangelog()
} }
} }

View File

@@ -1,10 +1,3 @@
kotlin.code.style=official kotlin.code.style=official
kotlin_version=1.4.31
kotlin_coroutines_version=1.4.3
kotlin_serialisation_version=1.1.0
klock_version=2.0.6
ktor_version=1.5.2
gradle_github_release_plugin_version=2.2.12 library_version=0.19.0
library_version=0.8.0

39
gradle/libs.versions.toml Normal file
View File

@@ -0,0 +1,39 @@
[versions]
kt = "2.2.21"
kt-coroutines = "1.10.2"
kt-serialization = "1.9.0"
klock = "5.4.0"
ktor = "3.3.0"
microutils = "0.26.5"
gh-release = "2.5.2"
nmcp = "1.2.0"
[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.korge:korlibs-time", 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" }
nmcp-aggregation = { id = "com.gradleup.nmcp.aggregation", version.ref = "nmcp" }

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-6.8.3-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip

View File

@@ -1,8 +1,8 @@
apply plugin: 'maven-publish' apply plugin: 'maven-publish'
apply plugin: 'signing'
task javadocsJar(type: Jar) { task javadocsJar(type: Jar) {
classifier = 'javadoc' archiveClassifier = 'javadoc'
} }
publishing { publishing {
@@ -20,40 +20,93 @@ publishing {
} }
developers { developers {
developer {
developer { id = "InsanusMokrassar"
id = "InsanusMokrassar" name = "Ovsyannikov Alexey"
name = "Ovsyannikov Alexey" email = "ovsyannikov.alexey95@gmail.com"
email = "ovsyannikov.alexey95@gmail.com" }
}
} }
licenses { licenses {
license {
license { name = "Apache Software License 2.0"
name = "Apache Software License 2.0" url = "https://github.com/InsanusMokrassar/SauceNaoAPI/blob/master/LICENSE"
url = "https://github.com/InsanusMokrassar/SauceNaoAPI/blob/master/LICENSE" }
}
} }
} }
repositories { }
if ((project.hasProperty('SONATYPE_USER') || System.getenv('SONATYPE_USER') != null) && (project.hasProperty('SONATYPE_PASSWORD') || System.getenv('SONATYPE_PASSWORD') != null)) { repositories {
maven { if ((project.hasProperty('GITHUBPACKAGES_USER') || System.getenv('GITHUBPACKAGES_USER') != null) && (project.hasProperty('GITHUBPACKAGES_PASSWORD') || System.getenv('GITHUBPACKAGES_PASSWORD') != null)) {
name = "sonatype" maven {
url = uri("https://oss.sonatype.org/service/local/staging/deploy/maven2/") name = "GithubPackages"
credentials { url = uri("https://maven.pkg.github.com/InsanusMokrassar/SauceNaoAPI")
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') credentials {
} username = project.hasProperty('GITHUBPACKAGES_USER') ? project.property('GITHUBPACKAGES_USER') : System.getenv('GITHUBPACKAGES_USER')
password = project.hasProperty('GITHUBPACKAGES_PASSWORD') ? project.property('GITHUBPACKAGES_PASSWORD') : System.getenv('GITHUBPACKAGES_PASSWORD')
}
}
}
if ((project.hasProperty('INMONEXUS_USER') || System.getenv('INMONEXUS_USER') != null) && (project.hasProperty('INMONEXUS_PASSWORD') || System.getenv('INMONEXUS_PASSWORD') != null)) {
maven {
name = "InmoNexus"
url = uri("https://nexus.inmo.dev/repository/maven-releases/")
credentials {
username = project.hasProperty('INMONEXUS_USER') ? project.property('INMONEXUS_USER') : System.getenv('INMONEXUS_USER')
password = project.hasProperty('INMONEXUS_PASSWORD') ? project.property('INMONEXUS_PASSWORD') : System.getenv('INMONEXUS_PASSWORD')
}
}
}
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")) {
apply plugin: 'signing'
signing { signing {
useGpgCmd() useGpgCmd()
sign publishing.publications
sign publishing.publications
}
task signAll {
tasks.withType(Sign).forEach {
dependsOn(it)
}
}
// Workaround to make android sign operations depend on signing tasks
project.getTasks().withType(AbstractPublishToMaven.class).configureEach {
def signingTasks = project.getTasks().withType(Sign.class)
mustRunAfter(signingTasks)
}
// Workaround to make test tasks use sign
project.getTasks().withType(Sign.class).configureEach { signTask ->
def withoutSign = (signTask.name.startsWith("sign") ? signTask.name.minus("sign") : signTask.name)
def pubName = withoutSign.endsWith("Publication") ? withoutSign.substring(0, withoutSign.length() - "Publication".length()) : withoutSign
// These tasks only exist for native targets, hence findByName() to avoid trying to find them for other targets
// Task ':linkDebugTest<platform>' uses this output of task ':sign<platform>Publication' without declaring an explicit or implicit dependency
def debugTestTask = tasks.findByName("linkDebugTest$pubName")
if (debugTestTask != null) {
signTask.mustRunAfter(debugTestTask)
}
// Task ':compileTestKotlin<platform>' uses this output of task ':sign<platform>Publication' without declaring an explicit or implicit dependency
def testTask = tasks.findByName("compileTestKotlin$pubName")
if (testTask != null) {
signTask.mustRunAfter(testTask)
}
}
} }

View File

@@ -1 +1 @@
{"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","includeGpgSigning":true,"developers":[{"id":"InsanusMokrassar","name":"Ovsyannikov Alexey","eMail":"ovsyannikov.alexey95@gmail.com"}],"repositories":[{"name":"sonatype","url":"https://oss.sonatype.org/service/local/staging/deploy/maven2/"}]}} {"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":"InmoNexus","url":"https://nexus.inmo.dev/repository/maven-releases/"},{"name":"sonatype","url":"https://oss.sonatype.org/service/local/staging/deploy/maven2/"}],"gpgSigning":{"type":"dev.inmo.kmppscriptbuilder.core.models.GpgSigning.Optional"}}}

View File

@@ -1,22 +1,22 @@
package dev.inmo.saucenaoapi 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.TooManyRequestsException
import dev.inmo.saucenaoapi.exceptions.sauceNaoAPIException import dev.inmo.saucenaoapi.exceptions.sauceNaoAPIException
import dev.inmo.saucenaoapi.models.* import dev.inmo.saucenaoapi.models.*
import dev.inmo.saucenaoapi.utils.* import dev.inmo.saucenaoapi.utils.*
import io.ktor.client.HttpClient import io.ktor.client.HttpClient
import io.ktor.client.features.ClientRequestException import io.ktor.client.plugins.ClientRequestException
import io.ktor.client.request.* import io.ktor.client.request.*
import io.ktor.client.request.forms.MultiPartFormDataContent import io.ktor.client.request.forms.MultiPartFormDataContent
import io.ktor.client.request.forms.formData import io.ktor.client.request.forms.formData
import io.ktor.client.statement.HttpResponse import io.ktor.client.statement.bodyAsText
import io.ktor.client.statement.readText
import io.ktor.http.* import io.ktor.http.*
import io.ktor.utils.io.core.Input import io.ktor.utils.io.core.Input
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
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,6 +24,7 @@ private const val URL_FIELD = "url"
private const val FILE_FIELD = "file" private const val FILE_FIELD = "file"
private const val FILENAME_FIELD = "filename" private const val FILENAME_FIELD = "filename"
private const val DB_FIELD = "db" private const val DB_FIELD = "db"
private const val DBS_FIELD = "dbs[]"
private const val DBMASK_FIELD = "dbmask" private const val DBMASK_FIELD = "dbmask"
private const val DBMASKI_FIELD = "dbmaski" private const val DBMASKI_FIELD = "dbmaski"
private const val RESULTS_COUNT_FIELD = "numres" private const val RESULTS_COUNT_FIELD = "numres"
@@ -40,26 +41,34 @@ val defaultSauceNaoParser = Json {
data class SauceNaoAPI( data class SauceNaoAPI(
private val apiToken: String? = null, private val apiToken: String? = null,
private val outputType: dev.inmo.saucenaoapi.OutputType = dev.inmo.saucenaoapi.JsonOutputType,
private val client: HttpClient = HttpClient(), private val client: HttpClient = HttpClient(),
private val searchUrl: String = SEARCH_URL, private val searchUrl: String = SEARCH_URL,
private val scope: CoroutineScope = CoroutineScope(Dispatchers.Default), private val scope: CoroutineScope = CoroutineScope(Dispatchers.Default),
private val parser: Json = defaultSauceNaoParser private val parser: Json = defaultSauceNaoParser
) : SauceCloseable { ) : SauceCloseable {
private val requestsChannel = Channel<Pair<Continuation<SauceNaoAnswer>, HttpRequestBuilder>>(Channel.UNLIMITED) private val requestsChannel = Channel<Pair<CompletableDeferred<SauceNaoAnswer>, HttpRequestBuilder>>(Channel.UNLIMITED)
private val timeManager = TimeManager(scope) private val subscope = CoroutineScope(scope.coroutineContext + SupervisorJob(scope.coroutineContext.job)).also {
private val quotaManager = RequestQuotaManager(scope) it.coroutineContext.job.invokeOnCompletion {
requestsChannel.close(it)
}
}
private val timeManager = TimeManager(subscope)
private val quotaManager = RequestQuotaManager(subscope)
val limitsState: LimitsState val limitsState: LimitsState by quotaManager::limitsState
get() = 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 = scope.launch { private val requestsJob = subscope.launch {
for ((callback, requestBuilder) in requestsChannel) { for ((callback, requestBuilder) in requestsChannel) {
quotaManager.getQuota() quotaManager.getQuota()
launch { launch {
try { try {
val answer = makeRequest(requestBuilder) val answer = makeRequest(requestBuilder)
callback.resume(answer) callback.complete(answer)
quotaManager.updateQuota(answer.header, timeManager) quotaManager.updateQuota(answer.header, timeManager)
} catch (e: TooManyRequestsException) { } catch (e: TooManyRequestsException) {
@@ -67,7 +76,7 @@ data class SauceNaoAPI(
requestsChannel.send(callback to requestBuilder) requestsChannel.send(callback to requestBuilder)
} catch (e: Exception) { } catch (e: Exception) {
try { try {
callback.resumeWithException(e) callback.completeExceptionally(e)
} catch (e: IllegalStateException) { // may happen when already resumed and api was closed } catch (e: IllegalStateException) { // may happen when already resumed and api was closed
// do nothing // do nothing
} }
@@ -80,65 +89,203 @@ data class SauceNaoAPI(
url: String, url: String,
resultsCount: Int? = null, resultsCount: Int? = null,
minSimilarity: Float? = null minSimilarity: Float? = null
): SauceNaoAnswer? = makeRequest( ): SauceNaoAnswer = makeRequest(
url.asSauceRequestSubject, url.asSauceRequestSubject,
resultsCount = resultsCount, resultsCount = resultsCount,
minSimilarity = minSimilarity 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( suspend fun request(
mediaInput: Input, mediaInput: Input,
mimeType: ContentType, mimeType: ContentType,
resultsCount: Int? = null, resultsCount: Int? = null,
minSimilarity: Float? = null minSimilarity: Float? = null
): SauceNaoAnswer? = makeRequest( ): SauceNaoAnswer = makeRequest(
mediaInput.asSauceRequestSubject(mimeType), mediaInput.asSauceRequestSubject(mimeType),
resultsCount = resultsCount, resultsCount = resultsCount,
minSimilarity = minSimilarity 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( suspend fun requestByDb(
url: String, url: String,
db: Int, db: Int,
resultsCount: Int? = null, resultsCount: Int? = null,
minSimilarity: Float? = null minSimilarity: Float? = null
): SauceNaoAnswer? = makeRequest( ): SauceNaoAnswer = requestByDBs(
url.asSauceRequestSubject, url,
db = db, db,
resultsCount = resultsCount, null,
minSimilarity = minSimilarity resultsCount,
minSimilarity
) )
suspend fun requestByMask( @Deprecated("Renamed", ReplaceWith("requestByMasks(url, dbmask, null, resultsCount, minSimilarity)"))
suspend fun request(
url: String, url: String,
dbmask: Int, dbmask: Int,
resultsCount: Int? = null, resultsCount: Int? = null,
minSimilarity: Float? = null minSimilarity: Float? = null
): SauceNaoAnswer? = makeRequest( ): SauceNaoAnswer = requestByMasks(
url.asSauceRequestSubject, url,
dbmask = dbmask, dbmask,
resultsCount = resultsCount, null,
minSimilarity = minSimilarity resultsCount,
minSimilarity
) )
@Deprecated("Renamed", ReplaceWith("requestByMasks(url, null, dbmaski, resultsCount, minSimilarity)"))
suspend fun requestByMaskI( suspend fun requestByMaskI(
url: String, url: String,
dbmaski: Int, dbmaski: Int,
resultsCount: Int? = null, resultsCount: Int? = null,
minSimilarity: Float? = null minSimilarity: Float? = null
): SauceNaoAnswer? = makeRequest( ): SauceNaoAnswer = requestByMasks(
url.asSauceRequestSubject, url,
dbmaski = dbmaski, null,
resultsCount = resultsCount, dbmaski,
minSimilarity = minSimilarity resultsCount,
minSimilarity
) )
private suspend fun makeRequest( private suspend fun makeRequest(
builder: HttpRequestBuilder builder: HttpRequestBuilder
): SauceNaoAnswer { ): SauceNaoAnswer {
return try { return try {
val call = client.request<HttpResponse>(builder) val call = client.request(builder)
val answerText = call.readText() val answerText = call.bodyAsText()
timeManager.addTimeAndClear() timeManager.addTimeAndClear()
parser.decodeFromString( parser.decodeFromString(
SauceNaoAnswerSerializer, SauceNaoAnswerSerializer,
@@ -152,33 +299,38 @@ data class SauceNaoAPI(
private suspend fun makeRequest( private suspend fun makeRequest(
request: SauceRequestSubject, request: SauceRequestSubject,
db: Int? = null, db: Int? = null,
dbs: Array<Int>? = null,
dbmask: Int? = null, dbmask: Int? = null,
dbmaski: Int? = null, dbmaski: Int? = null,
resultsCount: Int? = null, resultsCount: Int? = null,
minSimilarity: Float? = null minSimilarity: Float? = null
): SauceNaoAnswer? { ): SauceNaoAnswer {
return suspendCoroutine<SauceNaoAnswer> { val deferred = CompletableDeferred<SauceNaoAnswer>()
requestsChannel.offer(
it to HttpRequestBuilder().apply {
url(searchUrl)
apiToken ?.also { parameter(API_TOKEN_FIELD, it) } requestsChannel.trySend(
parameter(OUTPUT_TYPE_FIELD, outputType.typeCode) deferred to HttpRequestBuilder().apply {
db ?.also { parameter(DB_FIELD, it) } url(searchUrl)
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) { apiToken ?.also { parameter(API_TOKEN_FIELD, it) }
is UrlSauceRequestSubject -> { parameter(OUTPUT_TYPE_FIELD, JsonOutputType.typeCode)
parameter(URL_FIELD, request.url) db ?.also { parameter(DB_FIELD, it) }
} dbs ?.forEach { parameter(DBS_FIELD, it) }
is InputRequestSubject -> { dbmask ?.also { parameter(DBMASK_FIELD, it) }
val mimeType = request.mimeType dbmaski ?.also { parameter(DBMASKI_FIELD, it) }
resultsCount ?.also { parameter(RESULTS_COUNT_FIELD, it) }
minSimilarity ?.also { parameter(MINIMAL_SIMILARITY_FIELD, it) }
method = HttpMethod.Post when (request) {
body = MultiPartFormDataContent(formData { is UrlSauceRequestSubject -> {
parameter(URL_FIELD, request.url)
}
is InputRequestSubject -> {
val mimeType = request.mimeType
method = HttpMethod.Post
setBody(
MultiPartFormDataContent(
formData {
appendInput( appendInput(
FILE_FIELD, FILE_FIELD,
Headers.build { Headers.build {
@@ -197,19 +349,18 @@ data class SauceNaoAPI(
}, },
block = request::input block = request::input
) )
}) }
} )
)
} }
} }
) }
} )
return deferred.await()
} }
override fun close() { override fun close() {
requestsChannel.close() subscope.cancel()
client.close()
requestsJob.cancel()
timeManager.close()
quotaManager.close()
} }
} }

View File

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

View File

@@ -29,15 +29,27 @@ 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,17 +1,17 @@
package dev.inmo.saucenaoapi.exceptions package dev.inmo.saucenaoapi.exceptions
import korlibs.time.TimeSpan
import dev.inmo.saucenaoapi.additional.LONG_TIME_RECALCULATING_MILLIS import dev.inmo.saucenaoapi.additional.LONG_TIME_RECALCULATING_MILLIS
import dev.inmo.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) { return when (response.status) {
TooManyRequests -> { TooManyRequests -> {
val answerContent = response.readText() val answerContent = response.bodyAsText()
when { when {
answerContent.contains("daily limit") -> TooManyRequestsLongException(answerContent) answerContent.contains("daily limit") -> TooManyRequestsLongException(answerContent)
else -> TooManyRequestsShortException(answerContent) else -> TooManyRequestsShortException(answerContent)

View File

@@ -8,4 +8,6 @@ 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

@@ -159,3 +159,18 @@ 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

@@ -0,0 +1,26 @@
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,26 +1,48 @@
package dev.inmo.saucenaoapi.utils package dev.inmo.saucenaoapi.utils
import korlibs.time.DateTime
import dev.inmo.saucenaoapi.additional.LONG_TIME_RECALCULATING_MILLIS import dev.inmo.saucenaoapi.additional.LONG_TIME_RECALCULATING_MILLIS
import dev.inmo.saucenaoapi.additional.SHORT_TIME_RECALCULATING_MILLIS import dev.inmo.saucenaoapi.additional.SHORT_TIME_RECALCULATING_MILLIS
import dev.inmo.saucenaoapi.exceptions.TooManyRequestsException import dev.inmo.saucenaoapi.exceptions.TooManyRequestsException
import dev.inmo.saucenaoapi.exceptions.TooManyRequestsLongException import dev.inmo.saucenaoapi.exceptions.TooManyRequestsLongException
import dev.inmo.saucenaoapi.models.Header import dev.inmo.saucenaoapi.models.Header
import dev.inmo.saucenaoapi.models.LimitsState import dev.inmo.saucenaoapi.models.LimitsState
import com.soywiz.klock.DateTime import korlibs.time.millisecondsLong
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlin.coroutines.suspendCoroutine import kotlinx.coroutines.flow.MutableStateFlow
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 var longQuota = 1 private val _longQuotaFlow = MutableStateFlow(1)
private var shortQuota = 1 private val _shortQuotaFlow = MutableStateFlow(1)
private var longMaxQuota = 1 private val _longMaxQuotaFlow = MutableStateFlow(1)
private var shortMaxQuota = 1 private val _shortMaxQuotaFlow = MutableStateFlow(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,
@@ -35,6 +57,10 @@ 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(
@@ -83,21 +109,16 @@ internal class RequestQuotaManager (
) )
suspend fun getQuota() { suspend fun getQuota() {
return suspendCoroutine { val job = Job()
lateinit var callback: suspend () -> Unit lateinit var callback: suspend () -> Unit
callback = suspend { callback = suspend {
if (longQuota > 0 && shortQuota > 0) { if (longQuota > 0 && shortQuota > 0) {
it.resumeWith(Result.success(Unit)) job.complete()
} 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

@@ -6,12 +6,13 @@ interface SauceCloseable {
fun close() fun close()
} }
fun <T> SauceCloseable.use(block: (SauceCloseable) -> T): T = try { inline 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

@@ -1,13 +1,10 @@
package dev.inmo.saucenaoapi.utils package dev.inmo.saucenaoapi.utils
import korlibs.time.DateTime
import dev.inmo.saucenaoapi.additional.LONG_TIME_RECALCULATING_MILLIS import dev.inmo.saucenaoapi.additional.LONG_TIME_RECALCULATING_MILLIS
import dev.inmo.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
@@ -38,16 +35,16 @@ private data class TimeManagerTimeAdder(
} }
private data class TimeManagerMostOldestInLongGetter( private data class TimeManagerMostOldestInLongGetter(
private val continuation: Continuation<DateTime?> private val deferred: CompletableDeferred<DateTime?>
) : TimeManagerAction { ) : TimeManagerAction {
override suspend fun makeChangeWith(times: MutableList<DateTime>) { override suspend fun makeChangeWith(times: MutableList<DateTime>) {
times.clearTooOldTimes() times.clearTooOldTimes()
continuation.resumeWith(Result.success(times.minOrNull())) deferred.complete(times.minOrNull())
} }
} }
private data class TimeManagerMostOldestInShortGetter( private data class TimeManagerMostOldestInShortGetter(
private val continuation: Continuation<DateTime?> private val deferred: CompletableDeferred<DateTime?>
) : TimeManagerAction { ) : TimeManagerAction {
override suspend fun makeChangeWith(times: MutableList<DateTime>) { override suspend fun makeChangeWith(times: MutableList<DateTime>) {
times.clearTooOldTimes() times.clearTooOldTimes()
@@ -56,19 +53,17 @@ private data class TimeManagerMostOldestInShortGetter(
val limitTime = now - SHORT_TIME_RECALCULATING_MILLIS val limitTime = now - SHORT_TIME_RECALCULATING_MILLIS
continuation.resumeWith( deferred.complete(
Result.success( times.asSequence().filter {
times.asSequence().filter { limitTime < it
limitTime < it }.minOrNull()
}.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 {
@@ -76,6 +71,10 @@ 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() {
@@ -83,21 +82,20 @@ internal class TimeManager(
} }
suspend fun getMostOldestInLongPeriod(): DateTime? { suspend fun getMostOldestInLongPeriod(): DateTime? {
return suspendCoroutine { val deferred = CompletableDeferred<DateTime?>()
actionsChannel.offer( return if (actionsChannel.trySend(TimeManagerMostOldestInLongGetter(deferred)).isSuccess) {
TimeManagerMostOldestInLongGetter(it) deferred.await()
) } else {
null
} }
} }
suspend fun getMostOldestInShortPeriod(): DateTime? { suspend fun getMostOldestInShortPeriod(): DateTime? {
return suspendCoroutine { val deferred = CompletableDeferred<DateTime?>()
actionsChannel.offer(TimeManagerMostOldestInShortGetter(it)) return if (actionsChannel.trySend(TimeManagerMostOldestInShortGetter(deferred)).isSuccess) {
deferred.await()
} else {
null
} }
} }
override fun close() {
actionsChannel.close()
timeUpdateJob.cancel()
}
} }

View File

@@ -1,30 +1,27 @@
import dev.inmo.saucenaoapi.SauceNaoAPI import dev.inmo.saucenaoapi.SauceNaoAPI
import dev.inmo.saucenaoapi.utils.useSafe import io.ktor.client.HttpClient
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 scope = CoroutineScope(Dispatchers.Default) val client = HttpClient()
val scope = CoroutineScope(Dispatchers.IO).also {
val api = SauceNaoAPI(key, scope = scope) it.coroutineContext.job.invokeOnCompletion {
api.useSafe { _ -> client.close()
println( }
when {
requestSubject.startsWith("/") -> File(requestSubject).let {
api.request(
it.inputStream().asInput(),
ContentType.parse(Files.probeContentType(it.toPath()))
)
}
else -> api.request(requestSubject)
}
)
} }
val api = SauceNaoAPI(key, client, scope = scope)
println(
when {
requestSubject.startsWith("/") -> File(requestSubject).let {
api.request(it)
}
else -> api.request(requestSubject)
}
)
scope.cancel() scope.cancel()
} }