Compare commits

..

165 Commits

Author SHA1 Message Date
renovate[bot]
75f49cff77 Update Gradle to v8.14.4 2026-01-23 23:05:12 +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
cba4b2ccdb update publish scripts 2021-03-08 00:38:32 +06:00
6aabcca9d2 update publish scripts 2021-03-08 00:37:31 +06:00
503c4226d7 update dependencies 2021-03-08 00:35:39 +06:00
d82bff2563 start 0.8.0 2021-03-08 00:32:50 +06:00
8391e20dce Merge pull request #64 from InsanusMokrassar/0.7.2
0.7.2
2021-02-01 13:12:58 +06:00
67244683dd update dependencies 2021-02-01 13:11:05 +06:00
948dede0c8 start 0.7.2 2021-02-01 13:06:12 +06:00
6f171c5f6d update readme 2020-12-23 13:59:22 +06:00
6d85ccfffa Merge pull request #54 from InsanusMokrassar/0.7.1
0.7.1
2020-12-23 13:51:01 +06:00
bce508a8c2 update ktor 2020-12-23 13:47:34 +06:00
224f0a6cc0 update versions 2020-12-16 14:29:28 +06:00
61937ed209 start 0.7.1 2020-12-16 14:28:28 +06:00
8e861020d8 Update README.md 2020-12-02 15:09:45 +06:00
c231b956b4 Merge pull request #48 from InsanusMokrassar/0.7.0
0.7.0
2020-12-02 14:46:36 +06:00
c14df69ae1 fix in changelog 2020-12-02 14:45:45 +06:00
f55744a038 update publishing 2020-12-02 14:44:25 +06:00
f27c493771 upmigration 2020-12-02 14:39:54 +06:00
7bc1e822fc update package 2020-12-02 14:32:26 +06:00
eb9456a233 start 0.7.0 2020-12-02 14:30:49 +06:00
8c8a02054d Merge pull request #47 from InsanusMokrassar/0.6.2
0.6.2
2020-12-02 14:23:36 +06:00
f52eb2ad6e update ktor 2020-12-02 14:13:42 +06:00
4fcdb6f728 update versions 2020-12-02 14:08:19 +06:00
18cdf4ffbe update build scripts 2020-12-02 14:02:15 +06:00
33d143ea12 start 0.6.2 2020-12-02 14:01:04 +06:00
e0215dcf8f update github release file 2020-10-08 17:07:37 +06:00
ca4348b5f6 update publication files 2020-10-08 16:54:11 +06:00
249cbba1f7 Merge pull request #31 from InsanusMokrassar/0.6.1
0.6.1
2020-10-08 16:37:59 +06:00
a94b7b8020 add changelog parser 2020-10-08 15:16:41 +06:00
632b534da9 update versions 2020-10-08 14:51:21 +06:00
b30af7c8f9 start 0.6.1 2020-10-08 14:40:22 +06:00
dda2ca17d3 Update README.md 2020-08-30 04:34:19 +06:00
0591f26d0c Merge pull request #21 from InsanusMokrassar/0.6.0
0.6.0
2020-08-29 19:19:36 +06:00
d24e75a452 change repository 2020-08-29 19:16:21 +06:00
6b8d06da70 hotfix 2020-08-29 19:12:47 +06:00
cbd6651c67 hotfix 2020-08-29 19:11:57 +06:00
69bfa56692 hotfix 2020-08-29 19:11:36 +06:00
270be95784 large upgrade 2020-08-29 19:09:54 +06:00
09b4b45b75 update gradle 2020-08-29 18:50:36 +06:00
b80293103e update publication info 2020-08-29 11:46:20 +06:00
fe32aaacb2 upgrade up to multiplatform project 2020-08-22 23:00:07 +06:00
7cbaac8a3e start 0.6.0 2020-08-22 21:58:35 +06:00
6ab2646a03 All known fields were added to ResultData 2020-08-15 18:06:45 +06:00
a54ac4d6a6 start 0.5.1 2020-08-15 16:52:52 +06:00
03e201ae56 Merge pull request #10 from InsanusMokrassar/renovate/junit-junit-4.x
Update dependency junit:junit to v4.13
2020-08-13 11:19:50 +06:00
315d78db11 Merge pull request #11 from InsanusMokrassar/0.5.0
0.5.0
2020-08-13 11:19:31 +06:00
b597312a0c updates fixes 2020-08-13 11:17:11 +06:00
Renovate Bot
46ad249067 Update dependency junit:junit to v4.13 2020-08-12 22:21:51 +00:00
1348abd482 Merge pull request #6 from InsanusMokrassar/renovate/configure
Configure Renovate
2020-08-13 02:12:44 +06:00
Renovate Bot
c7dc76c8df Add renovate.json 2020-08-12 20:04:50 +00:00
541134d8a7 LimitsState 2019-12-14 00:29:12 +06:00
0c2c7e9e50 fixes 2019-12-14 00:21:12 +06:00
3ecfb4298b optional token 2019-12-13 22:51:16 +06:00
8c49e60dec fixes 2019-12-13 22:45:10 +06:00
51957eb369 replace 0.4.4 changelog block 2019-12-13 00:49:04 +06:00
1bab6417ed 0.4.4 2019-12-13 00:47:52 +06:00
ea9d76fa47 hotfix for SauceNaoAnswer serializer 2019-10-12 16:28:27 +06:00
d5d3de9559 Merge pull request #5 from InsanusMokrassar/0.4.2
0.4.2
2019-10-12 14:08:22 +06:00
56 changed files with 1819 additions and 988 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,22 +1,234 @@
# SauceNaoAPI Changelog # SauceNaoAPI Changelog
## 0.4.0 ## 0.19.0
* Update libraries versions * Versions:
* Kotlin `1.3.31` -> `1.3.50` * `Kotlin`: `2.2.20`
* Coroutines `1.2.1` -> `1.3.2` * `Coroutines`: `1.10.2`
* Serialization `0.11.0` -> `0.13.0` * `Serialization`: `1.9.0`
* Joda Time `2.10.1` -> `2.10.4` * `Ktor`: `3.3.0`
* Ktor `1.1.4` -> `1.2.5` * `MicroUtils`: `0.26.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.2 ## 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
* 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
**MAIN PACKAGE WAS CHANGED: `com.github.insanusmokrassar` -> `com.insanusmokrassar`**
* All known fields were added to `ResultData`
* Versions updates:
* `Kotlin`: `1.3.72` -> `1.4.0`
* `Coroutines`: `1.3.8` -> `1.3.9`
* `Serialization`: `0.20.0` -> `1.0.0-RC`
* `Klock`: `1.11.14` -> `1.12.0`
* `Ktor`: `1.3.2` -> `1.4.0`
## 0.5.0
* Versions updates
## 0.4.4
* Uploading of file
* Updates of versions
* Now `SauceNaoAPI` do not require api key
* `SauceNaoAPI` instances now can return `limitsState` object, which will contains `LimitsState` with currently known
state of limits
## 0.4.3
Hotfix for serializer of `SauceNaoAnswer`
## 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
@@ -25,6 +237,18 @@ 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,9 +1,20 @@
# 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)
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
wrapper classes, but now you can access them via `SauceNaoAnswer#row` field. wrapper classes, but now you can access them via `SauceNaoAnswer#row` field.
## Including
### Gradle
```groovy
implementation "dev.inmo:saucenaoapi:$saucenaoapi_version"
```
## Requester ## Requester
For the requests we are using `SauceNaoAPI` object. Unfortunately, for now it For the requests we are using `SauceNaoAPI` object. Unfortunately, for now it

View File

@@ -1,41 +1,98 @@
project.version = "0.4.2"
project.group = "com.github.insanusmokrassar"
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.jfrog.bintray.gradle:gradle-bintray-plugin:$gradle_bintray_plugin_version" classpath libs.buildscript.gh.release
} }
} }
apply plugin: 'kotlin' plugins {
apply plugin: 'kotlinx-serialization' alias(libs.plugins.multiplatform)
alias(libs.plugins.serialization)
alias(libs.plugins.nmcp.aggregation)
}
project.version = "$library_version"
project.group = "dev.inmo"
apply from: "publish.gradle" apply from: "publish.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/" }
maven { url "https://dl.bintray.com/kotlin/ktor" } mavenLocal()
} }
dependencies { kotlin {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" jvm {
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version" compilations.main {
implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime:$kotlin_serialisation_runtime_version" kotlinOptions {
implementation "joda-time:joda-time:$joda_time_version" jvmTarget = "17"
implementation "io.ktor:ktor-client-core:$ktor_version" }
implementation "io.ktor:ktor-client-okhttp:$ktor_version" }
}
js(IR) {
browser()
nodejs()
}
linuxX64()
mingwX64()
// Use JUnit test framework
testImplementation 'junit:junit:4.12' sourceSets {
commonMain {
dependencies {
implementation kotlin('stdlib')
api libs.kt.coroutines
api libs.kt.serialization
api libs.klock
api libs.ktor.client
api libs.microutils.common
api libs.microutils.ktor.common
api libs.microutils.mimetypes
}
}
commonTest {
dependencies {
implementation kotlin('test-common')
implementation kotlin('test-annotations-common')
}
}
jvmTest {
dependencies {
implementation kotlin('test-junit')
implementation libs.ktor.client.okhttp
}
}
jsTest {
dependencies {
implementation kotlin('test-js')
}
}
}
}
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
} }

24
changelog_parser.sh Executable file
View File

@@ -0,0 +1,24 @@
#!/bin/bash
function parse() {
version=$1
while IFS= read -r line && [ -z "`echo $line | grep -e "^#\+ $version"`" ]
do
: # do nothing
done
while IFS= read -r line && [ -z "`echo $line | grep -e "^#\+"`" ]
do
echo "$line"
done
}
version=$1
file=$2
if [ -n "$file" ]; then
parse $version < "$file"
else
parse $version
fi

28
github_release.gradle Normal file
View File

@@ -0,0 +1,28 @@
private String getCurrentVersionChangelog() {
OutputStream changelogDataOS = new ByteArrayOutputStream()
exec {
standardOutput = changelogDataOS
commandLine 'chmod', "+x", './changelog_parser.sh'
commandLine './changelog_parser.sh', "$library_version", 'CHANGELOG.md'
}
return changelogDataOS.toString().trim()
}
if (new File(projectDir, "secret.gradle").exists()) {
apply from: './secret.gradle'
apply plugin: "com.github.breadmoirai.github-release"
githubRelease {
token "${project.property('GITHUB_RELEASE_TOKEN')}"
owner = "InsanusMokrassar"
repo = "${rootProject.name}"
tagName = "v${project.version}"
releaseName = "${project.version}"
targetCommitish = "${project.version}"
body = getCurrentVersionChangelog()
}
}

View File

@@ -1,11 +1,3 @@
kotlin.code.style=official kotlin.code.style=official
kotlin_version=1.3.50
kotlin_coroutines_version=1.3.2
kotlin_serialisation_runtime_version=0.13.0
joda_time_version=2.10.4
ktor_version=1.2.5
project_public_name=SauceNao API library_version=0.19.0
project_public_description=SauceNao API library
gradle_bintray_plugin_version=1.8.4

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

@@ -0,0 +1,39 @@
[versions]
kt = "2.2.20"
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" }

Binary file not shown.

View File

@@ -1,6 +1,7 @@
#Wed Feb 20 11:39:27 HKT 2019
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.4-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-bin.zip

317
gradlew vendored
View File

@@ -1,78 +1,129 @@
#!/usr/bin/env sh #!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
############################################################################## ##############################################################################
## #
## Gradle start up script for UN*X # Gradle start up script for POSIX generated by Gradle.
## #
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
############################################################################## ##############################################################################
# Attempt to set APP_HOME # Attempt to set APP_HOME
# Resolve links: $0 may be a link # Resolve links: $0 may be a link
PRG="$0" app_path=$0
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do # Need this for daisy-chained symlinks.
ls=`ls -ld "$PRG"` while
link=`expr "$ls" : '.*-> \(.*\)$'` APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
if expr "$link" : '/.*' > /dev/null; then [ -h "$app_path" ]
PRG="$link" do
else ls=$( ls -ld "$app_path" )
PRG=`dirname "$PRG"`"/$link" link=${ls#*' -> '}
fi case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle" # This is normally unused
APP_BASE_NAME=`basename "$0"` # shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
DEFAULT_JVM_OPTS="" APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value. # Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum" MAX_FD=maximum
warn ( ) { warn () {
echo "$*" echo "$*"
} } >&2
die ( ) { die () {
echo echo
echo "$*" echo "$*"
echo echo
exit 1 exit 1
} } >&2
# OS specific support (must be 'true' or 'false'). # OS specific support (must be 'true' or 'false').
cygwin=false cygwin=false
msys=false msys=false
darwin=false darwin=false
nonstop=false nonstop=false
case "`uname`" in case "$( uname )" in #(
CYGWIN* ) CYGWIN* ) cygwin=true ;; #(
cygwin=true Darwin* ) darwin=true ;; #(
;; MSYS* | MINGW* ) msys=true ;; #(
Darwin* ) NONSTOP* ) nonstop=true ;;
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar CLASSPATH="\\\"\\\""
# Determine the Java command to use to start the JVM. # Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables # IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java" JAVACMD=$JAVA_HOME/jre/sh/java
else else
JAVACMD="$JAVA_HOME/bin/java" JAVACMD=$JAVA_HOME/bin/java
fi fi
if [ ! -x "$JAVACMD" ] ; then if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
@@ -81,92 +132,120 @@ Please set the JAVA_HOME variable in your environment to match the
location of your Java installation." location of your Java installation."
fi fi
else else
JAVACMD="java" JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the Please set the JAVA_HOME variable in your environment to match the
location of your Java installation." location of your Java installation."
fi
fi fi
# Increase the maximum file descriptors if we can. # Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
MAX_FD_LIMIT=`ulimit -H -n` case $MAX_FD in #(
if [ $? -eq 0 ] ; then max*)
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
MAX_FD="$MAX_FD_LIMIT" # shellcheck disable=SC2039,SC3045
fi MAX_FD=$( ulimit -H -n ) ||
ulimit -n $MAX_FD warn "Could not query maximum file descriptor limit"
if [ $? -ne 0 ] ; then esac
warn "Could not set maximum file descriptor limit: $MAX_FD" case $MAX_FD in #(
fi '' | soft) :;; #(
else *)
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
fi # shellcheck disable=SC2039,SC3045
fi ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac esac
fi fi
# Escape application args # Collect all arguments for the java command, stacking in reverse order:
save ( ) { # * args from the command line
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done # * the main class name
echo " " # * -classpath
} # * -D...appname settings
APP_ARGS=$(save "$@") # * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# Collect all arguments for the java command, following the shell quoting and substitution rules # For Cygwin or MSYS, switch paths to Windows format before running java
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong JAVACMD=$( cygpath --unix "$JAVACMD" )
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
cd "$(dirname "$0")" # Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@" exec "$JAVACMD" "$@"

80
gradlew.bat vendored
View File

@@ -1,4 +1,22 @@
@if "%DEBUG%" == "" @echo off @rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ########################################################################## @rem ##########################################################################
@rem @rem
@rem Gradle startup script for Windows @rem Gradle startup script for Windows
@@ -9,25 +27,29 @@
if "%OS%"=="Windows_NT" setlocal if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0 set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=. if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0 set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME% set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS= set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe @rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1 %JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init if %ERRORLEVEL% equ 0 goto execute
echo. echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. echo location of your Java installation. 1>&2
goto fail goto fail
@@ -35,48 +57,36 @@ goto fail
set JAVA_HOME=%JAVA_HOME:"=% set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init if exist "%JAVA_EXE%" goto execute
echo. echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. echo location of your Java installation. 1>&2
goto fail goto fail
:init
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
:execute :execute
@rem Setup the command line @rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar set CLASSPATH=
@rem Execute Gradle @rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
:end :end
@rem End local scope for the variables with windows NT shell @rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd if %ERRORLEVEL% equ 0 goto mainEnd
:fail :fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code! rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 set EXIT_CODE=%ERRORLEVEL%
exit /b 1 if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd :mainEnd
if "%OS%"=="Windows_NT" endlocal if "%OS%"=="Windows_NT" endlocal

View File

@@ -1,63 +0,0 @@
apply plugin: 'maven-publish'
apply plugin: 'signing'
task sourcesJar(type: Jar) {
from sourceSets.main.allSource
classifier = 'sources'
}
task javadocJar(type: Jar) {
from javadoc
classifier = 'javadoc'
}
publishing {
publications {
maven(MavenPublication) {
from components.java
groupId "${project.group}"
artifactId "${project.name}"
version "${project.version}"
artifact sourcesJar
artifact javadocJar
pom.withXml {
asNode().children().last() + {
resolveStrategy = Closure.DELEGATE_FIRST
name "${project_public_name}"
description "${project_public_description}"
url "https://insanusmokrassar.github.io/${project.name}"
scm {
connection "scm:git:git://github.com/insanusmokrassar/${project.name}.git"
developerConnection "scm:git:[fetch=]https://github.com/insanusmokrassar/${project.name}.git[push=]ssh:git@github.com:insanusmokrassar/${project.name}.git"
url "https://github.com/insanusmokrassar/${project.name}"
}
developers {
developer {
id "InsanusMokrassar"
name "Ovsyannikov Alexey"
email "ovsyannikov.alexey95@gmail.com"
}
}
licenses {
license {
name 'The Apache Software License, Version 2.0'
url 'https://github.com/InsanusMokrassar/TelegramBotAPI/blob/master/LICENSE'
distribution 'repo'
}
}
}
}
}
}
}
signing {
useGpgCmd()
sign publishing.publications.maven
}

View File

@@ -1,33 +1,112 @@
apply plugin: 'com.jfrog.bintray'
ext { apply plugin: 'maven-publish'
projectBintrayDir = "${project.group}/".replace(".", "/") + "${project.name}/${project.version}"
task javadocsJar(type: Jar) {
archiveClassifier = 'javadoc'
} }
bintray { publishing {
user = project.hasProperty('BINTRAY_USER') ? project.property('BINTRAY_USER') : System.getenv('BINTRAY_USER') publications.all {
key = project.hasProperty('BINTRAY_KEY') ? project.property('BINTRAY_KEY') : System.getenv('BINTRAY_KEY') artifact javadocsJar
publications = ["maven"]
filesSpec { pom {
into "$projectBintrayDir" description = "SauceNao API library"
from("build/libs") { name = "SauceNao API"
include "**/*.asc" url = "https://insanusmokrassar.github.io/${project.name}"
}
from("build/publications/maven") { scm {
rename 'pom-default.xml(.*)', "${project.name}-${project.version}.pom\$1" 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/SauceNaoAPI/blob/master/LICENSE"
}
}
} }
} }
pkg { repositories {
repo = 'StandardRepository' if ((project.hasProperty('GITHUBPACKAGES_USER') || System.getenv('GITHUBPACKAGES_USER') != null) && (project.hasProperty('GITHUBPACKAGES_PASSWORD') || System.getenv('GITHUBPACKAGES_PASSWORD') != null)) {
name = "${project.name}" maven {
vcsUrl = "https://github.com/InsanusMokrassar/${project.name}" name = "GithubPackages"
licenses = ['Apache-2.0'] url = uri("https://maven.pkg.github.com/InsanusMokrassar/SauceNaoAPI")
version {
name = "${project.version}" credentials {
released = new Date() username = project.hasProperty('GITHUBPACKAGES_USER') ? project.property('GITHUBPACKAGES_USER') : System.getenv('GITHUBPACKAGES_USER')
vcsTag = name 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'
apply from: "maven.publish.gradle" signing {
useGpgCmd()
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)
}
}
}

1
publish.kpsb Normal file
View File

@@ -0,0 +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","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"}}}

5
renovate.json Normal file
View File

@@ -0,0 +1,5 @@
{
"extends": [
"config:base"
]
}

View File

@@ -1,18 +1 @@
/* rootProject.name = 'saucenaoapi'
* This settings file was generated by the Gradle 'init' task.
*
* The settings file is used to specify which projects to include in your build.
* In a single project build this file can be empty or even removed.
*
* Detailed information about configuring a multi-project build in Gradle can be found
* in the user guide at https://docs.gradle.org/3.4.1/userguide/multi_project_builds.html
*/
/*
// To declare projects as part of a multi-project build use the 'include' method
include 'shared'
include 'api'
include 'services:webservice'
*/
rootProject.name = 'SauceNaoAPI'

View File

@@ -0,0 +1,17 @@
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

@@ -0,0 +1,366 @@
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

@@ -0,0 +1,16 @@
package dev.inmo.saucenaoapi
import io.ktor.http.ContentType
import io.ktor.utils.io.core.Input
internal sealed class SauceRequestSubject
internal data class UrlSauceRequestSubject(val url: String) : SauceRequestSubject()
internal data class InputRequestSubject(val input: Input, val mimeType: ContentType) : SauceRequestSubject()
internal val String.asSauceRequestSubject
get() = UrlSauceRequestSubject(this)
internal fun Input.asSauceRequestSubject(mimeType: ContentType)
= InputRequestSubject(this, mimeType)

View File

@@ -0,0 +1,23 @@
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

@@ -0,0 +1,11 @@
package dev.inmo.saucenaoapi.additional
import korlibs.time.TimeSpan
typealias AccountType = Int
const val defaultAccountType: AccountType = 1 // "basic"
typealias UserId = Int
val SHORT_TIME_RECALCULATING_MILLIS = TimeSpan(30.0 * 1000)
val LONG_TIME_RECALCULATING_MILLIS = TimeSpan(24.0 * 60 * 60 * 1000)

View File

@@ -1,7 +1,7 @@
package com.github.insanusmokrassar.SauceNaoAPI.additional.header package dev.inmo.saucenaoapi.additional.header
import com.github.insanusmokrassar.SauceNaoAPI.additional.* import dev.inmo.saucenaoapi.additional.*
import com.github.insanusmokrassar.SauceNaoAPI.models.Header import dev.inmo.saucenaoapi.models.Header
val Header.shortLimitStatus: LimitStatus val Header.shortLimitStatus: LimitStatus
get() = LimitStatus( get() = LimitStatus(
@@ -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,6 +1,6 @@
package com.github.insanusmokrassar.SauceNaoAPI.additional.header package dev.inmo.saucenaoapi.additional.header
import com.github.insanusmokrassar.SauceNaoAPI.models.Header import dev.inmo.saucenaoapi.models.Header
data class IndexInfo( data class IndexInfo(
val id: Int, val id: Int,

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,9 @@
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

@@ -0,0 +1,34 @@
package dev.inmo.saucenaoapi.exceptions
import korlibs.time.TimeSpan
import dev.inmo.saucenaoapi.additional.LONG_TIME_RECALCULATING_MILLIS
import dev.inmo.saucenaoapi.additional.SHORT_TIME_RECALCULATING_MILLIS
import io.ktor.client.plugins.ClientRequestException
import io.ktor.client.statement.bodyAsText
import io.ktor.http.HttpStatusCode.Companion.TooManyRequests
import io.ktor.utils.io.errors.IOException
internal suspend fun ClientRequestException.sauceNaoAPIException(): Exception {
return when (response.status) {
TooManyRequests -> {
val answerContent = response.bodyAsText()
when {
answerContent.contains("daily limit") -> TooManyRequestsLongException(answerContent)
else -> TooManyRequestsShortException(answerContent)
}
}
else -> this
}
}
sealed class TooManyRequestsException(message: String, cause: Throwable? = null) : IOException(message, cause) {
abstract val answerContent: String
abstract val waitTime: TimeSpan
}
class TooManyRequestsShortException(override val answerContent: String) : TooManyRequestsException("Too many requests were sent in the short period") {
override val waitTime: TimeSpan = SHORT_TIME_RECALCULATING_MILLIS
}
class TooManyRequestsLongException(override val answerContent: String) : TooManyRequestsException("Too many requests were sent in the long period") {
override val waitTime: TimeSpan = LONG_TIME_RECALCULATING_MILLIS
}

View File

@@ -1,9 +1,13 @@
package com.github.insanusmokrassar.SauceNaoAPI.models package dev.inmo.saucenaoapi.models
import com.github.insanusmokrassar.SauceNaoAPI.utils.JsonObjectSerializer import dev.inmo.saucenaoapi.defaultSauceNaoParser
import kotlinx.serialization.* import kotlinx.serialization.*
import kotlinx.serialization.internal.StringDescriptor import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.json.Json import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.jsonObject
@Serializable @Serializable
data class Header( data class Header(
@@ -38,22 +42,25 @@ data class Header(
val userId: Int? = null val userId: Int? = null
) )
object IndexesSerializer : KSerializer<List<HeaderIndex?>> { internal object IndexesSerializer : KSerializer<List<HeaderIndex?>> {
override val descriptor: SerialDescriptor = StringDescriptor override val descriptor: SerialDescriptor = String.serializer().descriptor
override fun deserialize(decoder: Decoder): List<HeaderIndex?> { override fun deserialize(decoder: Decoder): List<HeaderIndex?> {
val json = decoder.decodeSerializableValue(JsonObjectSerializer) val json = JsonObject.serializer().deserialize(decoder)
val parsed = json.keys.mapNotNull { it.toIntOrNull() }.sorted().mapNotNull { val parsed = json.keys.mapNotNull { it.toIntOrNull() }.sorted().mapNotNull {
val jsonObject = json.getObjectOrNull(it.toString()) ?: return@mapNotNull null val jsonObject = json[it.toString()] ?.jsonObject ?: return@mapNotNull null
val index = Json.nonstrict.parse(HeaderIndex.serializer(), Json.stringify(JsonObjectSerializer, jsonObject)) val index = defaultSauceNaoParser.decodeFromString(
HeaderIndex.serializer(),
defaultSauceNaoParser.encodeToString(JsonObject.serializer(), jsonObject)
)
it to index it to index
}.toMap() }.toMap()
return Array<HeaderIndex?>(parsed.keys.max() ?: 0) { return Array<HeaderIndex?>(parsed.keys.maxOrNull() ?: 0) {
parsed[it] parsed[it]
}.toList() }.toList()
} }
override fun serialize(encoder: Encoder, obj: List<HeaderIndex?>) { override fun serialize(encoder: Encoder, value: List<HeaderIndex?>) {
TODO() TODO()
} }
} }

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,176 @@
package dev.inmo.saucenaoapi.models
import dev.inmo.saucenaoapi.utils.CommonMultivariantStringSerializer
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class ResultData(
@Serializable(CommonMultivariantStringSerializer::class)
@SerialName("danbooru_id")
val danbooruId: String? = null,
@Serializable(CommonMultivariantStringSerializer::class)
@SerialName("gelbooru_id")
val gelbooruId: String? = null,
@Serializable(CommonMultivariantStringSerializer::class)
@SerialName("drawr_id")
val drawrId: String? = null,
@Serializable(CommonMultivariantStringSerializer::class)
@SerialName("pixiv_id")
val pixivId: String? = null,
@Serializable(CommonMultivariantStringSerializer::class)
@SerialName("seiga_id")
val seigaId: String? = null,
@Serializable(CommonMultivariantStringSerializer::class)
@SerialName("yandere_id")
val yandereId: String? = null,
@Serializable(CommonMultivariantStringSerializer::class)
@SerialName("konachan_id")
val konachanId: String? = null,
@Serializable(CommonMultivariantStringSerializer::class)
@SerialName("sankaku_id")
val sankakuId: String? = null,
@Serializable(CommonMultivariantStringSerializer::class)
@SerialName("anime-pictures_id")
val animePicturesId: String? = null,
@Serializable(CommonMultivariantStringSerializer::class)
@SerialName("e621_id")
val e621Id: String? = null,
@Serializable(CommonMultivariantStringSerializer::class)
@SerialName("idol_id")
val idolId: String? = null,
@Serializable(CommonMultivariantStringSerializer::class)
@SerialName("imdb_id")
val imdbId: String? = null,
@Serializable(CommonMultivariantStringSerializer::class)
@SerialName("anidb_aid")
val anidbAId: String? = null,
@Serializable(CommonMultivariantStringSerializer::class)
@SerialName("bcy_id")
val bcyId: String? = null,
@Serializable(CommonMultivariantStringSerializer::class)
@SerialName("ddb_id")
val ddbId: String? = null,
@Serializable(CommonMultivariantStringSerializer::class)
@SerialName("nijie_id")
val nijieId: String? = null,
@Serializable(CommonMultivariantStringSerializer::class)
@SerialName("getchu_id")
val getchuId: String? = null,
@Serializable(CommonMultivariantStringSerializer::class)
@SerialName("shutterstock_id")
val shutterstockId: String? = null,
@Serializable(CommonMultivariantStringSerializer::class)
@SerialName("contributor_id")
val contributorId: String? = null,
@Serializable(CommonMultivariantStringSerializer::class)
@SerialName("est_time")
val estTime: String? = null,
@Serializable(CommonMultivariantStringSerializer::class)
@SerialName("bcy_type")
val bcyType: String? = null,
@Serializable(CommonMultivariantStringSerializer::class)
@SerialName("da_id")
val daId: String? = null,
@Serializable(CommonMultivariantStringSerializer::class)
@SerialName("pg_id")
val pgId: String? = null,
@Serializable(CommonMultivariantStringSerializer::class)
@SerialName("mal_id")
val malId: String? = null,
@Serializable(CommonMultivariantStringSerializer::class)
@SerialName("md_id")
val mdId: String? = null,
@Serializable(CommonMultivariantStringSerializer::class)
@SerialName("mu_id")
val muId: String? = null,
@Serializable(CommonMultivariantStringSerializer::class)
@SerialName("pawoo_id")
val pawooId: String? = null,
@Serializable(CommonMultivariantStringSerializer::class)
@SerialName("pawoo_user_acct")
val pawooUserAcct: String? = null,
@Serializable(CommonMultivariantStringSerializer::class)
@SerialName("pawoo_user_username")
val pawooUserUsername: String? = null,
@Serializable(CommonMultivariantStringSerializer::class)
@SerialName("pawoo_user_display_name")
val pawooUserDisplayname: String? = null,
@Serializable(CommonMultivariantStringSerializer::class)
val title: String? = null,
@Serializable(CommonMultivariantStringSerializer::class)
@SerialName("jp_title")
val titleJp: String? = null,
@Serializable(CommonMultivariantStringSerializer::class)
@SerialName("eng_title")
val titleEng: String? = null,
@Serializable(CommonMultivariantStringSerializer::class)
@SerialName("alt_titles")
val titleAlt: List<String> = emptyList(),
@Serializable(CommonMultivariantStringSerializer::class)
@SerialName("jp_name")
val nameJp: String? = null,
@Serializable(CommonMultivariantStringSerializer::class)
@SerialName("eng_name")
val nameEng: String? = null,
@Serializable(CommonMultivariantStringSerializer::class)
val creator: String? = null,
@Serializable(CommonMultivariantStringSerializer::class)
val material: String? = null,
@Serializable(CommonMultivariantStringSerializer::class)
@SerialName("member_name")
val memberName: String? = null,
@Serializable(CommonMultivariantStringSerializer::class)
@SerialName("member_id")
val memberId: String? = null,
@Serializable(CommonMultivariantStringSerializer::class)
val part: String? = null,
@Serializable(CommonMultivariantStringSerializer::class)
@SerialName("part_name")
val partName: String? = null,
@Serializable(CommonMultivariantStringSerializer::class)
val date: String? = null,
@Serializable(CommonMultivariantStringSerializer::class)
val company: String? = null,
@Serializable(CommonMultivariantStringSerializer::class)
val file: String? = null,
@Serializable(CommonMultivariantStringSerializer::class)
val year: String? = null,
@Serializable(CommonMultivariantStringSerializer::class)
@SerialName("member_link_id")
val memberLinkId: String? = null,
@Serializable(CommonMultivariantStringSerializer::class)
@SerialName("author_name")
val authorName: String? = null,
@Serializable(CommonMultivariantStringSerializer::class)
@SerialName("author_url")
val authorUrl: String? = null,
@Serializable(CommonMultivariantStringSerializer::class)
val characters: String? = null,
@Serializable(CommonMultivariantStringSerializer::class)
val source: String? = null,
@Serializable(CommonMultivariantStringSerializer::class)
val url: String? = null,
@Serializable(CommonMultivariantStringSerializer::class)
val type: String? = null,
@Serializable(CommonMultivariantStringSerializer::class)
@SerialName("created_at")
val createdAt: String? = null,
@SerialName("ext_urls")
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 com.github.insanusmokrassar.SauceNaoAPI.models package dev.inmo.saucenaoapi.models
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable

View File

@@ -0,0 +1,56 @@
package dev.inmo.saucenaoapi.models
import dev.inmo.saucenaoapi.defaultSauceNaoParser
import kotlinx.serialization.*
import kotlinx.serialization.builtins.ListSerializer
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.buildJsonObject
@Serializable
private data class TemporalSauceNaoAnswerRepresentation(
val header: Header,
val results: List<Result> = emptyList(),
)
@Serializable(SauceNaoAnswerSerializer::class)
data class SauceNaoAnswer internal constructor(
val header: Header,
val results: List<Result> = emptyList(),
val raw: JsonObject = JsonObject(emptyMap())
)
@Serializer(SauceNaoAnswer::class)
object SauceNaoAnswerSerializer : KSerializer<SauceNaoAnswer> {
private val resultsSerializer = ListSerializer(Result.serializer())
private const val headerField = "header"
private const val resultsField = "results"
private val serializer = defaultSauceNaoParser
override fun deserialize(decoder: Decoder): SauceNaoAnswer {
val raw = JsonObject.serializer().deserialize(decoder)
return serializer.decodeFromJsonElement(
TemporalSauceNaoAnswerRepresentation.serializer(),
raw
).let {
SauceNaoAnswer(
it.header,
it.results,
raw
)
}
}
override fun serialize(encoder: Encoder, value: SauceNaoAnswer) {
val resultObject = buildJsonObject {
value.raw.forEach {
put(it.key, it.value)
}
put(headerField, serializer.encodeToJsonElement(Header.serializer(), value.header))
put(resultsField, serializer.encodeToJsonElement(resultsSerializer, value.results))
}
JsonObject.serializer().serialize(encoder, resultObject)
}
}

View File

@@ -0,0 +1,18 @@
package dev.inmo.saucenaoapi.utils
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializer
import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.json.*
@Serializer(String::class)
object CommonMultivariantStringSerializer : KSerializer<String> by String.serializer() {
override fun deserialize(decoder: Decoder): String {
return when (val parsed = JsonElement.serializer().deserialize(decoder)) {
is JsonPrimitive -> parsed.content
is JsonArray -> parsed.joinToString { it.jsonPrimitive.content }
else -> error("Unexpected answer object has been received: $parsed")
}
}
}

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

@@ -0,0 +1,124 @@
package dev.inmo.saucenaoapi.utils
import korlibs.time.DateTime
import dev.inmo.saucenaoapi.additional.LONG_TIME_RECALCULATING_MILLIS
import dev.inmo.saucenaoapi.additional.SHORT_TIME_RECALCULATING_MILLIS
import dev.inmo.saucenaoapi.exceptions.TooManyRequestsException
import dev.inmo.saucenaoapi.exceptions.TooManyRequestsLongException
import dev.inmo.saucenaoapi.models.Header
import dev.inmo.saucenaoapi.models.LimitsState
import korlibs.time.millisecondsLong
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
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.min
internal class RequestQuotaManager (
scope: CoroutineScope
) {
private val _longQuotaFlow = MutableStateFlow(1)
private val _shortQuotaFlow = MutableStateFlow(1)
private val _longMaxQuotaFlow = MutableStateFlow(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
get() = LimitsState(
shortMaxQuota,
longMaxQuota,
shortQuota,
longQuota
)
private val quotaActions = Channel<suspend () -> Unit>(Channel.UNLIMITED)
private val quotaJob = scope.launch {
for (callback in quotaActions) {
callback()
}
}.also {
it.invokeOnCompletion {
quotaActions.close(it)
}
}
private suspend fun updateQuota(
newLongQuota: Int,
newShortQuota: Int,
newMaxLongQuota: Int?,
newMaxShortQuota: Int?,
timeManager: TimeManager
) {
quotaActions.send(
suspend {
longMaxQuota = newMaxLongQuota ?: longMaxQuota
shortMaxQuota = newMaxShortQuota ?: shortMaxQuota
longQuota = min(newLongQuota, longMaxQuota)
shortQuota = min(newShortQuota, shortMaxQuota)
when {
longQuota < 1 -> (timeManager.getMostOldestInLongPeriod() ?: DateTime.now()) + LONG_TIME_RECALCULATING_MILLIS
shortQuota < 1 -> (timeManager.getMostOldestInShortPeriod() ?: DateTime.now()) + SHORT_TIME_RECALCULATING_MILLIS
else -> null
} ?.also {
delay((it - DateTime.now()).millisecondsLong)
shortQuota = max(shortQuota, 1)
longQuota = max(longQuota, 1)
}
Unit
}
)
}
suspend fun updateQuota(header: Header, timeManager: TimeManager) = updateQuota(
header.longRemaining,
header.shortRemaining,
header.longLimit,
header.shortLimit,
timeManager
)
suspend fun happenTooManyRequests(timeManager: TimeManager, e: TooManyRequestsException) = updateQuota(
if (e is TooManyRequestsLongException) 0 else 1,
0,
null,
null,
timeManager
)
suspend fun getQuota() {
val job = Job()
lateinit var callback: suspend () -> Unit
callback = suspend {
if (longQuota > 0 && shortQuota > 0) {
job.complete()
} else {
quotaActions.send(callback)
}
}
quotaActions.trySend(callback)
return job.join()
}
}

View File

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

View File

@@ -1,17 +1,13 @@
package com.github.insanusmokrassar.SauceNaoAPI.utils package dev.inmo.saucenaoapi.utils
import com.github.insanusmokrassar.SauceNaoAPI.additional.LONG_TIME_RECALCULATING_MILLIS import korlibs.time.DateTime
import com.github.insanusmokrassar.SauceNaoAPI.additional.SHORT_TIME_RECALCULATING_MILLIS import dev.inmo.saucenaoapi.additional.LONG_TIME_RECALCULATING_MILLIS
import kotlinx.coroutines.CoroutineScope import dev.inmo.saucenaoapi.additional.SHORT_TIME_RECALCULATING_MILLIS
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.launch
import kotlinx.io.core.Closeable
import org.joda.time.DateTime
import kotlin.coroutines.Continuation
import kotlin.coroutines.suspendCoroutine
private fun MutableList<DateTime>.clearTooOldTimes(relatedTo: DateTime = DateTime.now()) { private fun MutableList<DateTime>.clearTooOldTimes(relatedTo: DateTime = DateTime.now()) {
val limitValue = relatedTo.minusMillis(LONG_TIME_RECALCULATING_MILLIS) val limitValue = relatedTo - LONG_TIME_RECALCULATING_MILLIS
removeAll { removeAll {
it < limitValue it < limitValue
@@ -39,37 +35,35 @@ 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.min())) 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()
val now = DateTime.now() val now = DateTime.now()
val limitTime = now.minusMillis(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()
}.min()
)
) )
} }
} }
class TimeManager( internal class TimeManager(
scope: CoroutineScope scope: CoroutineScope
) : Closeable { ) {
private val actionsChannel = Channel<TimeManagerAction>(Channel.UNLIMITED) private val actionsChannel = Channel<TimeManagerAction>(Channel.UNLIMITED)
private val timeUpdateJob = scope.launch { private val timeUpdateJob = scope.launch {
@@ -77,6 +71,10 @@ 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() {
@@ -84,21 +82,20 @@ 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

@@ -0,0 +1,27 @@
import dev.inmo.saucenaoapi.SauceNaoAPI
import io.ktor.client.HttpClient
import kotlinx.coroutines.*
import java.io.File
suspend fun main(vararg args: String) {
val (key, requestSubject) = args
val client = HttpClient()
val scope = CoroutineScope(Dispatchers.IO).also {
it.coroutineContext.job.invokeOnCompletion {
client.close()
}
}
val api = SauceNaoAPI(key, client, scope = scope)
println(
when {
requestSubject.startsWith("/") -> File(requestSubject).let {
api.request(it)
}
else -> api.request(requestSubject)
}
)
scope.cancel()
}

View File

@@ -1,14 +0,0 @@
package com.github.insanusmokrassar.SauceNaoAPI
import kotlinx.coroutines.*
suspend fun main(vararg args: String) {
val (key, requestUrl) = args
val api = SauceNaoAPI(key, scope = GlobalScope)
api.use {
println(
it.request(requestUrl)
)
}
}

View File

@@ -1,17 +0,0 @@
package com.github.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

@@ -1,165 +0,0 @@
package com.github.insanusmokrassar.SauceNaoAPI
import com.github.insanusmokrassar.SauceNaoAPI.additional.LONG_TIME_RECALCULATING_MILLIS
import com.github.insanusmokrassar.SauceNaoAPI.additional.SHORT_TIME_RECALCULATING_MILLIS
import com.github.insanusmokrassar.SauceNaoAPI.exceptions.TooManyRequestsException
import com.github.insanusmokrassar.SauceNaoAPI.exceptions.sauceNaoAPIException
import com.github.insanusmokrassar.SauceNaoAPI.models.SauceNaoAnswer
import com.github.insanusmokrassar.SauceNaoAPI.utils.*
import com.github.insanusmokrassar.SauceNaoAPI.utils.calculateSleepTime
import io.ktor.client.HttpClient
import io.ktor.client.engine.okhttp.OkHttp
import io.ktor.client.features.ClientRequestException
import io.ktor.client.request.*
import io.ktor.client.response.readText
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import kotlinx.io.core.Closeable
import kotlinx.serialization.json.Json
import org.joda.time.DateTime
import java.util.logging.Logger
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 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"
data class SauceNaoAPI(
private val apiToken: String,
private val outputType: OutputType = JsonOutputType,
private val client: HttpClient = HttpClient(OkHttp),
private val searchUrl: String = SEARCH_URL,
private val scope: CoroutineScope = CoroutineScope(Dispatchers.Default)
) : Closeable {
private val logger = Logger.getLogger("SauceNaoAPI")
private val requestsChannel = Channel<Pair<Continuation<SauceNaoAnswer>, HttpRequestBuilder>>(Channel.UNLIMITED)
private val timeManager = TimeManager(scope)
private val quotaManager = RequestQuotaManager(scope)
private val requestsJob = scope.launch {
for ((callback, requestBuilder) in requestsChannel) {
quotaManager.getQuota()
launch {
try {
val answer = makeRequest(requestBuilder)
callback.resumeWith(Result.success(answer))
quotaManager.updateQuota(answer.header, timeManager)
} catch (e: TooManyRequestsException) {
quotaManager.happenTooManyRequests(timeManager)
requestsChannel.send(callback to requestBuilder)
} catch (e: Exception) {
try {
callback.resumeWith(Result.failure(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,
resultsCount = resultsCount,
minSimilarity = minSimilarity
)
suspend fun requestByDb(
url: String,
db: Int,
resultsCount: Int? = null,
minSimilarity: Float? = null
): SauceNaoAnswer? = makeRequest(
url,
db = db,
resultsCount = resultsCount,
minSimilarity = minSimilarity
)
suspend fun requestByMask(
url: String,
dbmask: Int,
resultsCount: Int? = null,
minSimilarity: Float? = null
): SauceNaoAnswer? = makeRequest(
url,
dbmask = dbmask,
resultsCount = resultsCount,
minSimilarity = minSimilarity
)
suspend fun requestByMaskI(
url: String,
dbmaski: Int,
resultsCount: Int? = null,
minSimilarity: Float? = null
): SauceNaoAnswer? = makeRequest(
url,
dbmaski = dbmaski,
resultsCount = resultsCount,
minSimilarity = minSimilarity
)
private suspend fun makeRequest(
builder: HttpRequestBuilder
): SauceNaoAnswer {
return try {
val call = client.execute(builder)
val answerText = call.response.readText()
logger.info(answerText)
timeManager.addTimeAndClear()
Json.nonstrict.parse(
SauceNaoAnswer.serializer(),
answerText
)
} catch (e: ClientRequestException) {
throw e.sauceNaoAPIException
}
}
private suspend fun makeRequest(
url: String,
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)
parameter(URL_FIELD, url)
parameter(API_TOKEN_FIELD, apiToken)
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) }
}
)
}
}
override fun close() {
requestsChannel.close()
client.close()
requestsJob.cancel()
timeManager.close()
quotaManager.close()
}
}

View File

@@ -1,23 +0,0 @@
package com.github.insanusmokrassar.SauceNaoAPI.additional
import com.github.insanusmokrassar.SauceNaoAPI.additional.header.ResultMetaInfo
import com.github.insanusmokrassar.SauceNaoAPI.additional.header.adapted
import com.github.insanusmokrassar.SauceNaoAPI.additional.results.AdaptedResult
import com.github.insanusmokrassar.SauceNaoAPI.additional.results.adapted
import com.github.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,9 +0,0 @@
package com.github.insanusmokrassar.SauceNaoAPI.additional
typealias AccountType = Int
const val defaultAccountType: AccountType = 1 // "basic"
typealias UserId = Int
const val SHORT_TIME_RECALCULATING_MILLIS = 30 * 1000
const val LONG_TIME_RECALCULATING_MILLIS = 24 * 60 * 60 * 1000

View File

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

View File

@@ -1,13 +0,0 @@
package com.github.insanusmokrassar.SauceNaoAPI.exceptions
import io.ktor.client.features.ClientRequestException
import io.ktor.http.HttpStatusCode.Companion.TooManyRequests
import kotlinx.io.IOException
val ClientRequestException.sauceNaoAPIException: Exception
get() = when (response.status) {
TooManyRequests -> TooManyRequestsException()
else -> this
}
class TooManyRequestsException : IOException()

View File

@@ -1,37 +0,0 @@
package com.github.insanusmokrassar.SauceNaoAPI.models
import com.github.insanusmokrassar.SauceNaoAPI.utils.CommonMultivariantStringSerializer
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class ResultData(
@Serializable(CommonMultivariantStringSerializer::class)
@SerialName("danbooru_id")
val danbooruId: String? = null,
@Serializable(CommonMultivariantStringSerializer::class)
@SerialName("gelbooru_id")
val gelbooruId: String? = null,
@Serializable(CommonMultivariantStringSerializer::class)
@SerialName("drawr_id")
val drawrId: String? = null,
@Serializable(CommonMultivariantStringSerializer::class)
@SerialName("pixiv_id")
val pixivId: String? = null,
@Serializable(CommonMultivariantStringSerializer::class)
val title: String? = null,
@Serializable(CommonMultivariantStringSerializer::class)
val creator: String? = null,
@Serializable(CommonMultivariantStringSerializer::class)
val material: String? = null,
@Serializable(CommonMultivariantStringSerializer::class)
@SerialName("member_name")
val memberName: String? = null,
@Serializable(CommonMultivariantStringSerializer::class)
@SerialName("member_id")
val memberId: String? = null,
@Serializable(CommonMultivariantStringSerializer::class)
val characters: String? = null,
@SerialName("ext_urls")
val extUrls: List<String> = emptyList()
)

View File

@@ -1,40 +0,0 @@
package com.github.insanusmokrassar.SauceNaoAPI.models
import kotlinx.serialization.*
import kotlinx.serialization.internal.ArrayListSerializer
import kotlinx.serialization.json.*
@Serializable(SauceNaoAnswerSerializer::class)
data class SauceNaoAnswer(
val header: Header,
val results: List<Result> = emptyList(),
val raw: JsonObject
)
@Serializer(SauceNaoAnswer::class)
object SauceNaoAnswerSerializer : KSerializer<SauceNaoAnswer> {
private val resultsSerializer = ArrayListSerializer(Result.serializer())
private const val headerField = "header"
private const val resultsField = "results"
private val serializer = Json.nonstrict
override fun deserialize(decoder: Decoder): SauceNaoAnswer {
val raw = JsonObjectSerializer.deserialize(decoder)
val header = serializer.fromJson(Header.serializer(), raw.getObject(headerField))
val results = serializer.fromJson(resultsSerializer, raw.getArray(resultsField))
return SauceNaoAnswer(header, results, raw)
}
override fun serialize(encoder: Encoder, obj: SauceNaoAnswer) {
val resultObject = JsonObject(
obj.raw.content.let {
it + mapOf(
headerField to serializer.toJson(Header.serializer(), obj.header),
resultsField to serializer.toJson(resultsSerializer, obj.results)
)
}
)
JsonObject.serializer().serialize(encoder, resultObject)
}
}

View File

@@ -1,19 +0,0 @@
package com.github.insanusmokrassar.SauceNaoAPI.utils
import kotlinx.serialization.*
import kotlinx.serialization.internal.ArrayListSerializer
import kotlinx.serialization.internal.StringSerializer
@Serializer(String::class)
object CommonMultivariantStringSerializer : KSerializer<String> by StringSerializer {
private val stringArraySerializer = ArrayListSerializer(StringSerializer)
override fun deserialize(decoder: Decoder): String {
return try {
decoder.decodeSerializableValue(StringSerializer)
} catch (e: Exception) {
decoder.decodeSerializableValue(stringArraySerializer).joinToString()
}
}
}

View File

@@ -1,162 +0,0 @@
package com.github.insanusmokrassar.SauceNaoAPI.utils
import kotlinx.serialization.*
import kotlinx.serialization.internal.*
import kotlinx.serialization.json.*
@Serializer(forClass = JsonElement::class)
internal object JsonElementSerializer : KSerializer<JsonElement> {
override val descriptor: SerialDescriptor = object : SerialClassDescImpl("JsonElementSerializer") {
override val kind: SerialKind
get() = UnionKind.SEALED
init {
addElement("JsonElement")
}
}
override fun serialize(encoder: Encoder, obj: JsonElement) {
when (obj) {
is JsonPrimitive -> JsonPrimitiveSerializer.serialize(encoder, obj)
is JsonObject -> JsonObjectSerializer.serialize(encoder, obj)
is JsonArray -> JsonArraySerializer.serialize(encoder, obj)
}
}
override fun deserialize(decoder: Decoder): JsonElement {
val input = decoder as? JsonInput ?: error("JsonElement is deserializable only when used by Json")
return input.decodeJson()
}
}
@Serializer(forClass = JsonPrimitive::class)
internal object JsonPrimitiveSerializer : KSerializer<JsonPrimitive> {
override val descriptor: SerialDescriptor =
JsonPrimitiveDescriptor
override fun serialize(encoder: Encoder, obj: JsonPrimitive) {
return if (obj is JsonNull) {
JsonNullSerializer.serialize(encoder, JsonNull)
} else {
JsonLiteralSerializer.serialize(encoder, obj as JsonLiteral)
}
}
override fun deserialize(decoder: Decoder): JsonPrimitive {
return if (decoder.decodeNotNullMark()) JsonPrimitive(decoder.decodeString())
else JsonNullSerializer.deserialize(decoder)
}
private object JsonPrimitiveDescriptor : SerialClassDescImpl("JsonPrimitive") {
override val kind: SerialKind
get() = PrimitiveKind.STRING
override val isNullable: Boolean
get() = true
init {
JsonPrimitiveSerializer.JsonPrimitiveDescriptor.addElement("JsonPrimitive")
}
}
}
@Serializer(forClass = JsonNull::class)
internal object JsonNullSerializer : KSerializer<JsonNull> {
override val descriptor: SerialDescriptor =
JsonNullDescriptor
override fun serialize(encoder: Encoder, obj: JsonNull) {
encoder.encodeNull()
}
override fun deserialize(decoder: Decoder): JsonNull {
decoder.decodeNull()
return JsonNull
}
private object JsonNullDescriptor : SerialClassDescImpl("JsonNull") {
override val kind: SerialKind
get() = UnionKind.OBJECT
override val isNullable: Boolean
get() = true
init {
JsonNullSerializer.JsonNullDescriptor.addElement("JsonNull")
}
}
}
@Serializer(forClass = JsonLiteral::class)
internal object JsonLiteralSerializer : KSerializer<JsonLiteral> {
override val descriptor: SerialDescriptor =
JsonLiteralDescriptor
override fun serialize(encoder: Encoder, obj: JsonLiteral) {
if (obj.isString) {
return encoder.encodeString(obj.content)
}
val integer = obj.intOrNull
if (integer != null) {
return encoder.encodeInt(integer)
}
val double = obj.doubleOrNull
if (double != null) {
return encoder.encodeDouble(double)
}
val boolean = obj.booleanOrNull
if (boolean != null) {
return encoder.encodeBoolean(boolean)
}
encoder.encodeString(obj.content)
}
override fun deserialize(decoder: Decoder): JsonLiteral {
return JsonLiteral(decoder.decodeString())
}
private object JsonLiteralDescriptor : SerialClassDescImpl("JsonLiteral") {
override val kind: SerialKind
get() = PrimitiveKind.STRING
init {
JsonLiteralSerializer.JsonLiteralDescriptor.addElement("JsonLiteral")
}
}
}
@Serializer(forClass = JsonObject::class)
internal object JsonObjectSerializer : KSerializer<JsonObject> {
override val descriptor: SerialDescriptor =
NamedMapClassDescriptor("JsonObject", StringSerializer.descriptor,
JsonElementSerializer.descriptor)
override fun serialize(encoder: Encoder, obj: JsonObject) {
LinkedHashMapSerializer(StringSerializer, JsonElementSerializer).serialize(encoder, obj.content)
}
override fun deserialize(decoder: Decoder): JsonObject {
return JsonObject(LinkedHashMapSerializer(StringSerializer, JsonElementSerializer).deserialize(decoder))
}
}
@Serializer(forClass = JsonArray::class)
internal object JsonArraySerializer : KSerializer<JsonArray> {
override val descriptor: SerialDescriptor = NamedListClassDescriptor("JsonArray",
JsonElementSerializer.descriptor)
override fun serialize(encoder: Encoder, obj: JsonArray) {
ArrayListSerializer(JsonElementSerializer).serialize(encoder, obj)
}
override fun deserialize(decoder: Decoder): JsonArray {
return JsonArray(ArrayListSerializer(JsonElementSerializer).deserialize(decoder))
}
}

View File

@@ -1,93 +0,0 @@
package com.github.insanusmokrassar.SauceNaoAPI.utils
import com.github.insanusmokrassar.SauceNaoAPI.additional.LONG_TIME_RECALCULATING_MILLIS
import com.github.insanusmokrassar.SauceNaoAPI.additional.SHORT_TIME_RECALCULATING_MILLIS
import com.github.insanusmokrassar.SauceNaoAPI.models.Header
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import kotlinx.io.core.Closeable
import org.joda.time.DateTime
import kotlin.coroutines.suspendCoroutine
import kotlin.math.max
import kotlin.math.min
class RequestQuotaManager (
private val scope: CoroutineScope
) : Closeable {
private var longQuota = 1
private var shortQuota = 1
private var longMaxQuota = 1
private var shortMaxQuota = 1
private val quotaActions = Channel<suspend () -> Unit>(Channel.UNLIMITED)
private val quotaJob = scope.launch {
for (callback in quotaActions) {
callback()
}
}
private suspend fun updateQuota(
newLongQuota: Int,
newShortQuota: Int,
newMaxLongQuota: Int?,
newMaxShortQuota: Int?,
timeManager: TimeManager
) {
quotaActions.send(
suspend {
longMaxQuota = newMaxLongQuota ?: longMaxQuota
shortMaxQuota = newMaxShortQuota ?: shortMaxQuota
longQuota = min(newLongQuota, longMaxQuota)
shortQuota = min(newShortQuota, shortMaxQuota)
when {
longQuota < 1 -> (timeManager.getMostOldestInLongPeriod() ?: DateTime.now()).millis + LONG_TIME_RECALCULATING_MILLIS
shortQuota < 1 -> (timeManager.getMostOldestInShortPeriod() ?: DateTime.now()).millis + SHORT_TIME_RECALCULATING_MILLIS
else -> null
} ?.also {
delay(it - DateTime.now().millis)
shortQuota = max(shortQuota, 1)
longQuota = max(longQuota, 1)
}
Unit
}
)
}
suspend fun updateQuota(header: Header, timeManager: TimeManager) = updateQuota(
header.longRemaining,
header.shortRemaining,
header.longLimit,
header.shortLimit,
timeManager
)
suspend fun happenTooManyRequests(timeManager: TimeManager) = updateQuota(
1,
0,
null,
null,
timeManager
)
suspend fun getQuota() {
return suspendCoroutine {
lateinit var callback: suspend () -> Unit
callback = suspend {
if (longQuota > 0 && shortQuota > 0) {
it.resumeWith(Result.success(Unit))
} else {
quotaActions.send(callback)
}
}
quotaActions.offer(callback)
}
}
override fun close() {
quotaJob.cancel()
quotaActions.close()
}
}

View File

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