├── .gitignore
├── .gitmodules
├── .idea
└── codeStyles
│ ├── Project.xml
│ └── codeStyleConfig.xml
├── Dockerfile
├── LICENSE
├── README.md
├── build.gradle.kts
├── gradle.properties
├── gradle
├── libs.versions.toml
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── komf-api-models
├── build.gradle.kts
└── src
│ └── commonMain
│ └── kotlin
│ └── snd
│ └── komf
│ └── api
│ ├── CommonTypes.kt
│ ├── KomfErrorResponse.kt
│ ├── KomfPage.kt
│ ├── PatchValue.kt
│ ├── config
│ ├── KomfConfig.kt
│ └── KomfConfigUpdateRequest.kt
│ ├── job
│ ├── KomfMetadataJob.kt
│ └── KomfMetadataJobEvents.kt
│ ├── mediaserver
│ ├── KomfMediaServerConnectionResponse.kt
│ └── KomfMediaServerLibrary.kt
│ ├── metadata
│ ├── KomfIdentifyRequest.kt
│ ├── KomfMetadataJobResponse.kt
│ └── KomfMetadataSearchResult.kt
│ └── notifications
│ ├── KomfAppriseRenderResult.kt
│ ├── KomfAppriseRequest.kt
│ ├── KomfAppriseTemplates.kt
│ ├── KomfDiscordRenderResult.kt
│ ├── KomfDiscordRequest.kt
│ ├── KomfDiscordTemplates.kt
│ └── KomfNotificationContext.kt
├── komf-app
├── build.gradle.kts
└── src
│ └── main
│ ├── kotlin
│ └── snd
│ │ └── komf
│ │ └── app
│ │ ├── AppContext.kt
│ │ ├── Application.kt
│ │ ├── api
│ │ ├── ConfigRoutes.kt
│ │ ├── JobRoutes.kt
│ │ ├── MediaServerRoutes.kt
│ │ ├── MetadataRoutes.kt
│ │ ├── NotificationRoutes.kt
│ │ ├── deprecated
│ │ │ ├── DeprecatedConfigRoutes.kt
│ │ │ ├── DeprecatedConfigUpdateMapper.kt
│ │ │ ├── DeprecatedMetadataRoutes.kt
│ │ │ └── dto
│ │ │ │ ├── AppConfigDto.kt
│ │ │ │ ├── AppConfigUpdateDto.kt
│ │ │ │ └── IdentifySeriesRequest.kt
│ │ └── mappers
│ │ │ ├── AppConfigMapper.kt
│ │ │ ├── AppConfigUpdateMapper.kt
│ │ │ └── CommonTypesMapper.kt
│ │ ├── config
│ │ ├── AppConfig.kt
│ │ ├── ConfigLoader.kt
│ │ └── ConfigWriter.kt
│ │ └── module
│ │ ├── MediaServerModule.kt
│ │ ├── NotificationsModule.kt
│ │ ├── ProvidersModule.kt
│ │ └── ServerModule.kt
│ └── resources
│ ├── apprise_body.vm
│ ├── apprise_title.vm
│ ├── db
│ └── migration
│ │ └── sqlite
│ │ ├── V1__initial_migration.sql
│ │ ├── V2__matched_books.sql
│ │ ├── V3__table_indexes.sql
│ │ ├── V4__drop_provider_columns.sql
│ │ ├── V5__cleanup.sql
│ │ ├── V6__server_type_columns.sql
│ │ ├── V7__series_match.sql
│ │ ├── V8__delete_yen_press_matches.sql
│ │ └── V9__delete_kodansha_matches.sql
│ ├── description.vm
│ └── title.vm
├── komf-client
├── build.gradle.kts
└── src
│ └── commonMain
│ └── kotlin
│ └── snd
│ └── komf
│ └── client
│ ├── KomfClientFactory.kt
│ ├── KomfConfigClient.kt
│ ├── KomfErrorResponse.kt
│ ├── KomfJobClient.kt
│ ├── KomfMediaServerClient.kt
│ ├── KomfMetadataClient.kt
│ └── KomfNotificationClient.kt
├── komf-core
├── build.gradle.kts
└── src
│ ├── commonMain
│ └── kotlin
│ │ └── snd
│ │ └── komf
│ │ ├── comicinfo
│ │ ├── ComicInfo.kt
│ │ └── ComicInfoWriter.kt
│ │ ├── ktor
│ │ ├── RateLimiter.kt
│ │ ├── RateLimiterPlugin.kt
│ │ └── UserAgent.kt
│ │ ├── model
│ │ ├── Author.kt
│ │ ├── BookMetadata.kt
│ │ ├── Image.kt
│ │ ├── MatchQuery.kt
│ │ ├── MatchType.kt
│ │ ├── MediaType.kt
│ │ ├── SeriesMetadata.kt
│ │ ├── SeriesSearchResult.kt
│ │ ├── UpdateMode.kt
│ │ └── WebLink.kt
│ │ ├── providers
│ │ ├── CoreProviders.kt
│ │ ├── MetadataConfigApplier.kt
│ │ ├── MetadataProvider.kt
│ │ ├── MetadataProvidersConfig.kt
│ │ ├── ProviderFactory.kt
│ │ ├── anilist
│ │ │ ├── AniListClient.kt
│ │ │ ├── AniListMetadataMapper.kt
│ │ │ ├── AniListMetadataProvider.kt
│ │ │ └── model
│ │ │ │ ├── AniListMedia.kt
│ │ │ │ ├── AniListQuery.kt
│ │ │ │ └── AniListResponse.kt
│ │ ├── bangumi
│ │ │ ├── BangumiClient.kt
│ │ │ ├── BangumiMetadataMapper.kt
│ │ │ ├── BangumiMetadataProvider.kt
│ │ │ └── model
│ │ │ │ ├── BangumiSubject.kt
│ │ │ │ └── SubjectSearch.kt
│ │ ├── bookwalker
│ │ │ ├── BookWalkerClient.kt
│ │ │ ├── BookWalkerMapper.kt
│ │ │ ├── BookWalkerMetadataProvider.kt
│ │ │ ├── BookWalkerParser.kt
│ │ │ └── model
│ │ │ │ ├── BookWalkerBook.kt
│ │ │ │ ├── BookWalkerBookListPage.kt
│ │ │ │ ├── BookWalkerCategory.kt
│ │ │ │ └── BookWalkerSearchResult.kt
│ │ ├── comicvine
│ │ │ ├── ComicVineClient.kt
│ │ │ ├── ComicVineMetadataMapper.kt
│ │ │ ├── ComicVineMetadataProvider.kt
│ │ │ ├── ComicVineRateLimiter.kt
│ │ │ └── model
│ │ │ │ ├── ComicVineCredit.kt
│ │ │ │ ├── ComicVineImage.kt
│ │ │ │ ├── ComicVineIssue.kt
│ │ │ │ ├── ComicVineSearchResult.kt
│ │ │ │ ├── ComicVineStoryArc.kt
│ │ │ │ └── ComicVineVolume.kt
│ │ ├── hentag
│ │ │ ├── HentagBook.kt
│ │ │ ├── HentagClient.kt
│ │ │ ├── HentagMetadataMapper.kt
│ │ │ └── HentagMetadataProvider.kt
│ │ ├── kodansha
│ │ │ ├── KodanshaClient.kt
│ │ │ ├── KodanshaMetadataMapper.kt
│ │ │ ├── KodanshaMetadataProvider.kt
│ │ │ └── model
│ │ │ │ ├── KodanshaBook.kt
│ │ │ │ ├── KodanshaCreator.kt
│ │ │ │ ├── KodanshaResponse.kt
│ │ │ │ ├── KodanshaSearchResult.kt
│ │ │ │ ├── KodanshaSeries.kt
│ │ │ │ └── KodanshaThumbnail.kt
│ │ ├── mal
│ │ │ ├── MalClient.kt
│ │ │ ├── MalMetadataMapper.kt
│ │ │ ├── MalMetadataProvider.kt
│ │ │ └── model
│ │ │ │ ├── MalSearchResults.kt
│ │ │ │ └── MalSeries.kt
│ │ ├── mangabaka
│ │ │ ├── MangaBakaClient.kt
│ │ │ ├── MangaBakaMetadataMapper.kt
│ │ │ ├── MangaBakaMetadataProvider.kt
│ │ │ └── model
│ │ │ │ ├── MangaBakaSearchResponse.kt
│ │ │ │ └── MangaBakaSeries.kt
│ │ ├── mangadex
│ │ │ ├── MangaDexClient.kt
│ │ │ ├── MangaDexMetadataMapper.kt
│ │ │ ├── MangaDexMetadataProvider.kt
│ │ │ └── model
│ │ │ │ ├── MangaDexLink.kt
│ │ │ │ ├── MangaDexManga.kt
│ │ │ │ └── MangaDexResponse.kt
│ │ ├── mangaupdates
│ │ │ ├── MangaUpdatesClient.kt
│ │ │ ├── MangaUpdatesMetadataMapper.kt
│ │ │ ├── MangaUpdatesMetadataProvider.kt
│ │ │ └── model
│ │ │ │ ├── MangaUpdatesGenre.kt
│ │ │ │ ├── MangaUpdatesImage.kt
│ │ │ │ ├── MangaUpdatesSearchRequest.kt
│ │ │ │ ├── MangaUpdatesSearchResult.kt
│ │ │ │ ├── MangaUpdatesSeries.kt
│ │ │ │ ├── SearchResultPage.kt
│ │ │ │ └── SeriesType.kt
│ │ ├── nautiljon
│ │ │ ├── NautiljonClient.kt
│ │ │ ├── NautiljonMetadataProvider.kt
│ │ │ ├── NautiljonParser.kt
│ │ │ ├── NautiljonSeriesMetadataMapper.kt
│ │ │ └── model
│ │ │ │ ├── NautiljonSeries.kt
│ │ │ │ ├── NautiljonVolume.kt
│ │ │ │ └── SearchResult.kt
│ │ ├── viz
│ │ │ ├── VizClient.kt
│ │ │ ├── VizMetadataMapper.kt
│ │ │ ├── VizMetadataProvider.kt
│ │ │ ├── VizParser.kt
│ │ │ └── model
│ │ │ │ ├── AgeRating.kt
│ │ │ │ ├── VizAllBooksId.kt
│ │ │ │ ├── VizBook.kt
│ │ │ │ ├── VizBookReleaseType.kt
│ │ │ │ └── VizSeriesBook.kt
│ │ └── yenpress
│ │ │ ├── YenPressClient.kt
│ │ │ ├── YenPressMetadataMapper.kt
│ │ │ ├── YenPressMetadataProvider.kt
│ │ │ ├── YenPressParser.kt
│ │ │ └── model
│ │ │ ├── YenPressBook.kt
│ │ │ ├── YenPressSearchResult.kt
│ │ │ └── YenPressSeriesId.kt
│ │ └── util
│ │ ├── BookNameParser.kt
│ │ ├── ImageHash.kt
│ │ ├── NameSimilarityMatcher.kt
│ │ ├── OsPlatform.kt
│ │ ├── SimpleNaturalComparator.kt
│ │ └── StringUtils.kt
│ └── jvmMain
│ └── kotlin
│ └── snd
│ └── komf
│ └── util
│ └── ImageHash.jvm.kt
├── komf-mediaserver
├── build.gradle.kts
└── src
│ ├── androidMain
│ └── kotlin
│ │ └── snd
│ │ └── komf
│ │ └── mediaserver
│ │ └── repository
│ │ └── Database.android.kt
│ ├── commonMain
│ ├── kotlin
│ │ └── snd
│ │ │ └── komf
│ │ │ └── mediaserver
│ │ │ ├── MediaServerClient.kt
│ │ │ ├── MediaServerEventListener.kt
│ │ │ ├── MetadataServiceProvider.kt
│ │ │ ├── jobs
│ │ │ ├── KomfJobTracker.kt
│ │ │ ├── KomfJobsRepository.kt
│ │ │ └── MetadataJob.kt
│ │ │ ├── kavita
│ │ │ ├── JwtConsumer.kt
│ │ │ ├── KavitaAuthClient.kt
│ │ │ ├── KavitaClient.kt
│ │ │ ├── KavitaMediaServerClientAdapter.kt
│ │ │ ├── KavitaResourceNotFoundException.kt
│ │ │ ├── KavitaTokenProvider.kt
│ │ │ └── model
│ │ │ │ ├── KavitaAgeRating.kt
│ │ │ │ ├── KavitaAuthor.kt
│ │ │ │ ├── KavitaChapter.kt
│ │ │ │ ├── KavitaLibrary.kt
│ │ │ │ ├── KavitaSeries.kt
│ │ │ │ ├── KavitaVolume.kt
│ │ │ │ ├── events
│ │ │ │ ├── CoverUpdateEvent.kt
│ │ │ │ ├── KavitaEvent.kt
│ │ │ │ ├── NotificationProgressEvent.kt
│ │ │ │ └── SeriesRemovedEvent.kt
│ │ │ │ └── request
│ │ │ │ ├── KavitaChapterMetadataUpdateRequest.kt
│ │ │ │ ├── KavitaCoverUploadRequest.kt
│ │ │ │ ├── KavitaSeriesMetadataUpdateRequest.kt
│ │ │ │ └── KavitaSeriesUpdateRequest.kt
│ │ │ ├── komga
│ │ │ ├── KomgaEventHandler.kt
│ │ │ └── KomgaMediaServerClientAdapter.kt
│ │ │ ├── metadata
│ │ │ ├── MetadataEventHandler.kt
│ │ │ ├── MetadataMapper.kt
│ │ │ ├── MetadataMerger.kt
│ │ │ ├── MetadataPostProcessor.kt
│ │ │ ├── MetadataService.kt
│ │ │ ├── MetadataUpdater.kt
│ │ │ └── repository
│ │ │ │ ├── BookThumbnailsRepository.kt
│ │ │ │ ├── SeriesMatchRepository.kt
│ │ │ │ └── SeriesThumbnailsRepository.kt
│ │ │ ├── model
│ │ │ ├── MediaServer.kt
│ │ │ ├── MediaServerAlternativeTitle.kt
│ │ │ ├── MediaServerAuthor.kt
│ │ │ ├── MediaServerBook.kt
│ │ │ ├── MediaServerBookMetadata.kt
│ │ │ ├── MediaServerBookMetadataUpdate.kt
│ │ │ ├── MediaServerBookThumbnail.kt
│ │ │ ├── MediaServerLibrary.kt
│ │ │ ├── MediaServerSeries.kt
│ │ │ ├── MediaServerSeriesMetadata.kt
│ │ │ ├── MediaServerSeriesMetadataUpdate.kt
│ │ │ ├── MediaServerSeriesSearch.kt
│ │ │ ├── MediaServerSeriesThumbnail.kt
│ │ │ ├── Page.kt
│ │ │ └── SeriesAndBookMetadata.kt
│ │ │ └── repository
│ │ │ └── RepositoryModule.kt
│ └── sqldelight
│ │ ├── migrations
│ │ └── 1.sqm
│ │ └── snd
│ │ └── komf
│ │ └── mediaserver
│ │ └── repository
│ │ ├── BookThumbnail.sq
│ │ ├── KomfJobRecord.sq
│ │ ├── SeriesMatch.sq
│ │ └── SeriesThumbnail.sq
│ └── jvmMain
│ └── kotlin
│ └── snd
│ └── komf
│ └── mediaserver
│ ├── kavita
│ ├── JvmJwtConsumer.kt
│ └── KavitaEventHandler.kt
│ └── repository
│ └── Database.jvm.kt
├── komf-notifications
├── build.gradle.kts
└── src
│ └── commonMain
│ └── kotlin
│ └── snd
│ └── komf
│ └── notifications
│ ├── NotificationsEventHandler.kt
│ ├── VelocityTemplates.kt
│ ├── apprise
│ ├── AppriseCliService.kt
│ ├── AppriseConfig.kt
│ └── AppriseVelocityTemplates.kt
│ └── discord
│ ├── DiscordConfig.kt
│ ├── DiscordVelocityTemplates.kt
│ ├── DiscordWebhookService.kt
│ └── model
│ ├── NotificationContext.kt
│ ├── Webhook.kt
│ └── WebhookExecuteRequest.kt
└── settings.gradle.kts
/.gitignore:
--------------------------------------------------------------------------------
1 | .gradle
2 | !gradle/wrapper/gradle-wrapper.jar
3 | build/
4 | **/build/
5 | !src/**/build/
6 |
7 | ### intellij idea ###
8 | .idea/*
9 | !/.idea/codeStyles/
10 | !/.idea/runConfigurations/
11 | .idea/modules.xml
12 | .idea/jarrepositories.xml
13 | .idea/compiler.xml
14 | .idea/libraries/
15 | *.iws
16 | *.iml
17 | *.ipr
18 | out/
19 | !**/src/main/**/out/
20 | !**/src/test/**/out/
21 |
22 | ### eclipse ###
23 | .apt_generated
24 | .classpath
25 | .factorypath
26 | .project
27 | .settings
28 | .springbeans
29 | .sts4-cache
30 | bin/
31 | !**/src/main/**/bin/
32 | !**/src/test/**/bin/
33 |
34 | ### netbeans ###
35 | /nbproject/private/
36 | /nbbuild/
37 | /dist/
38 | /nbdist/
39 | /.nb-gradle/
40 |
41 | ### vs code ###
42 | .vscode/
43 |
44 | ### mac os ###
45 | .ds_store
46 |
47 | database.sqlite
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "Komelia"]
2 | path = Komelia
3 | url = https://github.com/Snd-R/Komelia
4 |
--------------------------------------------------------------------------------
/.idea/codeStyles/Project.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM eclipse-temurin:21-jre AS base-amd64
2 |
3 | FROM eclipse-temurin:21-jre AS base-arm64
4 |
5 | FROM eclipse-temurin:17-jre AS base-arm
6 |
7 | FROM base-${TARGETARCH} AS build-final
8 |
9 | RUN apt-get update && apt-get install -y pipx \
10 | && rm -rf /var/lib/apt/lists/*
11 |
12 | RUN pipx install --include-deps pipx \
13 | && /root/.local/bin/pipx install --global --include-deps apprise
14 |
15 | WORKDIR /app
16 | COPY komf-app/build/libs/komf-app-1.0.0-SNAPSHOT-all.jar ./
17 | ENV LC_ALL=en_US.UTF-8
18 | ENV KOMF_CONFIG_DIR="/config"
19 | ENTRYPOINT ["java","-jar", "komf-app-1.0.0-SNAPSHOT-all.jar"]
20 | EXPOSE 8085
21 |
22 | LABEL org.opencontainers.image.url=https://github.com/Snd-R/komf org.opencontainers.image.source=https://github.com/Snd-R/komf
23 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Snd-R
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | // this is necessary to avoid the plugins to be loaded multiple times
3 | // in each subproject's classloader
4 | alias(libs.plugins.androidLibrary) apply false
5 | alias(libs.plugins.kotlinAtomicfu) apply false
6 | alias(libs.plugins.kotlinAndroid) apply false
7 | alias(libs.plugins.kotlinJvm) apply false
8 | alias(libs.plugins.kotlinMultiplatform) apply false
9 | alias(libs.plugins.mavenPublish) apply false
10 | }
11 |
12 | tasks.wrapper {
13 | gradleVersion = "8.9"
14 | distributionType = Wrapper.DistributionType.ALL
15 | }
16 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | kotlin.code.style=official
2 |
3 | #Gradle
4 | org.gradle.jvmargs=-Dkotlin.daemon.jvm.options\="-Xmx4096M"
5 |
6 | #Android
7 | android.nonTransitiveRClass=true
8 | android.useAndroidX=true
9 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Snd-R/komf/60bb248ccd2297412bac415c650878b043ce4b82/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip
4 | networkTimeout=10000
5 | validateDistributionUrl=true
6 | zipStoreBase=GRADLE_USER_HOME
7 | zipStorePath=wrapper/dists
8 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 | @rem SPDX-License-Identifier: Apache-2.0
17 | @rem
18 |
19 | @if "%DEBUG%"=="" @echo off
20 | @rem ##########################################################################
21 | @rem
22 | @rem Gradle startup script for Windows
23 | @rem
24 | @rem ##########################################################################
25 |
26 | @rem Set local scope for the variables with windows NT shell
27 | if "%OS%"=="Windows_NT" setlocal
28 |
29 | set DIRNAME=%~dp0
30 | if "%DIRNAME%"=="" set DIRNAME=.
31 | @rem This is normally unused
32 | set APP_BASE_NAME=%~n0
33 | set APP_HOME=%DIRNAME%
34 |
35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
37 |
38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
40 |
41 | @rem Find java.exe
42 | if defined JAVA_HOME goto findJavaFromJavaHome
43 |
44 | set JAVA_EXE=java.exe
45 | %JAVA_EXE% -version >NUL 2>&1
46 | if %ERRORLEVEL% equ 0 goto execute
47 |
48 | echo. 1>&2
49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
50 | echo. 1>&2
51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
52 | echo location of your Java installation. 1>&2
53 |
54 | goto fail
55 |
56 | :findJavaFromJavaHome
57 | set JAVA_HOME=%JAVA_HOME:"=%
58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
59 |
60 | if exist "%JAVA_EXE%" goto execute
61 |
62 | echo. 1>&2
63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
64 | echo. 1>&2
65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
66 | echo location of your Java installation. 1>&2
67 |
68 | goto fail
69 |
70 | :execute
71 | @rem Setup the command line
72 |
73 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
74 |
75 |
76 | @rem Execute Gradle
77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
78 |
79 | :end
80 | @rem End local scope for the variables with windows NT shell
81 | if %ERRORLEVEL% equ 0 goto mainEnd
82 |
83 | :fail
84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
85 | rem the _cmd.exe /c_ return code!
86 | set EXIT_CODE=%ERRORLEVEL%
87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1
88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
89 | exit /b %EXIT_CODE%
90 |
91 | :mainEnd
92 | if "%OS%"=="Windows_NT" endlocal
93 |
94 | :omega
95 |
--------------------------------------------------------------------------------
/komf-api-models/build.gradle.kts:
--------------------------------------------------------------------------------
1 | @file:OptIn(ExperimentalKotlinGradlePluginApi::class)
2 |
3 | import com.vanniktech.maven.publish.SonatypeHost
4 | import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
5 | import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
6 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget
7 |
8 | plugins {
9 | alias(libs.plugins.androidLibrary)
10 | alias(libs.plugins.kotlinMultiplatform)
11 | alias(libs.plugins.kotlinSerialization)
12 | alias(libs.plugins.mavenPublish)
13 | signing
14 | }
15 |
16 | group = "io.github.snd-r"
17 | version = libs.versions.app.version.get()
18 |
19 | kotlin {
20 | jvmToolchain(17)
21 | androidTarget {
22 | compilerOptions { jvmTarget.set(JvmTarget.JVM_17) }
23 | publishLibraryVariants("release")
24 | }
25 | jvm { compilerOptions { jvmTarget.set(JvmTarget.JVM_17) } }
26 | @OptIn(ExperimentalWasmDsl::class)
27 | wasmJs {
28 | moduleName = "komf-api-models"
29 | browser()
30 | }
31 |
32 | sourceSets {
33 | commonMain.dependencies {
34 | implementation(libs.kotlinx.serialization.json)
35 | implementation(libs.kotlinx.datetime)
36 | }
37 | }
38 |
39 | }
40 | android {
41 | namespace = "snd.komf"
42 | compileSdk = 35
43 |
44 | defaultConfig {
45 | minSdk = 26
46 | }
47 | compileOptions {
48 | sourceCompatibility = JavaVersion.VERSION_17
49 | targetCompatibility = JavaVersion.VERSION_17
50 | }
51 |
52 | }
53 |
54 | mavenPublishing {
55 | publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL, automaticRelease = false)
56 | coordinates("io.github.snd-r.komf", "api-models", libs.versions.app.version.get())
57 | signAllPublications()
58 |
59 | pom {
60 | name.set("Komf API models")
61 | description.set("Komf API models")
62 | url.set("https://github.com/Snd-R/komf")
63 | licenses {
64 | license {
65 | name.set("MIT License")
66 | url.set("https://github.com/Snd-R/komf/blob/master/LICENSE")
67 | distribution.set("repo")
68 | }
69 | }
70 | developers {
71 | developer {
72 | id.set("Snd-R")
73 | name.set("Snd-R")
74 | url.set("https://github.com/Snd-R")
75 | }
76 | }
77 | scm {
78 | url.set("https://github.com/Snd-R/komf")
79 | connection.set("scm:git:git://github.com/Snd-R/komf.git")
80 | developerConnection.set("scm:git:ssh://git@github.com/Snd-R/komf.git")
81 | }
82 | }
83 | }
84 | signing {
85 | useGpgCmd()
86 | }
87 |
--------------------------------------------------------------------------------
/komf-api-models/src/commonMain/kotlin/snd/komf/api/CommonTypes.kt:
--------------------------------------------------------------------------------
1 | package snd.komf.api
2 |
3 | import kotlinx.serialization.KSerializer
4 | import kotlinx.serialization.Serializable
5 | import kotlinx.serialization.descriptors.PrimitiveKind
6 | import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
7 | import kotlinx.serialization.descriptors.nullable
8 | import kotlinx.serialization.encoding.Decoder
9 | import kotlinx.serialization.encoding.Encoder
10 | import kotlin.jvm.JvmInline
11 |
12 | enum class KomfAuthorRole {
13 | WRITER,
14 | PENCILLER,
15 | INKER,
16 | COLORIST,
17 | LETTERER,
18 | COVER,
19 | EDITOR,
20 | TRANSLATOR
21 | }
22 |
23 | enum class KomfMediaType {
24 | MANGA,
25 | NOVEL,
26 | COMIC,
27 | }
28 |
29 | enum class KomfNameMatchingMode {
30 | EXACT,
31 | CLOSEST_MATCH,
32 | }
33 |
34 | enum class KomfReadingDirection {
35 | LEFT_TO_RIGHT,
36 | RIGHT_TO_LEFT,
37 | VERTICAL,
38 | WEBTOON
39 | }
40 |
41 | enum class KomfUpdateMode {
42 | API,
43 | COMIC_INFO,
44 | }
45 |
46 | enum class MediaServer {
47 | KOMGA,
48 | KAVITA
49 | }
50 |
51 |
52 | @Serializable(with = KomfProvidersSerializer::class)
53 | sealed interface KomfProviders
54 | enum class KomfCoreProviders : KomfProviders {
55 | ANILIST,
56 | BANGUMI,
57 | BOOK_WALKER,
58 | COMIC_VINE,
59 | HENTAG,
60 | KODANSHA,
61 | MAL,
62 | MANGA_BAKA,
63 | MANGA_UPDATES,
64 | MANGADEX,
65 | NAUTILJON,
66 | YEN_PRESS,
67 | VIZ,
68 | }
69 |
70 | data class UnknownKomfProvider(val name: String) : KomfProviders
71 |
72 | class KomfProvidersSerializer : KSerializer {
73 | override val descriptor = PrimitiveSerialDescriptor("KomfProviders", PrimitiveKind.STRING).nullable
74 |
75 | override fun serialize(encoder: Encoder, value: KomfProviders) {
76 | when (value) {
77 | is KomfCoreProviders -> encoder.encodeString(value.name)
78 | is UnknownKomfProvider -> encoder.encodeString(value.name)
79 | }
80 | }
81 |
82 | override fun deserialize(decoder: Decoder): KomfProviders {
83 | val name = decoder.decodeString()
84 | return runCatching { KomfCoreProviders.valueOf(name) }
85 | .getOrElse { UnknownKomfProvider(name) }
86 | }
87 | }
88 |
89 | enum class MangaDexLink {
90 | MANGA_DEX,
91 | ANILIST,
92 | ANIME_PLANET,
93 | BOOKWALKER_JP,
94 | MANGA_UPDATES,
95 | NOVEL_UPDATES,
96 | KITSU,
97 | AMAZON,
98 | EBOOK_JAPAN,
99 | MY_ANIME_LIST,
100 | CD_JAPAN,
101 | RAW,
102 | ENGLISH_TL,
103 | }
104 |
105 |
106 | @JvmInline
107 | @Serializable
108 | value class KomfServerSeriesId(val value: String) {
109 | override fun toString() = value
110 | }
111 |
112 | @JvmInline
113 | @Serializable
114 | value class KomfServerLibraryId(val value: String) {
115 | override fun toString() = value
116 | }
117 |
118 | @JvmInline
119 | @Serializable
120 | value class KomfProviderSeriesId(val value: String) {
121 | override fun toString() = value
122 | }
123 |
--------------------------------------------------------------------------------
/komf-api-models/src/commonMain/kotlin/snd/komf/api/KomfErrorResponse.kt:
--------------------------------------------------------------------------------
1 | package snd.komf.api
2 |
3 | import kotlinx.serialization.Serializable
4 |
5 | @Serializable
6 | data class KomfErrorResponse(val message: String)
7 |
8 |
--------------------------------------------------------------------------------
/komf-api-models/src/commonMain/kotlin/snd/komf/api/KomfPage.kt:
--------------------------------------------------------------------------------
1 | package snd.komf.api
2 |
3 | import kotlinx.serialization.Serializable
4 |
5 | @Serializable
6 | data class KomfPage(
7 | val content: T,
8 | val totalPages: Int,
9 | val currentPage: Int,
10 | )
--------------------------------------------------------------------------------
/komf-api-models/src/commonMain/kotlin/snd/komf/api/PatchValue.kt:
--------------------------------------------------------------------------------
1 | package snd.komf.api
2 |
3 | import kotlinx.serialization.ExperimentalSerializationApi
4 | import kotlinx.serialization.KSerializer
5 | import kotlinx.serialization.Serializable
6 | import kotlinx.serialization.SerializationException
7 | import kotlinx.serialization.descriptors.SerialDescriptor
8 | import kotlinx.serialization.encoding.Decoder
9 | import kotlinx.serialization.encoding.Encoder
10 |
11 | @Serializable(PatchValueSerializer::class)
12 | sealed class PatchValue {
13 | data object Unset : PatchValue()
14 | data object None : PatchValue()
15 | class Some(val value: T) : PatchValue()
16 |
17 | fun patch(original: T?, patch: T?): PatchValue {
18 | return when {
19 | original == patch -> Unset
20 | patch == null -> None
21 | else -> Some(patch)
22 | }
23 | }
24 |
25 | fun getOrNull(): T? = when (this) {
26 | None, Unset -> null
27 | is Some -> value
28 | }
29 | }
30 |
31 | class PatchValueSerializer(
32 | private val valueSerializer: KSerializer
33 | ) : KSerializer> {
34 | override val descriptor: SerialDescriptor = valueSerializer.descriptor
35 |
36 | @OptIn(ExperimentalSerializationApi::class)
37 | override fun deserialize(decoder: Decoder): PatchValue {
38 | return when (val value = decoder.decodeNullableSerializableValue(valueSerializer)) {
39 | null -> PatchValue.None
40 | else -> PatchValue.Some(value)
41 | }
42 | }
43 |
44 | @OptIn(ExperimentalSerializationApi::class)
45 | override fun serialize(encoder: Encoder, value: PatchValue) {
46 | when (value) {
47 | PatchValue.None -> encoder.encodeNull()
48 | is PatchValue.Some -> valueSerializer.serialize(encoder, value.value)
49 | PatchValue.Unset -> throw SerializationException("Value is unset. Make sure that property has default unset value")
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/komf-api-models/src/commonMain/kotlin/snd/komf/api/job/KomfMetadataJob.kt:
--------------------------------------------------------------------------------
1 | package snd.komf.api.job
2 |
3 | import kotlinx.datetime.Instant
4 | import kotlinx.serialization.Serializable
5 | import snd.komf.api.KomfServerSeriesId
6 | import kotlin.jvm.JvmInline
7 |
8 | @JvmInline
9 | @Serializable
10 | value class KomfMetadataJobId(val value: String) {
11 | override fun toString() = value
12 | }
13 |
14 | @Serializable
15 | data class KomfMetadataJob(
16 | val seriesId: KomfServerSeriesId,
17 | val id: KomfMetadataJobId,
18 | val status: KomfMetadataJobStatus,
19 | val message: String?,
20 |
21 | val startedAt: Instant,
22 | val finishedAt: Instant?,
23 | )
24 |
25 | enum class KomfMetadataJobStatus {
26 | RUNNING,
27 | FAILED,
28 | COMPLETED
29 | }
30 |
--------------------------------------------------------------------------------
/komf-api-models/src/commonMain/kotlin/snd/komf/api/job/KomfMetadataJobEvents.kt:
--------------------------------------------------------------------------------
1 | package snd.komf.api.job
2 |
3 | import kotlinx.serialization.Serializable
4 | import snd.komf.api.KomfProviders
5 |
6 | const val providerSeriesEventName = "ProviderSeriesEvent"
7 | const val providerBookEventName = "ProviderBookEvent"
8 | const val providerCompletedEventName = "ProviderCompletedEvent"
9 | const val providerErrorEventName = "ProviderErrorEvent"
10 | const val postProcessingStartName = "PostProcessingStartEvent"
11 | const val processingErrorEvent = "ProcessingErrorEvent"
12 | const val eventsStreamNotFoundName = "EventStreamNotFoundEvent"
13 |
14 | @Serializable
15 | sealed interface KomfMetadataJobEvent {
16 |
17 | @Serializable
18 | data class ProviderSeriesEvent(
19 | val provider: KomfProviders,
20 | ) : KomfMetadataJobEvent
21 |
22 | @Serializable
23 | data class ProviderBookEvent(
24 | val provider: KomfProviders,
25 | val totalBooks: Int,
26 | val bookProgress: Int,
27 | ) : KomfMetadataJobEvent
28 |
29 |
30 | @Serializable
31 | data class ProviderErrorEvent(
32 | val provider: KomfProviders,
33 | val message: String
34 | ) : KomfMetadataJobEvent
35 |
36 | @Serializable
37 | data class ProviderCompletedEvent(
38 | val provider: KomfProviders,
39 | ) : KomfMetadataJobEvent
40 |
41 | @Serializable
42 | data object PostProcessingStartEvent : KomfMetadataJobEvent
43 |
44 | @Serializable
45 | data class ProcessingErrorEvent(val message: String) : KomfMetadataJobEvent
46 |
47 | @Serializable
48 | data object NotFound : KomfMetadataJobEvent
49 |
50 | @Serializable
51 | data object UnknownEvent : KomfMetadataJobEvent
52 | }
53 |
--------------------------------------------------------------------------------
/komf-api-models/src/commonMain/kotlin/snd/komf/api/mediaserver/KomfMediaServerConnectionResponse.kt:
--------------------------------------------------------------------------------
1 | package snd.komf.api.mediaserver
2 |
3 | import kotlinx.serialization.Serializable
4 |
5 | @Serializable
6 | data class KomfMediaServerConnectionResponse(
7 | val success: Boolean,
8 | val httpStatusCode: Int?,
9 | val errorMessage: String?
10 | )
--------------------------------------------------------------------------------
/komf-api-models/src/commonMain/kotlin/snd/komf/api/mediaserver/KomfMediaServerLibrary.kt:
--------------------------------------------------------------------------------
1 | package snd.komf.api.mediaserver
2 |
3 | import kotlinx.serialization.Serializable
4 | import kotlin.jvm.JvmInline
5 |
6 | @Serializable
7 | data class KomfMediaServerLibrary(
8 | val id: KomfMediaServerLibraryId,
9 | val name: String,
10 | val roots: Collection,
11 | )
12 |
13 | @JvmInline
14 | @Serializable
15 | value class KomfMediaServerLibraryId(val value: String)
16 |
--------------------------------------------------------------------------------
/komf-api-models/src/commonMain/kotlin/snd/komf/api/metadata/KomfIdentifyRequest.kt:
--------------------------------------------------------------------------------
1 | package snd.komf.api.metadata
2 |
3 | import kotlinx.serialization.Serializable
4 | import snd.komf.api.KomfProviderSeriesId
5 | import snd.komf.api.KomfProviders
6 | import snd.komf.api.KomfServerLibraryId
7 | import snd.komf.api.KomfServerSeriesId
8 |
9 | @Serializable
10 | data class KomfIdentifyRequest(
11 | val libraryId: KomfServerLibraryId?,
12 | val seriesId: KomfServerSeriesId,
13 | val provider: KomfProviders,
14 | val providerSeriesId: KomfProviderSeriesId,
15 | )
16 |
--------------------------------------------------------------------------------
/komf-api-models/src/commonMain/kotlin/snd/komf/api/metadata/KomfMetadataJobResponse.kt:
--------------------------------------------------------------------------------
1 | package snd.komf.api.metadata
2 |
3 | import kotlinx.serialization.Serializable
4 | import snd.komf.api.job.KomfMetadataJobId
5 |
6 | @Serializable
7 | data class KomfMetadataJobResponse(
8 | val jobId: KomfMetadataJobId
9 | )
--------------------------------------------------------------------------------
/komf-api-models/src/commonMain/kotlin/snd/komf/api/metadata/KomfMetadataSearchResult.kt:
--------------------------------------------------------------------------------
1 | package snd.komf.api.metadata
2 |
3 | import kotlinx.serialization.Serializable
4 | import snd.komf.api.KomfProviderSeriesId
5 | import snd.komf.api.KomfProviders
6 |
7 | @Serializable
8 | data class KomfMetadataSeriesSearchResult(
9 | val url: String?,
10 | val imageUrl: String? = null,
11 | val title: String,
12 | val provider: KomfProviders,
13 | val resultId: KomfProviderSeriesId,
14 | )
15 |
--------------------------------------------------------------------------------
/komf-api-models/src/commonMain/kotlin/snd/komf/api/notifications/KomfAppriseRenderResult.kt:
--------------------------------------------------------------------------------
1 | package snd.komf.api.notifications
2 |
3 | import kotlinx.serialization.Serializable
4 |
5 | @Serializable
6 | data class KomfAppriseRenderResult(
7 | val title: String?,
8 | val body: String,
9 | )
10 |
11 |
--------------------------------------------------------------------------------
/komf-api-models/src/commonMain/kotlin/snd/komf/api/notifications/KomfAppriseRequest.kt:
--------------------------------------------------------------------------------
1 | package snd.komf.api.notifications
2 |
3 | import kotlinx.serialization.Serializable
4 |
5 | @Serializable
6 | data class KomfAppriseRequest(
7 | val context: KomfNotificationContext = KomfNotificationContext(),
8 | val templates: KomfAppriseTemplates
9 | )
10 |
--------------------------------------------------------------------------------
/komf-api-models/src/commonMain/kotlin/snd/komf/api/notifications/KomfAppriseTemplates.kt:
--------------------------------------------------------------------------------
1 | package snd.komf.api.notifications
2 |
3 | import kotlinx.serialization.Serializable
4 |
5 | @Serializable
6 | data class KomfAppriseTemplates(
7 | val titleTemplate: String? = null,
8 | val bodyTemplate: String? = null,
9 | )
10 |
--------------------------------------------------------------------------------
/komf-api-models/src/commonMain/kotlin/snd/komf/api/notifications/KomfDiscordRenderResult.kt:
--------------------------------------------------------------------------------
1 | package snd.komf.api.notifications
2 |
3 | import kotlinx.serialization.Serializable
4 |
5 | @Serializable
6 | data class KomfDiscordRenderResult(
7 | val title: String?,
8 | val titleUrl: String?,
9 | val description: String?,
10 | val fields: List,
11 | val footer: String?,
12 | )
13 |
14 | @Serializable
15 | data class EmbedField(
16 | val name: String,
17 | val value: String,
18 | val inline: Boolean,
19 | )
20 |
--------------------------------------------------------------------------------
/komf-api-models/src/commonMain/kotlin/snd/komf/api/notifications/KomfDiscordRequest.kt:
--------------------------------------------------------------------------------
1 | package snd.komf.api.notifications
2 |
3 | import kotlinx.serialization.Serializable
4 |
5 | @Serializable
6 | data class KomfDiscordRequest(
7 | val context: KomfNotificationContext = KomfNotificationContext(),
8 | val templates: KomfDiscordTemplates
9 | )
10 |
--------------------------------------------------------------------------------
/komf-api-models/src/commonMain/kotlin/snd/komf/api/notifications/KomfDiscordTemplates.kt:
--------------------------------------------------------------------------------
1 | package snd.komf.api.notifications
2 |
3 | import kotlinx.serialization.Serializable
4 |
5 | @Serializable
6 | data class KomfDiscordTemplates(
7 | val titleTemplate: String? = null,
8 | val titleUrlTemplate: String? = null,
9 | val descriptionTemplate: String? = null,
10 | val fields: List = emptyList(),
11 | val footerTemplate: String? = null,
12 | )
13 |
14 | @Serializable
15 | data class EmbedFieldTemplate(
16 | val nameTemplate: String,
17 | val valueTemplate: String,
18 | val inline: Boolean,
19 | )
20 |
--------------------------------------------------------------------------------
/komf-app/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget
2 |
3 | plugins {
4 | alias(libs.plugins.kotlinJvm)
5 | alias(libs.plugins.kotlinSerialization)
6 | alias(libs.plugins.shadow)
7 | }
8 |
9 | group = "io.github.snd-r"
10 | version = "1.0.0-SNAPSHOT"
11 |
12 | kotlin {
13 | jvmToolchain(17)
14 | compilerOptions {
15 | jvmTarget.set(JvmTarget.JVM_17)
16 | }
17 | }
18 | java {
19 | targetCompatibility = JavaVersion.VERSION_17
20 | sourceCompatibility = JavaVersion.VERSION_17
21 | toolchain {
22 | languageVersion = JavaLanguageVersion.of(17)
23 | }
24 | }
25 |
26 | dependencies {
27 | implementation(project(":komf-core"))
28 | implementation(project(":komf-mediaserver"))
29 | implementation(project(":komf-notifications"))
30 | implementation(project(":komf-api-models"))
31 |
32 | implementation(libs.logback.core)
33 | implementation(libs.logback.classic)
34 | implementation(libs.slf4j.api)
35 | implementation(libs.kotlin.logging)
36 |
37 | implementation(libs.kotlinx.datetime)
38 | implementation(libs.kotlinx.serialization.json)
39 | implementation(libs.ktor.server.core)
40 | implementation(libs.ktor.server.content.negotiation)
41 | implementation(libs.ktor.server.cio)
42 | implementation(libs.ktor.server.cors)
43 | implementation(libs.ktor.server.default.headers)
44 | implementation(libs.ktor.server.status.pages)
45 | implementation(libs.ktor.server.sse)
46 | implementation(libs.ktor.client.auth)
47 | implementation(libs.ktor.client.core)
48 | implementation(libs.ktor.client.content.negotiation)
49 | implementation(libs.ktor.client.encoding)
50 | implementation(libs.ktor.client.okhttp)
51 | implementation(libs.ktor.serialization.kotlinx.json)
52 | implementation(libs.okhttp)
53 | implementation(libs.okhttp.sse)
54 | implementation(libs.okhttp.logging.interceptor)
55 | implementation(libs.kaml)
56 | }
57 |
58 | tasks {
59 | shadowJar {
60 | manifest {
61 | attributes(Pair("Main-Class", "snd.komf.app.ApplicationKt"))
62 | }
63 | }
64 | }
65 |
66 | tasks.register("depsize") {
67 | description = "Prints dependencies for \"runtime\" configuration"
68 | doLast {
69 | listConfigurationDependencies(configurations["runtimeClasspath"])
70 | }
71 | }
72 |
73 | fun listConfigurationDependencies(configuration: Configuration) {
74 | val formatStr = "%,10.2f"
75 |
76 | val size = configuration.sumOf { it.length() / (1024.0 * 1024.0) }
77 |
78 | val out = StringBuffer()
79 | out.append("\nConfiguration name: \"${configuration.name}\"\n")
80 | if (size > 0) {
81 | out.append("Total dependencies size:".padEnd(65))
82 | out.append("${String.format(formatStr, size)} Mb\n\n")
83 |
84 | configuration.sortedBy { -it.length() }
85 | .forEach {
86 | out.append(it.name.padEnd(65))
87 | out.append("${String.format(formatStr, (it.length() / 1024.0))} kb\n")
88 | }
89 | } else {
90 | out.append("No dependencies found")
91 | }
92 | println(out)
93 | }
94 |
--------------------------------------------------------------------------------
/komf-app/src/main/kotlin/snd/komf/app/Application.kt:
--------------------------------------------------------------------------------
1 | package snd.komf.app
2 |
3 | import io.github.oshai.kotlinlogging.KotlinLogging
4 | import java.nio.file.Path
5 | import kotlin.system.exitProcess
6 |
7 | private val logger = KotlinLogging.logger {}
8 |
9 | fun main(vararg args: String) {
10 | runCatching {
11 | val configFile = args.firstOrNull()?.let { Path.of(it) }
12 | val configDir = System.getenv("KOMF_CONFIG_DIR")?.let { Path.of(it) }
13 | AppContext(configDir?: configFile)
14 | }.getOrElse {
15 | logger.error(it) { "Failed to start the app" }
16 | exitProcess(1)
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/komf-app/src/main/kotlin/snd/komf/app/api/ConfigRoutes.kt:
--------------------------------------------------------------------------------
1 | package snd.komf.app.api
2 |
3 | import io.github.oshai.kotlinlogging.KotlinLogging
4 | import io.ktor.http.*
5 | import io.ktor.server.request.*
6 | import io.ktor.server.response.*
7 | import io.ktor.server.routing.*
8 | import kotlinx.coroutines.sync.Mutex
9 | import kotlinx.coroutines.sync.withLock
10 | import snd.komf.api.KomfErrorResponse
11 | import snd.komf.api.config.KomfConfigUpdateRequest
12 | import snd.komf.app.AppContext
13 | import snd.komf.app.api.mappers.AppConfigMapper
14 | import snd.komf.app.api.mappers.AppConfigUpdateMapper
15 |
16 | private val logger = KotlinLogging.logger {}
17 |
18 | class ConfigRoutes(
19 | private val appContext: AppContext,
20 | ) {
21 | private val configMapper = AppConfigMapper()
22 | private val updateConfigMapper = AppConfigUpdateMapper()
23 | private val mutex = Mutex()
24 |
25 | fun registerRoutes(routing: Route) {
26 | with(routing) {
27 | getConfigRoute()
28 | updateConfigRoute()
29 | }
30 | }
31 |
32 | private fun Route.getConfigRoute() {
33 | get("/config") {
34 | call.respond(configMapper.toDto(appContext.appConfig))
35 | }
36 | }
37 |
38 | private fun Route.updateConfigRoute() {
39 | patch("/config") {
40 | val request = call.receive()
41 | mutex.withLock {
42 | val config = updateConfigMapper.patch(appContext.appConfig, request)
43 | try {
44 | appContext.refreshState(config)
45 | } catch (e: Exception) {
46 | logger.catching(e)
47 | call.respond(
48 | HttpStatusCode.UnprocessableEntity,
49 | KomfErrorResponse("${e::class.simpleName}: ${e.message}")
50 | )
51 | return@patch
52 | }
53 |
54 | }
55 | call.response.status(HttpStatusCode.NoContent)
56 | }
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/komf-app/src/main/kotlin/snd/komf/app/api/MediaServerRoutes.kt:
--------------------------------------------------------------------------------
1 | package snd.komf.app.api
2 |
3 | import io.ktor.client.plugins.*
4 | import io.ktor.http.*
5 | import io.ktor.server.response.*
6 | import io.ktor.server.routing.*
7 | import kotlinx.coroutines.flow.StateFlow
8 | import snd.komf.api.mediaserver.KomfMediaServerConnectionResponse
9 | import snd.komf.api.mediaserver.KomfMediaServerLibrary
10 | import snd.komf.api.mediaserver.KomfMediaServerLibraryId
11 | import snd.komf.mediaserver.MediaServerClient
12 |
13 | class MediaServerRoutes(
14 | private val mediaServerClient: StateFlow,
15 | ) {
16 |
17 | fun registerRoutes(routing: Route) {
18 | routing.route("/media-server") {
19 | checkConnectionRoute()
20 | getLibrariesRoute()
21 | }
22 | }
23 |
24 | private fun Route.checkConnectionRoute() {
25 | get("/connected") {
26 | try {
27 | mediaServerClient.value.getLibraries()
28 | call.respond(
29 | HttpStatusCode.OK,
30 | KomfMediaServerConnectionResponse(
31 | success = true,
32 | httpStatusCode = HttpStatusCode.OK.value,
33 | errorMessage = null
34 | )
35 | )
36 | } catch (exception: ResponseException) {
37 | call.respond(
38 | HttpStatusCode.OK,
39 | KomfMediaServerConnectionResponse(
40 | success = false,
41 | httpStatusCode = exception.response.status.value,
42 | errorMessage = HttpStatusCode.fromValue(exception.response.status.value).description
43 | )
44 | )
45 | } catch (exception: Exception) {
46 | call.respond(
47 | HttpStatusCode.OK,
48 | KomfMediaServerConnectionResponse(
49 | success = false,
50 | httpStatusCode = null,
51 | errorMessage =
52 | buildString {
53 | exception.message?.let { append(it) }
54 | exception.cause?.message?.let { append("; $it") }
55 | }
56 | )
57 | )
58 |
59 | }
60 | }
61 | }
62 |
63 | private fun Route.getLibrariesRoute() {
64 | get("/libraries") {
65 | val libraries = mediaServerClient.value.getLibraries().map {
66 | KomfMediaServerLibrary(
67 | id = KomfMediaServerLibraryId(it.id.value),
68 | name = it.name,
69 | roots = it.roots
70 | )
71 | }
72 | call.respond(HttpStatusCode.OK, libraries)
73 | }
74 | }
75 | }
--------------------------------------------------------------------------------
/komf-app/src/main/kotlin/snd/komf/app/api/deprecated/DeprecatedConfigRoutes.kt:
--------------------------------------------------------------------------------
1 | package snd.komf.app.api.deprecated
2 |
3 | import io.ktor.http.*
4 | import io.ktor.server.application.*
5 | import io.ktor.server.request.*
6 | import io.ktor.server.response.*
7 | import io.ktor.server.routing.*
8 | import snd.komf.app.AppContext
9 | import snd.komf.app.api.deprecated.dto.AppConfigUpdateDto
10 |
11 | class DeprecatedConfigRoutes(
12 | private val appContext: AppContext,
13 | private val configMapper: DeprecatedConfigUpdateMapper,
14 | ) {
15 |
16 | fun registerRoutes(application: Application) {
17 | application.routing {
18 | getConfigRoute()
19 | updateConfigRoute()
20 | }
21 | }
22 |
23 | private fun Routing.getConfigRoute() {
24 | get("/config") {
25 | val config = configMapper.toDto(appContext.appConfig)
26 | call.respond(config)
27 | }
28 | }
29 |
30 | private fun Routing.updateConfigRoute() {
31 | patch("/config") {
32 | val request = call.receive()
33 | val config = configMapper.patch(appContext.appConfig, request)
34 |
35 | try {
36 | appContext.refreshState(config)
37 | } catch (e: Exception) {
38 | call.respond(HttpStatusCode.UnprocessableEntity, "${e::class.simpleName}: ${e.message}")
39 | return@patch
40 | }
41 |
42 | call.response.status(HttpStatusCode.NoContent)
43 | }
44 | }
45 | }
--------------------------------------------------------------------------------
/komf-app/src/main/kotlin/snd/komf/app/api/deprecated/dto/IdentifySeriesRequest.kt:
--------------------------------------------------------------------------------
1 | package snd.komf.app.api.deprecated.dto
2 |
3 | import kotlinx.serialization.Serializable
4 |
5 | @Serializable
6 | data class IdentifySeriesRequest(
7 | val libraryId: String? = null,
8 | val seriesId: String,
9 | val provider: String,
10 | val providerSeriesId: String,
11 | val edition: String? = null
12 | )
13 |
--------------------------------------------------------------------------------
/komf-app/src/main/kotlin/snd/komf/app/config/ConfigWriter.kt:
--------------------------------------------------------------------------------
1 | package snd.komf.app.config
2 |
3 | import com.charleskorn.kaml.Yaml
4 | import java.nio.file.Path
5 | import kotlin.io.path.exists
6 | import kotlin.io.path.isDirectory
7 | import kotlin.io.path.isWritable
8 | import kotlin.io.path.writeText
9 | import kotlin.text.Charsets.UTF_8
10 |
11 | class ConfigWriter(private val yaml: Yaml) {
12 |
13 | @Synchronized
14 | fun writeConfig(config: AppConfig, path: Path) {
15 | checkWriteAccess(path)
16 | if (path.isDirectory()) {
17 | path.resolve("application.yml")
18 | .writeText(yaml.encodeToString(AppConfig.serializer(), config), UTF_8)
19 | } else {
20 | path.writeText(yaml.encodeToString(AppConfig.serializer(), config), UTF_8)
21 | }
22 | }
23 |
24 | @Synchronized
25 | fun writeConfigToDefaultPath(config: AppConfig) {
26 | val filePath = Path.of(".").toAbsolutePath().normalize().resolve("application.yml")
27 | if (filePath.exists())
28 | checkWriteAccess(filePath)
29 |
30 | filePath.writeText(yaml.encodeToString(AppConfig.serializer(), config), UTF_8)
31 | }
32 |
33 | private fun checkWriteAccess(path: Path) {
34 | if (path.isWritable().not()) throw AccessDeniedException(file = path.toFile(), reason = "No write access to config file")
35 | }
36 | }
--------------------------------------------------------------------------------
/komf-app/src/main/kotlin/snd/komf/app/module/NotificationsModule.kt:
--------------------------------------------------------------------------------
1 | package snd.komf.app.module
2 |
3 | import io.ktor.client.*
4 | import io.ktor.client.plugins.*
5 | import io.ktor.client.plugins.contentnegotiation.*
6 | import io.ktor.http.HttpStatusCode.Companion.TooManyRequests
7 | import io.ktor.serialization.kotlinx.json.*
8 | import kotlinx.serialization.json.Json
9 | import snd.komf.app.config.NotificationsConfig
10 | import snd.komf.ktor.HttpRequestRateLimiter
11 | import snd.komf.ktor.komfUserAgent
12 | import snd.komf.notifications.apprise.AppriseCliService
13 | import snd.komf.notifications.apprise.AppriseVelocityTemplates
14 | import snd.komf.notifications.discord.DiscordVelocityTemplates
15 | import snd.komf.notifications.discord.DiscordWebhookService
16 | import kotlin.time.Duration.Companion.seconds
17 |
18 |
19 | class NotificationsModule(
20 | notificationsConfig: NotificationsConfig,
21 | ktorBaseClient: HttpClient,
22 | ) {
23 | private val discordConfig = notificationsConfig.discord
24 | private val appriseConfig = notificationsConfig.apprise
25 |
26 | private val json = Json {
27 | ignoreUnknownKeys = true
28 | encodeDefaults = false
29 | }
30 | private val discordKtorClient = ktorBaseClient.config {
31 | install(HttpRequestRetry) {
32 | retryIf(3) { _, response ->
33 | when (response.status.value) {
34 | TooManyRequests.value -> true
35 | in 500..599 -> true
36 | else -> false
37 | }
38 | }
39 | exponentialDelay(respectRetryAfterHeader = true)
40 | }
41 | install(HttpRequestRateLimiter) {
42 | interval = 2.seconds
43 | eventsPerInterval = 4
44 | allowBurst = false
45 | }
46 | install(UserAgent) { agent = komfUserAgent }
47 | install(ContentNegotiation) { json(json) }
48 | }
49 |
50 | val appriseVelocityRenderer = AppriseVelocityTemplates(notificationsConfig.templatesDirectory)
51 | val appriseService = AppriseCliService(
52 | urls = notificationsConfig.apprise.urls ?: emptyList(),
53 | templateRenderer = appriseVelocityRenderer,
54 | seriesCover = appriseConfig.seriesCover,
55 | )
56 |
57 | val discordVelocityRenderer = DiscordVelocityTemplates(notificationsConfig.templatesDirectory)
58 | val discordWebhookService = DiscordWebhookService(
59 | ktor = discordKtorClient,
60 | json = json,
61 | templateRenderer = discordVelocityRenderer,
62 | seriesCover = discordConfig.seriesCover,
63 | webhooks = discordConfig.webhooks ?: emptyList(),
64 | embedColor = discordConfig.embedColor,
65 | )
66 | }
--------------------------------------------------------------------------------
/komf-app/src/main/kotlin/snd/komf/app/module/ProvidersModule.kt:
--------------------------------------------------------------------------------
1 | package snd.komf.app.module
2 |
3 | import io.ktor.client.*
4 | import snd.komf.providers.MetadataProvidersConfig
5 | import snd.komf.providers.ProviderFactory
6 |
7 | class ProvidersModule(
8 | providersConfig: MetadataProvidersConfig,
9 | ktorBaseClient: HttpClient
10 | ) {
11 | private val providerFactory = ProviderFactory(ktorBaseClient)
12 |
13 | val metadataProviders = providerFactory.getMetadataProviders(providersConfig)
14 | }
--------------------------------------------------------------------------------
/komf-app/src/main/resources/apprise_body.vm:
--------------------------------------------------------------------------------
1 | #if (${series.metadata.summary} != "")
2 | ${series.metadata.summary}
3 | #end
4 | new #if(${books.size()} == 1)book was #{else}books were #{end}added to library ${library.name}:
5 | #foreach ($book in $books)
6 | ${book.name}
7 | #end
--------------------------------------------------------------------------------
/komf-app/src/main/resources/apprise_title.vm:
--------------------------------------------------------------------------------
1 | $series.name
2 |
--------------------------------------------------------------------------------
/komf-app/src/main/resources/db/migration/sqlite/V1__initial_migration.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE MATCHED_SERIES
2 | (
3 | SERIES_ID varchar NOT NULL PRIMARY KEY,
4 | THUMBNAIL_ID varchar,
5 | PROVIDER varchar NOT NULL,
6 | PROVIDER_SERIES_ID varchar NOT NULL
7 | );
8 |
--------------------------------------------------------------------------------
/komf-app/src/main/resources/db/migration/sqlite/V2__matched_books.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE MATCHED_BOOKS
2 | (
3 | BOOK_ID varchar NOT NULL PRIMARY KEY,
4 | SERIES_ID varchar NOT NULL,
5 | THUMBNAIL_ID varchar
6 | );
7 |
--------------------------------------------------------------------------------
/komf-app/src/main/resources/db/migration/sqlite/V3__table_indexes.sql:
--------------------------------------------------------------------------------
1 | CREATE INDEX matched_books_book_id_idx on MATCHED_BOOKS (BOOK_ID);
2 | CREATE INDEX matched_series_series_id_idx on MATCHED_SERIES (SERIES_ID);
3 |
--------------------------------------------------------------------------------
/komf-app/src/main/resources/db/migration/sqlite/V4__drop_provider_columns.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE MATCHED_SERIES
2 | RENAME TO MATCHED_SERIES_OLD;
3 |
4 | CREATE TABLE MATCHED_SERIES
5 | (
6 | SERIES_ID varchar NOT NULL PRIMARY KEY,
7 | THUMBNAIL_ID varchar
8 | );
9 |
10 | INSERT INTO MATCHED_SERIES (SERIES_ID, THUMBNAIL_ID)
11 | SELECT SERIES_ID, THUMBNAIL_ID
12 | FROM MATCHED_SERIES_OLD;
13 |
14 | DROP TABLE MATCHED_SERIES_OLD
15 |
--------------------------------------------------------------------------------
/komf-app/src/main/resources/db/migration/sqlite/V5__cleanup.sql:
--------------------------------------------------------------------------------
1 | DELETE
2 | FROM MATCHED_BOOKS
3 | WHERE THUMBNAIL_ID IS NULL;
4 |
5 | DELETE
6 | FROM MATCHED_SERIES
7 | WHERE THUMBNAIL_ID IS NULL;
8 |
--------------------------------------------------------------------------------
/komf-app/src/main/resources/db/migration/sqlite/V6__server_type_columns.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE MATCHED_SERIES
2 | RENAME TO MATCHED_SERIES_OLD;
3 |
4 | CREATE TABLE MATCHED_SERIES
5 | (
6 | SERIES_ID varchar NOT NULL,
7 | SERVER_TYPE varchar NOT NULL,
8 | THUMBNAIL_ID varchar,
9 | PRIMARY KEY (SERIES_ID, SERVER_TYPE)
10 | );
11 |
12 | INSERT INTO MATCHED_SERIES (SERIES_ID, THUMBNAIL_ID, SERVER_TYPE)
13 | SELECT SERIES_ID, THUMBNAIL_ID, 'KOMGA'
14 | FROM MATCHED_SERIES_OLD;
15 |
16 | DROP TABLE MATCHED_SERIES_OLD;
17 |
18 | ALTER TABLE MATCHED_BOOKS
19 | RENAME TO MATCHED_BOOKS_OLD;
20 |
21 | CREATE TABLE MATCHED_BOOKS
22 | (
23 | BOOK_ID varchar NOT NULL,
24 | SERVER_TYPE varchar NOT NULL,
25 | SERIES_ID varchar NOT NULL,
26 | THUMBNAIL_ID varchar,
27 | PRIMARY KEY (BOOK_ID, SERVER_TYPE)
28 | );
29 |
30 | INSERT INTO MATCHED_BOOKS (BOOK_ID, SERIES_ID, THUMBNAIL_ID, SERVER_TYPE)
31 | SELECT BOOK_ID, SERIES_ID, THUMBNAIL_ID, 'KOMGA'
32 | FROM MATCHED_BOOKS_OLD;
33 |
34 | DROP TABLE MATCHED_BOOKS_OLD;
35 |
--------------------------------------------------------------------------------
/komf-app/src/main/resources/db/migration/sqlite/V7__series_match.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE MATCHED_SERIES
2 | RENAME TO SERIES_THUMBNAILS;
3 |
4 | ALTER TABLE MATCHED_BOOKS
5 | RENAME TO BOOK_THUMBNAILS;
6 |
7 |
8 | CREATE TABLE SERIES_MATCH
9 | (
10 | SERIES_ID varchar NOT NULL,
11 | TYPE varchar NOT NULL,
12 | SERVER_TYPE varchar NOT NULL,
13 | PROVIDER varchar NOT NULL,
14 | PROVIDER_SERIES_ID varchar NOT NULL,
15 | EDITION varchar,
16 | PRIMARY KEY (SERIES_ID, SERVER_TYPE)
17 | );
18 |
--------------------------------------------------------------------------------
/komf-app/src/main/resources/db/migration/sqlite/V8__delete_yen_press_matches.sql:
--------------------------------------------------------------------------------
1 | DELETE
2 | FROM SERIES_MATCH
3 | WHERE PROVIDER = 'YEN_PRESS'
--------------------------------------------------------------------------------
/komf-app/src/main/resources/db/migration/sqlite/V9__delete_kodansha_matches.sql:
--------------------------------------------------------------------------------
1 | DELETE
2 | FROM SERIES_MATCH
3 | WHERE PROVIDER = 'KODANSHA';
4 |
--------------------------------------------------------------------------------
/komf-app/src/main/resources/description.vm:
--------------------------------------------------------------------------------
1 | #if (${series.metadata.summary} != "")
2 | ${series.metadata.summary}
3 | #end
4 | ***new #if(${books.size()} == 1)book was #{else}books were #{end}added to library ${library.name}:***
5 | #foreach ($book in $books)
6 | **${book.name}**
7 | #end
8 |
--------------------------------------------------------------------------------
/komf-app/src/main/resources/title.vm:
--------------------------------------------------------------------------------
1 | $series.name
--------------------------------------------------------------------------------
/komf-client/build.gradle.kts:
--------------------------------------------------------------------------------
1 | @file:OptIn(ExperimentalKotlinGradlePluginApi::class)
2 |
3 | import com.vanniktech.maven.publish.SonatypeHost
4 | import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
5 | import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
6 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget
7 |
8 | plugins {
9 | alias(libs.plugins.androidLibrary)
10 | alias(libs.plugins.kotlinMultiplatform)
11 | alias(libs.plugins.kotlinSerialization)
12 | alias(libs.plugins.mavenPublish)
13 | signing
14 | }
15 |
16 | group = "io.github.snd-r"
17 | version = libs.versions.app.version.get()
18 |
19 | kotlin {
20 | jvmToolchain(17)
21 | androidTarget {
22 | compilerOptions { jvmTarget.set(JvmTarget.JVM_17) }
23 | publishLibraryVariants("release")
24 | }
25 | jvm {
26 | compilerOptions { jvmTarget.set(JvmTarget.JVM_17) }
27 | }
28 |
29 | @OptIn(ExperimentalWasmDsl::class)
30 | wasmJs {
31 | moduleName = "komf-client"
32 | browser()
33 | }
34 |
35 | sourceSets {
36 | commonMain.dependencies {
37 | api(project(":komf-api-models"))
38 | implementation(libs.kotlin.logging)
39 | implementation(libs.kotlinx.datetime)
40 | implementation(libs.kotlinx.serialization.json)
41 | implementation(libs.ktor.client.core)
42 | implementation(libs.ktor.client.content.negotiation)
43 | implementation(libs.ktor.client.encoding)
44 | implementation(libs.ktor.serialization.kotlinx.json)
45 | }
46 | }
47 |
48 | }
49 | android {
50 | namespace = "snd.komf"
51 | compileSdk = 35
52 |
53 | defaultConfig {
54 | minSdk = 26
55 | }
56 | compileOptions {
57 | sourceCompatibility = JavaVersion.VERSION_17
58 | targetCompatibility = JavaVersion.VERSION_17
59 | }
60 | }
61 |
62 | mavenPublishing {
63 | publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL, automaticRelease = false)
64 | coordinates("io.github.snd-r.komf", "client", libs.versions.app.version.get())
65 | signAllPublications()
66 |
67 | pom {
68 | name.set("Komf API client")
69 | description.set("Komf API client")
70 | url.set("https://github.com/Snd-R/komf")
71 | licenses {
72 | license {
73 | name.set("MIT License")
74 | url.set("https://github.com/Snd-R/komf/blob/master/LICENSE")
75 | distribution.set("repo")
76 | }
77 | }
78 | developers {
79 | developer {
80 | id.set("Snd-R")
81 | name.set("Snd-R")
82 | url.set("https://github.com/Snd-R")
83 | }
84 | }
85 | scm {
86 | url.set("https://github.com/Snd-R/komf")
87 | connection.set("scm:git:git://github.com/Snd-R/komf.git")
88 | developerConnection.set("scm:git:ssh://git@github.com/Snd-R/komf.git")
89 | }
90 | }
91 | }
92 | signing {
93 | useGpgCmd()
94 | }
95 |
--------------------------------------------------------------------------------
/komf-client/src/commonMain/kotlin/snd/komf/client/KomfClientFactory.kt:
--------------------------------------------------------------------------------
1 | package snd.komf.client
2 |
3 | import io.ktor.client.*
4 | import io.ktor.client.plugins.*
5 | import io.ktor.client.plugins.contentnegotiation.*
6 | import io.ktor.client.plugins.cookies.*
7 | import io.ktor.client.plugins.sse.*
8 | import io.ktor.serialization.kotlinx.json.*
9 | import kotlinx.serialization.json.Json
10 | import snd.komf.api.MediaServer
11 |
12 | class KomfClientFactory private constructor(private val builder: Builder) {
13 |
14 | fun configClient() = KomfConfigClient(ktor)
15 | fun metadataClient(mediaServer: MediaServer) = KomfMetadataClient(ktor, mediaServer)
16 | fun mediaServerClient(mediaServer: MediaServer) = KomfMediaServerClient(ktor, mediaServer)
17 | fun jobClient() = KomfJobClient(ktor = ktor, json = json)
18 | fun notificationClient() = KomfNotificationClient(ktor = ktor)
19 |
20 | private val json = Json(builder.json) {
21 | ignoreUnknownKeys = true
22 | encodeDefaults = false
23 | }
24 | private val baseUrl: () -> String = builder.baseUrl
25 |
26 | private val ktor: HttpClient = (builder.ktor ?: HttpClient()).config {
27 | expectSuccess = true
28 | builder.cookieStorage?.let { install(HttpCookies) { storage = it } }
29 | defaultRequest { url(baseUrl()) }
30 | install(ContentNegotiation) { json(json) }
31 | install(SSE)
32 | }
33 |
34 | class Builder {
35 | internal var ktor: HttpClient? = null
36 | internal var baseUrl: () -> String = { "http://localhost:8085" }
37 | internal var cookieStorage: CookiesStorage? = AcceptAllCookiesStorage()
38 | internal var json: Json = Json
39 |
40 | fun ktor(ktor: HttpClient) = apply {
41 | this.ktor = ktor
42 | }
43 |
44 | fun baseUrl(block: () -> String) = apply {
45 | this.baseUrl = block
46 | }
47 |
48 | fun cookieStorage(cookiesStorage: CookiesStorage?) = apply {
49 | this.cookieStorage = cookiesStorage
50 | }
51 |
52 | fun build(): KomfClientFactory {
53 | return KomfClientFactory(this)
54 | }
55 | }
56 | }
--------------------------------------------------------------------------------
/komf-client/src/commonMain/kotlin/snd/komf/client/KomfConfigClient.kt:
--------------------------------------------------------------------------------
1 | package snd.komf.client
2 |
3 | import io.ktor.client.*
4 | import io.ktor.client.call.*
5 | import io.ktor.client.request.*
6 | import io.ktor.http.*
7 | import snd.komf.api.config.KomfConfig
8 | import snd.komf.api.config.KomfConfigUpdateRequest
9 |
10 | class KomfConfigClient(private val ktor: HttpClient) {
11 |
12 | suspend fun getConfig(): KomfConfig {
13 | return ktor.get("/api/config").body()
14 | }
15 |
16 | suspend fun updateConfig(request: KomfConfigUpdateRequest) {
17 | ktor.patch("/api/config") {
18 | contentType(ContentType.Application.Json)
19 | setBody(request)
20 | }
21 | }
22 |
23 | }
--------------------------------------------------------------------------------
/komf-client/src/commonMain/kotlin/snd/komf/client/KomfErrorResponse.kt:
--------------------------------------------------------------------------------
1 | package snd.komf.client
2 |
3 | import io.ktor.client.call.*
4 | import io.ktor.client.plugins.*
5 | import io.ktor.serialization.*
6 | import kotlinx.serialization.SerializationException
7 | import snd.komf.api.KomfErrorResponse
8 |
9 | suspend fun ResponseException.toKomfErrorResponse(): KomfErrorResponse? =
10 | try {
11 | response.body()
12 | } catch (e: SerializationException) {
13 | null
14 | } catch (e: JsonConvertException) {
15 | null
16 | }
17 |
--------------------------------------------------------------------------------
/komf-client/src/commonMain/kotlin/snd/komf/client/KomfJobClient.kt:
--------------------------------------------------------------------------------
1 | package snd.komf.client
2 |
3 | import io.ktor.client.*
4 | import io.ktor.client.call.*
5 | import io.ktor.client.plugins.sse.*
6 | import io.ktor.client.request.*
7 | import kotlinx.coroutines.flow.Flow
8 | import kotlinx.coroutines.flow.map
9 | import kotlinx.serialization.json.Json
10 | import snd.komf.api.KomfPage
11 | import snd.komf.api.job.KomfMetadataJob
12 | import snd.komf.api.job.KomfMetadataJobEvent
13 | import snd.komf.api.job.KomfMetadataJobEvent.ProcessingErrorEvent
14 | import snd.komf.api.job.KomfMetadataJobEvent.ProviderBookEvent
15 | import snd.komf.api.job.KomfMetadataJobEvent.ProviderCompletedEvent
16 | import snd.komf.api.job.KomfMetadataJobEvent.ProviderErrorEvent
17 | import snd.komf.api.job.KomfMetadataJobEvent.ProviderSeriesEvent
18 | import snd.komf.api.job.KomfMetadataJobEvent.UnknownEvent
19 | import snd.komf.api.job.KomfMetadataJobId
20 | import snd.komf.api.job.KomfMetadataJobStatus
21 | import snd.komf.api.job.eventsStreamNotFoundName
22 | import snd.komf.api.job.postProcessingStartName
23 | import snd.komf.api.job.processingErrorEvent
24 | import snd.komf.api.job.providerBookEventName
25 | import snd.komf.api.job.providerCompletedEventName
26 | import snd.komf.api.job.providerErrorEventName
27 | import snd.komf.api.job.providerSeriesEventName
28 |
29 | class KomfJobClient(
30 | private val ktor: HttpClient,
31 | private val json: Json
32 | ) {
33 |
34 | suspend fun getJob(jobId: KomfMetadataJobId): KomfMetadataJob {
35 | return ktor.get("/api/jobs/$jobId").body()
36 | }
37 |
38 | suspend fun getJobs(
39 | status: KomfMetadataJobStatus? = null,
40 | page: Int? = null,
41 | pageSize: Int? = null,
42 | ): KomfPage> {
43 | return ktor.get("/api/jobs") {
44 | status?.let { parameter("status", status.name) }
45 | page?.let { parameter("page", page) }
46 | pageSize?.let { parameter("pageSize", pageSize) }
47 | }.body()
48 | }
49 |
50 | suspend fun getJobEvents(jobId: KomfMetadataJobId): Flow {
51 | return ktor.sseSession("/api/jobs/${jobId.value}/events").incoming
52 | .map { json.toKomfEvent(it.event, it.data) }
53 | }
54 |
55 | suspend fun deleteAll() {
56 | ktor.delete("/api/jobs/all")
57 | }
58 |
59 |
60 | private fun Json.toKomfEvent(event: String?, data: String?): KomfMetadataJobEvent {
61 | if (data == null) return UnknownEvent
62 |
63 | return when (event) {
64 | providerSeriesEventName -> decodeFromString(data)
65 | providerBookEventName -> decodeFromString(data)
66 | providerErrorEventName -> decodeFromString(data)
67 | providerCompletedEventName -> decodeFromString(data)
68 | postProcessingStartName -> KomfMetadataJobEvent.PostProcessingStartEvent
69 | processingErrorEvent -> decodeFromString(data)
70 | eventsStreamNotFoundName -> KomfMetadataJobEvent.NotFound
71 | else -> UnknownEvent
72 | }
73 | }
74 | }
--------------------------------------------------------------------------------
/komf-client/src/commonMain/kotlin/snd/komf/client/KomfMediaServerClient.kt:
--------------------------------------------------------------------------------
1 | package snd.komf.client
2 |
3 | import io.ktor.client.*
4 | import io.ktor.client.call.*
5 | import io.ktor.client.request.*
6 | import snd.komf.api.MediaServer
7 | import snd.komf.api.mediaserver.KomfMediaServerConnectionResponse
8 | import snd.komf.api.mediaserver.KomfMediaServerLibrary
9 |
10 | class KomfMediaServerClient(
11 | private val ktor: HttpClient,
12 | mediaServer: MediaServer
13 | ) {
14 | private val mediaServerApiPrefix = "/api/${mediaServer.name.lowercase()}/media-server"
15 |
16 | suspend fun checkConnection(): KomfMediaServerConnectionResponse {
17 | return ktor.get("$mediaServerApiPrefix/connected").body()
18 | }
19 |
20 | suspend fun getLibraries(): List {
21 | return ktor.get("$mediaServerApiPrefix/libraries").body()
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/komf-client/src/commonMain/kotlin/snd/komf/client/KomfNotificationClient.kt:
--------------------------------------------------------------------------------
1 | package snd.komf.client
2 |
3 | import io.ktor.client.*
4 | import io.ktor.client.call.*
5 | import io.ktor.client.request.*
6 | import io.ktor.http.*
7 | import snd.komf.api.notifications.KomfAppriseRenderResult
8 | import snd.komf.api.notifications.KomfAppriseRequest
9 | import snd.komf.api.notifications.KomfAppriseTemplates
10 | import snd.komf.api.notifications.KomfDiscordTemplates
11 | import snd.komf.api.notifications.KomfDiscordRenderResult
12 | import snd.komf.api.notifications.KomfDiscordRequest
13 |
14 | class KomfNotificationClient(
15 | private val ktor: HttpClient,
16 | ) {
17 | suspend fun getDiscordTemplates(): KomfDiscordTemplates {
18 | return ktor.get("/api/notifications/discord/templates").body()
19 | }
20 |
21 | suspend fun updateDiscordTemplates(templates: KomfDiscordTemplates): KomfDiscordTemplates {
22 | return ktor.post("/api/notifications/discord/templates") {
23 | contentType(ContentType.Application.Json)
24 | setBody(templates)
25 | }.body()
26 | }
27 |
28 | suspend fun renderDiscord(request: KomfDiscordRequest): KomfDiscordRenderResult {
29 | return ktor.post("/api/notifications/discord/render") {
30 | contentType(ContentType.Application.Json)
31 | setBody(request)
32 | }.body()
33 | }
34 |
35 | suspend fun sendDiscord(request: KomfDiscordRequest) {
36 | ktor.post("/api/notifications/discord/send") {
37 | contentType(ContentType.Application.Json)
38 | setBody(request)
39 | }
40 | }
41 |
42 |
43 | suspend fun getAppriseTemplates(): KomfAppriseTemplates {
44 | return ktor.get("/api/notifications/apprise/templates").body()
45 | }
46 |
47 | suspend fun updateAppriseTemplates(templates: KomfAppriseTemplates): KomfAppriseTemplates {
48 | return ktor.post("/api/notifications/apprise/templates") {
49 | contentType(ContentType.Application.Json)
50 | setBody(templates)
51 | }.body()
52 | }
53 |
54 | suspend fun renderApprise(request: KomfAppriseRequest): KomfAppriseRenderResult {
55 | return ktor.post("/api/notifications/apprise/render") {
56 | contentType(ContentType.Application.Json)
57 | setBody(request)
58 | }.body()
59 | }
60 |
61 | suspend fun sendApprise(request: KomfAppriseRequest) {
62 | ktor.post("/api/notifications/apprise/send") {
63 | contentType(ContentType.Application.Json)
64 | setBody(request)
65 | }
66 | }
67 |
68 | }
--------------------------------------------------------------------------------
/komf-core/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
2 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget
3 |
4 | plugins {
5 | alias(libs.plugins.androidLibrary)
6 | alias(libs.plugins.kotlinAtomicfu)
7 | alias(libs.plugins.kotlinMultiplatform)
8 | alias(libs.plugins.kotlinSerialization)
9 | }
10 |
11 | group = "io.github.snd-r"
12 | version = libs.versions.app.version.get()
13 |
14 | kotlin {
15 | jvmToolchain(17)
16 | jvm {
17 | @OptIn(ExperimentalKotlinGradlePluginApi::class)
18 | compilerOptions {
19 | jvmTarget.set(JvmTarget.JVM_17)
20 | }
21 | }
22 | androidTarget {
23 | @OptIn(ExperimentalKotlinGradlePluginApi::class)
24 | compilerOptions {
25 | jvmTarget.set(JvmTarget.JVM_17)
26 | }
27 | }
28 |
29 | // @OptIn(ExperimentalWasmDsl::class)
30 | // wasmJs {
31 | // moduleName = "komf-core"
32 | // }
33 |
34 | sourceSets {
35 | commonMain.dependencies {
36 | implementation(libs.cache4k)
37 | implementation(libs.commons.compress)
38 | implementation(libs.commons.text)
39 | implementation(libs.kotlin.logging)
40 | implementation(libs.kotlinx.coroutines.core)
41 | implementation(libs.kotlinx.datetime)
42 | implementation(libs.kotlinx.serialization.json)
43 | implementation(libs.kotlinx.io.core)
44 | implementation(libs.ktor.client.core)
45 | implementation(libs.ktor.client.content.negotiation)
46 | implementation(libs.ktor.client.encoding)
47 | implementation(libs.ktor.serialization.kotlinx.json)
48 | implementation(libs.ksoup)
49 | implementation(libs.xmlutil.core)
50 | implementation(libs.xmlutil.serialization)
51 |
52 | }
53 |
54 | val jvmMain by getting
55 | jvmMain.dependencies {
56 | implementation(libs.twelvemonkeys.imageio.core)
57 | implementation(libs.twelvemonkeys.imageio.jpeg)
58 | implementation(libs.twelvemonkeys.imageio.webp)
59 |
60 | }
61 | }
62 |
63 | }
64 |
65 | android {
66 | namespace = "snd.komf"
67 | compileSdk = 35
68 |
69 | defaultConfig {
70 | minSdk = 26
71 | }
72 | compileOptions {
73 | sourceCompatibility = JavaVersion.VERSION_17
74 | targetCompatibility = JavaVersion.VERSION_17
75 | }
76 |
77 | }
78 |
--------------------------------------------------------------------------------
/komf-core/src/commonMain/kotlin/snd/komf/ktor/RateLimiterPlugin.kt:
--------------------------------------------------------------------------------
1 | package snd.komf.ktor
2 |
3 | import io.ktor.client.plugins.api.*
4 | import kotlin.time.Duration
5 | import kotlin.time.Duration.Companion.minutes
6 |
7 | class RateLimiterPluginConfig {
8 | var eventsPerInterval: Int = 60
9 | var interval: Duration = 1.minutes
10 | var allowBurst = true
11 | var preconfigured: ThroughputLimiter? = null
12 | }
13 |
14 | val HttpRequestRateLimiter = createClientPlugin("RateLimiter", ::RateLimiterPluginConfig) {
15 |
16 | val limiter = pluginConfig.preconfigured
17 | ?: if (pluginConfig.allowBurst) {
18 | intervalLimiter(pluginConfig.eventsPerInterval, pluginConfig.interval)
19 | } else {
20 | rateLimiter(pluginConfig.eventsPerInterval, pluginConfig.interval)
21 | }
22 | onRequest { _, _ -> limiter.acquire() }
23 | }
24 |
--------------------------------------------------------------------------------
/komf-core/src/commonMain/kotlin/snd/komf/ktor/UserAgent.kt:
--------------------------------------------------------------------------------
1 | package snd.komf.ktor
2 |
3 | const val komfUserAgent = "Snd-R/komf (https://github.com/Snd-R/komf)"
4 |
--------------------------------------------------------------------------------
/komf-core/src/commonMain/kotlin/snd/komf/model/Author.kt:
--------------------------------------------------------------------------------
1 | package snd.komf.model
2 |
3 | import kotlinx.serialization.Serializable
4 |
5 | @Serializable
6 | data class Author(
7 | val name: String,
8 | val role: AuthorRole
9 | )
10 | enum class AuthorRole {
11 | WRITER,
12 | PENCILLER,
13 | INKER,
14 | COLORIST,
15 | LETTERER,
16 | COVER,
17 | EDITOR,
18 | TRANSLATOR
19 | }
20 |
--------------------------------------------------------------------------------
/komf-core/src/commonMain/kotlin/snd/komf/model/BookMetadata.kt:
--------------------------------------------------------------------------------
1 | package snd.komf.model
2 |
3 | import kotlinx.datetime.LocalDate
4 | import kotlinx.serialization.Serializable
5 | import kotlin.jvm.JvmInline
6 | import kotlin.math.floor
7 |
8 | @JvmInline
9 | @Serializable
10 | value class ProviderBookId(val id: String)
11 |
12 | @Serializable
13 | data class BookMetadata(
14 | val title: String? = null,
15 | val summary: String? = null,
16 | val number: BookRange? = null,
17 | val numberSort: Double? = null,
18 | val releaseDate: LocalDate? = null,
19 | val authors: List = emptyList(),
20 | val tags: Set = emptySet(),
21 | val isbn: String? = null,
22 | val links: List = emptyList(),
23 | val chapters: Collection = emptyList(),
24 | val storyArcs: List? = null,
25 |
26 | val startChapter: Int? = null,
27 | val endChapter: Int? = null,
28 |
29 | val thumbnail: Image? = null,
30 | )
31 |
32 | @Serializable
33 | data class BookStoryArc(val name: String, val number: Int)
34 |
35 | @Serializable
36 | data class Chapter(
37 | val name: String?,
38 | val number: Int
39 | )
40 |
41 | @Serializable
42 | data class BookRange(
43 | val start: Double,
44 | val end: Double
45 | ) {
46 | constructor(start: Double) : this(start, start)
47 |
48 | constructor(start: Int) : this(start.toDouble(), start.toDouble())
49 |
50 | override fun toString(): String {
51 | val start = if (floor(start) == start) start.toInt() else start
52 | val end = if (floor(end) == end) end.toInt() else end
53 | return if (start == end) {
54 | start.toString()
55 | } else "$start-$end"
56 | }
57 | }
58 |
59 | @Serializable
60 | data class ProviderBookMetadata(
61 | val id: ProviderBookId? = null,
62 | val seriesId: ProviderSeriesId? = null,
63 | val metadata: BookMetadata,
64 | )
65 |
66 |
--------------------------------------------------------------------------------
/komf-core/src/commonMain/kotlin/snd/komf/model/Image.kt:
--------------------------------------------------------------------------------
1 | package snd.komf.model
2 |
3 | import kotlinx.serialization.Serializable
4 | import kotlinx.serialization.Transient
5 |
6 | @Serializable
7 | data class Image(
8 | @Transient
9 | val bytes: ByteArray = byteArrayOf(),
10 | val mimeType: String? = null
11 | ) {
12 |
13 | override fun hashCode(): Int {
14 | return bytes.contentHashCode()
15 | }
16 |
17 | override fun equals(other: Any?): Boolean {
18 | if (this === other) return true
19 | if (other == null || this::class != other::class) return false
20 |
21 | other as Image
22 |
23 | if (!bytes.contentEquals(other.bytes)) return false
24 | if (mimeType != other.mimeType) return false
25 |
26 | return true
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/komf-core/src/commonMain/kotlin/snd/komf/model/MatchQuery.kt:
--------------------------------------------------------------------------------
1 | package snd.komf.model
2 |
3 | data class MatchQuery(
4 | val seriesName: String,
5 | val startYear: Int?,
6 | val bookQualifier: BookQualifier?,
7 | )
8 |
9 | data class BookQualifier(
10 | val name: String,
11 | val number: BookRange,
12 | val cover: Image?
13 | )
--------------------------------------------------------------------------------
/komf-core/src/commonMain/kotlin/snd/komf/model/MatchType.kt:
--------------------------------------------------------------------------------
1 | package snd.komf.model
2 |
3 | enum class MatchType {
4 | MANUAL,
5 | AUTOMATIC
6 | }
--------------------------------------------------------------------------------
/komf-core/src/commonMain/kotlin/snd/komf/model/MediaType.kt:
--------------------------------------------------------------------------------
1 | package snd.komf.model
2 |
3 | enum class MediaType {
4 | MANGA,
5 | NOVEL,
6 | COMIC,
7 | }
--------------------------------------------------------------------------------
/komf-core/src/commonMain/kotlin/snd/komf/model/SeriesMetadata.kt:
--------------------------------------------------------------------------------
1 | package snd.komf.model
2 |
3 | import kotlinx.datetime.LocalDate
4 | import kotlinx.serialization.Serializable
5 | import kotlin.jvm.JvmInline
6 |
7 | @JvmInline
8 | @Serializable
9 | value class ProviderSeriesId(val value: String) {
10 | override fun toString() = value
11 | }
12 |
13 | @Serializable
14 | data class SeriesMetadata(
15 | val status: SeriesStatus? = null,
16 | val title: SeriesTitle? = null,
17 | val titles: Collection = emptyList(),
18 | val summary: String? = null,
19 | val publisher: Publisher? = null,
20 | val alternativePublishers: Set = emptySet(),
21 | val readingDirection: ReadingDirection? = null,
22 | val ageRating: Int? = null,
23 | val language: String? = null,
24 | val genres: Collection = emptyList(),
25 | val tags: Collection = emptyList(),
26 | val totalBookCount: Int? = null,
27 | val authors: List = emptyList(),
28 | val releaseDate: ReleaseDate? = null,
29 | val links: Collection = emptyList(),
30 | val score: Double? = null,
31 |
32 | val thumbnail: Image? = null,
33 | )
34 |
35 | @Serializable
36 | data class SeriesTitle(
37 | val name: String,
38 | val type: TitleType?,
39 | val language: String?,
40 | )
41 |
42 | @Serializable
43 | data class ProviderSeriesMetadata(
44 | val id: ProviderSeriesId,
45 | val metadata: SeriesMetadata,
46 | val books: List = emptyList(),
47 | )
48 |
49 | @Serializable
50 | data class SeriesBook(
51 | val id: ProviderBookId,
52 | val number: BookRange?,
53 | val name: String?,
54 | val type: String?,
55 | val edition: String?
56 | )
57 |
58 | @Serializable
59 | data class ReleaseDate(
60 | val year: Int?,
61 | val month: Int?,
62 | val day: Int?
63 | )
64 |
65 | enum class TitleType(val label: String) {
66 | ROMAJI("Romaji"),
67 | LOCALIZED("Localized"),
68 | NATIVE("Native"),
69 | }
70 |
71 | enum class ReadingDirection {
72 | LEFT_TO_RIGHT, RIGHT_TO_LEFT, VERTICAL, WEBTOON
73 | }
74 |
75 | enum class SeriesStatus {
76 | ENDED,
77 | ONGOING,
78 | ABANDONED,
79 | HIATUS,
80 | COMPLETED
81 | }
82 |
83 | @Serializable
84 | data class Publisher(
85 | val name: String,
86 | val type: PublisherType? = null,
87 | val languageTag: String? = null
88 | )
89 |
90 | enum class PublisherType {
91 | ORIGINAL,
92 | LOCALIZED
93 | }
94 |
95 | fun LocalDate.toReleaseDate() = ReleaseDate(year = year, month = monthNumber, day = dayOfMonth)
96 |
97 |
--------------------------------------------------------------------------------
/komf-core/src/commonMain/kotlin/snd/komf/model/SeriesSearchResult.kt:
--------------------------------------------------------------------------------
1 | package snd.komf.model
2 |
3 | import kotlinx.serialization.Serializable
4 | import snd.komf.providers.CoreProviders
5 |
6 | @Serializable
7 | data class SeriesSearchResult(
8 | val url: String?,
9 | val imageUrl: String? = null,
10 | val title: String,
11 | val provider: CoreProviders,
12 | val resultId: String,
13 | )
14 |
--------------------------------------------------------------------------------
/komf-core/src/commonMain/kotlin/snd/komf/model/UpdateMode.kt:
--------------------------------------------------------------------------------
1 | package snd.komf.model
2 |
3 | enum class UpdateMode {
4 | API,
5 | COMIC_INFO,
6 | // OPF,
7 | }
8 |
--------------------------------------------------------------------------------
/komf-core/src/commonMain/kotlin/snd/komf/model/WebLink.kt:
--------------------------------------------------------------------------------
1 | package snd.komf.model
2 |
3 | import kotlinx.serialization.Serializable
4 |
5 | @Serializable
6 | data class WebLink(
7 | val label: String,
8 | val url: String,
9 | )
10 |
--------------------------------------------------------------------------------
/komf-core/src/commonMain/kotlin/snd/komf/providers/CoreProviders.kt:
--------------------------------------------------------------------------------
1 | package snd.komf.providers
2 |
3 | enum class CoreProviders {
4 | ANILIST,
5 | BANGUMI,
6 | BOOK_WALKER,
7 | COMIC_VINE,
8 | HENTAG,
9 | KODANSHA,
10 | MAL,
11 | MANGA_UPDATES,
12 | MANGADEX,
13 | MANGA_BAKA,
14 | NAUTILJON,
15 | YEN_PRESS,
16 | VIZ,
17 | }
18 |
--------------------------------------------------------------------------------
/komf-core/src/commonMain/kotlin/snd/komf/providers/MetadataProvider.kt:
--------------------------------------------------------------------------------
1 | package snd.komf.providers
2 |
3 | import snd.komf.model.Image
4 | import snd.komf.model.MatchQuery
5 | import snd.komf.model.ProviderBookId
6 | import snd.komf.model.ProviderBookMetadata
7 | import snd.komf.model.ProviderSeriesId
8 | import snd.komf.model.ProviderSeriesMetadata
9 | import snd.komf.model.SeriesSearchResult
10 |
11 | interface MetadataProvider {
12 | fun providerName(): CoreProviders
13 |
14 | suspend fun getSeriesMetadata(seriesId: ProviderSeriesId): ProviderSeriesMetadata
15 |
16 | suspend fun getSeriesCover(seriesId: ProviderSeriesId): Image?
17 |
18 | suspend fun getBookMetadata(seriesId: ProviderSeriesId, bookId: ProviderBookId): ProviderBookMetadata
19 |
20 | suspend fun searchSeries(seriesName: String, limit: Int = 5): Collection
21 |
22 | suspend fun matchSeriesMetadata(matchQuery: MatchQuery): ProviderSeriesMetadata?
23 | }
24 |
25 |
--------------------------------------------------------------------------------
/komf-core/src/commonMain/kotlin/snd/komf/providers/anilist/AniListMetadataProvider.kt:
--------------------------------------------------------------------------------
1 | package snd.komf.providers.anilist
2 |
3 | import snd.komf.model.Image
4 | import snd.komf.model.MatchQuery
5 | import snd.komf.model.MediaType
6 | import snd.komf.model.ProviderBookId
7 | import snd.komf.model.ProviderBookMetadata
8 | import snd.komf.model.ProviderSeriesId
9 | import snd.komf.model.ProviderSeriesMetadata
10 | import snd.komf.model.SeriesSearchResult
11 | import snd.komf.providers.MetadataProvider
12 | import snd.komf.providers.CoreProviders.ANILIST
13 | import snd.komf.providers.anilist.model.AniListMediaFormat
14 | import snd.komf.util.NameSimilarityMatcher
15 |
16 | private val mangaMediaFormats = listOf(AniListMediaFormat.MANGA, AniListMediaFormat.ONE_SHOT)
17 | private val novelMediaFormats = listOf(AniListMediaFormat.NOVEL)
18 |
19 | class AniListMetadataProvider(
20 | private val client: AniListClient,
21 | private val metadataMapper: AniListMetadataMapper,
22 | private val nameMatcher: NameSimilarityMatcher,
23 | private val fetchSeriesCovers: Boolean,
24 | mediaType: MediaType,
25 | ) : MetadataProvider {
26 | private val seriesFormats = when (mediaType) {
27 | MediaType.MANGA -> mangaMediaFormats
28 | MediaType.NOVEL -> novelMediaFormats
29 | MediaType.COMIC -> throw IllegalStateException("Comics media type is not supported")
30 | }
31 |
32 | override fun providerName() = ANILIST
33 |
34 | override suspend fun getSeriesMetadata(seriesId: ProviderSeriesId): ProviderSeriesMetadata {
35 | val series = client.getMedia(seriesId.value.toInt())
36 | val thumbnail = if (fetchSeriesCovers) client.getThumbnail(series) else null
37 | return metadataMapper.toSeriesMetadata(series, thumbnail)
38 | }
39 |
40 | override suspend fun getSeriesCover(seriesId: ProviderSeriesId): Image? {
41 | val series = client.getMedia(seriesId.value.toInt())
42 | return client.getThumbnail(series)
43 | }
44 |
45 | override suspend fun getBookMetadata(seriesId: ProviderSeriesId, bookId: ProviderBookId): ProviderBookMetadata {
46 | throw UnsupportedOperationException()
47 | }
48 |
49 | override suspend fun searchSeries(seriesName: String, limit: Int): Collection {
50 | val searchResults = client.search(seriesName.take(400), seriesFormats, limit)
51 | return searchResults.map { metadataMapper.toSearchResult(it) }
52 | }
53 |
54 | override suspend fun matchSeriesMetadata(matchQuery: MatchQuery): ProviderSeriesMetadata? {
55 | val seriesName = matchQuery.seriesName
56 | val searchResults = client.search(seriesName.take(400), seriesFormats)
57 |
58 | val match = searchResults.firstOrNull {
59 | val titles = listOfNotNull(
60 | it.title?.english,
61 | it.title?.romaji,
62 | it.title?.native
63 | )
64 |
65 | nameMatcher.matches(seriesName, titles)
66 | }
67 |
68 | return match?.let {
69 | val thumbnail = if (fetchSeriesCovers) client.getThumbnail(it) else null
70 | metadataMapper.toSeriesMetadata(it, thumbnail)
71 | }
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/komf-core/src/commonMain/kotlin/snd/komf/providers/anilist/model/AniListMedia.kt:
--------------------------------------------------------------------------------
1 | package snd.komf.providers.anilist.model
2 |
3 | import kotlinx.serialization.Serializable
4 |
5 | @Serializable
6 | data class AniListMedia(
7 | val id: Int,
8 | val title: AniListTitle? = null,
9 | val type: AniListMediaFormat? = null,
10 | val format: AniListMediaFormat? = null,
11 | val status: AniListMediaStatus? = null,
12 | val description: String? = null,
13 | val chapters: Int? = null,
14 | val volumes: Int? = null,
15 | val coverImage: AniListMediaCoverImage? = null,
16 | val startDate: AniListFuzzyDate? = null,
17 | val genres: List? = null,
18 | val synonyms: List? = null,
19 | val tags: List? = null,
20 | val staff: AniListStaffConnection? = null,
21 | val meanScore: Int? = null,
22 |
23 | )
24 |
25 | @Serializable
26 | data class AniListTitle(
27 | val romaji: String? = null,
28 | val english: String? = null,
29 | val native: String? = null,
30 | )
31 |
32 | @Serializable
33 | enum class AniListMediaFormat {
34 | TV,
35 | TV_SHORT,
36 | MOVIE,
37 | SPECIAL,
38 | OVA,
39 | ONA,
40 | MUSIC,
41 | MANGA,
42 | NOVEL,
43 | ONE_SHOT,
44 | }
45 |
46 | @Serializable
47 | enum class AniListMediaType {
48 | MANGA,
49 | ANIME,
50 | }
51 |
52 |
53 | @Serializable
54 | enum class AniListMediaStatus {
55 | FINISHED,
56 | RELEASING,
57 | NOT_YET_RELEASED,
58 | CANCELLED,
59 | HIATUS
60 | }
61 |
62 | @Serializable
63 | data class AniListMediaCoverImage(
64 | val extraLarge: String? = null,
65 | val large: String? = null,
66 | val medium: String? = null,
67 | val color: String? = null,
68 | )
69 |
70 | @Serializable
71 | data class AniListFuzzyDate(
72 | val year: Int? = null,
73 | val month: Int? = null,
74 | val day: Int? = null
75 | )
76 |
77 | @Serializable
78 | data class AniListMediaTag(
79 | val name: String,
80 | val description: String? = null,
81 | val category: String? = null,
82 | val rank: Int? = null
83 | )
84 |
85 | @Serializable
86 | data class AniListStaffConnection(
87 | val edges: List
88 | )
89 |
90 | @Serializable
91 | data class AniListStaffEdge(
92 | val node: AniListStaff? = null,
93 | val role: String? = null,
94 | )
95 |
96 | @Serializable
97 | data class AniListStaff(
98 | val name: AniListStaffName? = null,
99 | val languageV2: String? = null
100 | )
101 |
102 | @Serializable
103 | data class AniListStaffName(
104 | val full: String? = null
105 | )
--------------------------------------------------------------------------------
/komf-core/src/commonMain/kotlin/snd/komf/providers/anilist/model/AniListQuery.kt:
--------------------------------------------------------------------------------
1 | package snd.komf.providers.anilist.model
2 |
3 | import kotlinx.serialization.Serializable
4 | import snd.komf.providers.anilist.model.AniListMediaFormat
5 |
6 | @Serializable
7 | data class AniListSearchQuery(
8 | val search: String,
9 | val formats: List,
10 | val perPage: Int
11 | )
12 |
13 | @Serializable
14 | data class AniListMediaQuery(
15 | val id: Int
16 | )
--------------------------------------------------------------------------------
/komf-core/src/commonMain/kotlin/snd/komf/providers/anilist/model/AniListResponse.kt:
--------------------------------------------------------------------------------
1 | package snd.komf.providers.anilist.model
2 |
3 | import kotlinx.serialization.Serializable
4 |
5 | @Serializable
6 | data class AniListResponse(
7 | val data: T
8 | )
9 |
10 | @Serializable
11 | data class AniListMediaSearchResponse(
12 | val mediaSearch: AniListMediaSearchMedia
13 | )
14 |
15 | @Serializable
16 | data class AniListMediaSearchMedia(
17 | val media: List
18 | )
19 |
20 | @Serializable
21 | data class AniListMediaResponse(
22 | val media: AniListMedia
23 | )
24 |
--------------------------------------------------------------------------------
/komf-core/src/commonMain/kotlin/snd/komf/providers/bangumi/BangumiClient.kt:
--------------------------------------------------------------------------------
1 | package snd.komf.providers.bangumi
2 |
3 | import io.ktor.client.*
4 | import io.ktor.client.call.*
5 | import io.ktor.client.request.*
6 | import io.ktor.http.*
7 | import kotlinx.serialization.json.add
8 | import kotlinx.serialization.json.buildJsonObject
9 | import kotlinx.serialization.json.put
10 | import kotlinx.serialization.json.putJsonArray
11 | import snd.komf.model.Image
12 | import snd.komf.providers.bangumi.model.BangumiSubject
13 | import snd.komf.providers.bangumi.model.SearchSubjectsResponse
14 | import snd.komf.providers.bangumi.model.SubjectRelation
15 | import snd.komf.providers.bangumi.model.SubjectType
16 |
17 | class BangumiClient(
18 | private val ktor: HttpClient,
19 | ) {
20 | private val apiV0Url = "https://api.bgm.tv/v0"
21 |
22 | suspend fun searchSeries(
23 | keyword: String,
24 | ): SearchSubjectsResponse {
25 | return ktor.post("$apiV0Url/search/subjects") {
26 | contentType(ContentType.Application.Json)
27 | setBody(
28 | buildJsonObject {
29 | put("keyword", keyword)
30 | put("filter", buildJsonObject {
31 | putJsonArray("type") { add(SubjectType.BOOK.value) }
32 | put("nsfw", true) // include NSFW content
33 | })
34 | }
35 | )
36 |
37 | }.body()
38 | }
39 |
40 | suspend fun getSubject(subjectId: Long): BangumiSubject {
41 | return ktor.get("$apiV0Url/subjects/$subjectId") {
42 | }.body()
43 | }
44 |
45 | suspend fun getSubjectRelations(subjectId: Long): Collection {
46 | return ktor.get("$apiV0Url/subjects/$subjectId/subjects") {
47 | }.body()
48 | }
49 |
50 | suspend fun getThumbnail(subject: BangumiSubject): Image? {
51 | return (subject.images.common ?: subject.images.medium)?.ifBlank { null }?.let {
52 | val bytes: ByteArray = ktor.get(it) {
53 | }.body()
54 | Image(bytes)
55 | }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/komf-core/src/commonMain/kotlin/snd/komf/providers/bangumi/model/SubjectSearch.kt:
--------------------------------------------------------------------------------
1 | package snd.komf.providers.bangumi.model
2 |
3 | import kotlinx.serialization.SerialName
4 | import kotlinx.serialization.Serializable
5 |
6 | @Serializable
7 | data class SearchSubjectsResponse(
8 | val total: Int? = null,
9 | val limit: Int? = null,
10 | val offset: Int? = null,
11 | val data: List
12 | )
13 |
14 | @Serializable
15 | data class SubjectSearchData(
16 | val id: Long,
17 | val image: String? = null,
18 | val summary: String? = null,
19 | val name: String,
20 |
21 | @SerialName("name_cn")
22 | val nameCn: String? = null,
23 | val tags: List = emptyList(),
24 | val rating: SubjectRating? = null,
25 | val type: SubjectType? = null
26 | )
27 |
--------------------------------------------------------------------------------
/komf-core/src/commonMain/kotlin/snd/komf/providers/bookwalker/BookWalkerClient.kt:
--------------------------------------------------------------------------------
1 | package snd.komf.providers.bookwalker
2 |
3 | import io.ktor.client.*
4 | import io.ktor.client.call.*
5 | import io.ktor.client.plugins.*
6 | import io.ktor.client.request.*
7 | import io.ktor.client.statement.*
8 | import io.ktor.http.*
9 | import snd.komf.model.Image
10 | import snd.komf.providers.bookwalker.model.BookWalkerBook
11 | import snd.komf.providers.bookwalker.model.BookWalkerBookId
12 | import snd.komf.providers.bookwalker.model.BookWalkerBookListPage
13 | import snd.komf.providers.bookwalker.model.BookWalkerCategory
14 | import snd.komf.providers.bookwalker.model.BookWalkerSearchResult
15 | import snd.komf.providers.bookwalker.model.BookWalkerSeriesId
16 |
17 | const val bookWalkerBaseUrl = "https://global.bookwalker.jp"
18 |
19 | class BookWalkerClient(
20 | private val ktor: HttpClient
21 | ) {
22 | private val parser = BookWalkerParser()
23 |
24 | suspend fun searchSeries(name: String, category: BookWalkerCategory): Collection {
25 |
26 | return try {
27 | val document = ktor.get("$bookWalkerBaseUrl/search/") {
28 | parameter("word", name)
29 | parameter("qcat", category.number)
30 | parameter("np", 0)
31 | }.bodyAsText()
32 | parser.parseSearchResults(document)
33 | } catch (e: ClientRequestException) {
34 | if (e.response.status == HttpStatusCode.NotFound) emptyList()
35 | else throw e
36 | }
37 |
38 | }
39 |
40 | suspend fun getSeriesBooks(id: BookWalkerSeriesId, page: Int): BookWalkerBookListPage {
41 | val document = ktor.get("$bookWalkerBaseUrl/series/${id.id}/") {
42 | parameter("page", page)
43 | }.bodyAsText()
44 | return parser.parseSeriesBooks(document)
45 | }
46 |
47 | suspend fun getBook(id: BookWalkerBookId): BookWalkerBook {
48 | val document = ktor.get("$bookWalkerBaseUrl/${id.id}/").bodyAsText()
49 | return parser.parseBook(document)
50 | }
51 |
52 | suspend fun getThumbnail(url: String?): Image? {
53 | return url?.let {
54 | val bytes: ByteArray = ktor.get(it).body()
55 | Image(bytes)
56 | }
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/komf-core/src/commonMain/kotlin/snd/komf/providers/bookwalker/model/BookWalkerBook.kt:
--------------------------------------------------------------------------------
1 | package snd.komf.providers.bookwalker.model
2 |
3 | import kotlinx.datetime.LocalDate
4 | import snd.komf.model.BookRange
5 | import kotlin.jvm.JvmInline
6 |
7 | @JvmInline
8 | value class BookWalkerBookId(val id: String)
9 |
10 | data class BookWalkerBook(
11 | val id: BookWalkerBookId,
12 | val seriesId: BookWalkerSeriesId?,
13 | val name: String,
14 | val number: BookRange?,
15 |
16 | val seriesTitle: String?,
17 | val japaneseTitle: String?,
18 | val romajiTitle: String?,
19 | val artists: Collection,
20 | val authors: Collection,
21 | val publisher: String,
22 | val genres: Collection,
23 | val availableSince: LocalDate?,
24 |
25 | val synopsis: String?,
26 | val imageUrl: String?,
27 | )
28 |
29 |
--------------------------------------------------------------------------------
/komf-core/src/commonMain/kotlin/snd/komf/providers/bookwalker/model/BookWalkerBookListPage.kt:
--------------------------------------------------------------------------------
1 | package snd.komf.providers.bookwalker.model
2 |
3 | import snd.komf.model.BookRange
4 |
5 | data class BookWalkerBookListPage(
6 | val page: Int,
7 | val totalPages: Int,
8 | val books: Collection
9 | )
10 |
11 | data class BookWalkerSeriesBook(
12 | val id: BookWalkerBookId,
13 | val number: BookRange?,
14 | val name: String
15 | )
16 |
--------------------------------------------------------------------------------
/komf-core/src/commonMain/kotlin/snd/komf/providers/bookwalker/model/BookWalkerCategory.kt:
--------------------------------------------------------------------------------
1 | package snd.komf.providers.bookwalker.model
2 |
3 | enum class BookWalkerCategory(val number: Int) {
4 | MANGA(2),
5 | LIGHT_NOVELS(3),
6 | AUDIO_BOOKS(401),
7 | BOOKSHELF_SKIN(101),
8 | ART_BOOK(7),
9 | INTL_MANGA(11),
10 | PRACTICAL(4),
11 | FICTION(1)
12 | }
--------------------------------------------------------------------------------
/komf-core/src/commonMain/kotlin/snd/komf/providers/bookwalker/model/BookWalkerSearchResult.kt:
--------------------------------------------------------------------------------
1 | package snd.komf.providers.bookwalker.model
2 |
3 | import kotlin.jvm.JvmInline
4 |
5 | @JvmInline
6 | value class BookWalkerSeriesId(val id: String)
7 |
8 | data class BookWalkerSearchResult(
9 | val seriesId: BookWalkerSeriesId?,
10 | val bookId: BookWalkerBookId?,
11 | val seriesName: String,
12 | val imageUrl: String?,
13 | )
14 |
15 |
--------------------------------------------------------------------------------
/komf-core/src/commonMain/kotlin/snd/komf/providers/comicvine/ComicVineClient.kt:
--------------------------------------------------------------------------------
1 | package snd.komf.providers.comicvine
2 |
3 | import io.ktor.client.*
4 | import io.ktor.client.call.*
5 | import io.ktor.client.request.*
6 | import snd.komf.model.Image
7 | import snd.komf.providers.comicvine.ComicVineClient.ComicVineTypeId.ISSUE
8 | import snd.komf.providers.comicvine.ComicVineClient.ComicVineTypeId.VOLUME
9 | import snd.komf.providers.comicvine.model.ComicVineIssue
10 | import snd.komf.providers.comicvine.model.ComicVineIssueId
11 | import snd.komf.providers.comicvine.model.ComicVineSearchResult
12 | import snd.komf.providers.comicvine.model.ComicVineStoryArc
13 | import snd.komf.providers.comicvine.model.ComicVineStoryArcId
14 | import snd.komf.providers.comicvine.model.ComicVineVolume
15 | import snd.komf.providers.comicvine.model.ComicVineVolumeId
16 | import snd.komf.providers.comicvine.model.ComicVineVolumeSearch
17 |
18 | private const val baseUrl = "https://comicvine.gamespot.com/api"
19 |
20 | class ComicVineClient(
21 | private val ktor: HttpClient,
22 | private val apiKey: String,
23 | private val rateLimiter: ComicVineRateLimiter,
24 | ) {
25 |
26 | suspend fun searchVolume(name: String): ComicVineSearchResult> {
27 | rateLimiter.searchAcquire()
28 | return ktor.get("$baseUrl/search/") {
29 | parameter("query", name)
30 | parameter("format", "json")
31 | parameter("resources", "volume")
32 | parameter("api_key", apiKey)
33 | }.body()
34 | }
35 |
36 | suspend fun getVolume(id: ComicVineVolumeId): ComicVineSearchResult {
37 | rateLimiter.volumeAcquire()
38 | return ktor.get("$baseUrl/volume/${VOLUME.id}-${id.value}/") {
39 | parameter("format", "json")
40 | parameter("api_key", apiKey)
41 | }.body()
42 | }
43 |
44 | suspend fun getIssue(id: ComicVineIssueId): ComicVineSearchResult {
45 | rateLimiter.issueAcquire()
46 | return ktor.get("$baseUrl/issue/${ISSUE.id}-${id.value}/") {
47 | parameter("format", "json")
48 | parameter("api_key", apiKey)
49 | }.body()
50 | }
51 |
52 | suspend fun getStoryArc(id: ComicVineStoryArcId): ComicVineSearchResult {
53 | rateLimiter.storyArcAcquire()
54 | return ktor.get("$baseUrl/story_arc/${ComicVineTypeId.STORY_ARC.id}-${id.value}/") {
55 | parameter("format", "json")
56 | parameter("api_key", apiKey)
57 | }.body()
58 |
59 | }
60 |
61 | suspend fun getCover(url: String): Image {
62 | rateLimiter.coverAcquire()
63 | val bytes: ByteArray = ktor.get(url).body()
64 | return Image(bytes)
65 | }
66 |
67 | private enum class ComicVineTypeId(val id: Int) {
68 | VOLUME(4050),
69 | ISSUE(4000),
70 | STORY_ARC(4045)
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/komf-core/src/commonMain/kotlin/snd/komf/providers/comicvine/ComicVineRateLimiter.kt:
--------------------------------------------------------------------------------
1 | package snd.komf.providers.comicvine
2 |
3 | import snd.komf.ktor.intervalLimiter
4 | import snd.komf.ktor.rateLimiter
5 | import kotlin.time.Duration.Companion.minutes
6 |
7 | class ComicVineRateLimiter {
8 | private val searchLimiter = LimiterInternal()
9 | private val volumeLimiter = LimiterInternal()
10 | private val issueLimiter = LimiterInternal()
11 | private val storyArcLimiter = LimiterInternal()
12 | private val coverLimiter = LimiterInternal()
13 |
14 | suspend fun searchAcquire() = searchLimiter.acquire()
15 | suspend fun volumeAcquire() = volumeLimiter.acquire()
16 | suspend fun issueAcquire() = issueLimiter.acquire()
17 | suspend fun storyArcAcquire() = storyArcLimiter.acquire()
18 | suspend fun coverAcquire() = coverLimiter.acquire()
19 |
20 | private class LimiterInternal {
21 | private val burstingLimiter = intervalLimiter(50, 60.minutes)
22 | private val regularLimiter = rateLimiter(144, 60.minutes)
23 |
24 | suspend fun acquire() {
25 | if (!burstingLimiter.tryAcquire()) {
26 | regularLimiter.acquire()
27 | }
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/komf-core/src/commonMain/kotlin/snd/komf/providers/comicvine/model/ComicVineCredit.kt:
--------------------------------------------------------------------------------
1 | package snd.komf.providers.comicvine.model
2 |
3 | import kotlinx.serialization.SerialName
4 | import kotlinx.serialization.Serializable
5 |
6 | @Serializable
7 | data class ComicVineCredit(
8 | val id: Int,
9 | val name: String,
10 | @SerialName("api_detail_url")
11 | val apiDetailUrl: String,
12 | @SerialName("site_detail_url")
13 | val siteDetailUrl: String,
14 | )
15 |
16 |
--------------------------------------------------------------------------------
/komf-core/src/commonMain/kotlin/snd/komf/providers/comicvine/model/ComicVineImage.kt:
--------------------------------------------------------------------------------
1 | package snd.komf.providers.comicvine.model
2 |
3 | import kotlinx.serialization.SerialName
4 | import kotlinx.serialization.Serializable
5 |
6 | @Serializable()
7 | data class ComicVineImage(
8 | @SerialName("icon_url")
9 | val iconUrl: String? = null,
10 | @SerialName("medium_url")
11 | val mediumUrl: String? = null,
12 | @SerialName("screen_url")
13 | val screenUrl: String? = null,
14 | @SerialName("screen_large_url")
15 | val screenLargeUrl: String? = null,
16 | @SerialName("small_url")
17 | val smallUrl: String? = null,
18 | @SerialName("super_url")
19 | val superUrl: String? = null,
20 | @SerialName("thumb_url")
21 | val thumbUrl: String? = null,
22 | @SerialName("tiny_url")
23 | val tinyUrl: String? = null,
24 | @SerialName("original_url")
25 | val originalUrl: String? = null,
26 | @SerialName("image_tags")
27 | val imageTags: String? = null,
28 | )
29 |
30 |
--------------------------------------------------------------------------------
/komf-core/src/commonMain/kotlin/snd/komf/providers/comicvine/model/ComicVineIssue.kt:
--------------------------------------------------------------------------------
1 | package snd.komf.providers.comicvine.model
2 |
3 | import kotlinx.serialization.SerialName
4 | import kotlinx.serialization.Serializable
5 | import snd.komf.model.ProviderBookId
6 | import kotlin.jvm.JvmInline
7 |
8 | @JvmInline
9 | @Serializable
10 | value class ComicVineIssueId(val value: Int) {
11 | override fun toString() = value.toString()
12 | }
13 |
14 | fun ProviderBookId.toComicVineIssueId() = ComicVineIssueId(id.toInt())
15 |
16 | @Serializable
17 | data class ComicVineIssue(
18 | val id: ComicVineIssueId,
19 | val name: String? = null,
20 | @SerialName("api_detail_url")
21 | val apiDetailUrl: String,
22 | @SerialName("site_detail_url")
23 | val siteDetailUrl: String,
24 |
25 | val aliases: String? = null,
26 | @SerialName("associated_images")
27 | val associatedImages: List? = null,
28 | @SerialName("character_credits")
29 | val characterCredits: List? = null,
30 | @SerialName("concept_credits")
31 | val conceptCredits: List? = null,
32 | @SerialName("cover_date")
33 | val coverDate: String? = null,
34 | @SerialName("date_added")
35 | val dateAdded: String? = null,
36 | @SerialName("date_last_updated")
37 | val dateLastUpdated: String? = null,
38 | val description: String? = null,
39 | val image: ComicVineImage? = null,
40 | @SerialName("issue_number")
41 | val issueNumber: String? = null,
42 | @SerialName("location_credits")
43 | val locationCredits: List? = null,
44 | @SerialName("object_credits")
45 | val objectCredits: List? = null,
46 | @SerialName("person_credits")
47 | val personCredits: List? = null,
48 | @SerialName("store_date")
49 | val storeDate: String? = null,
50 | @SerialName("story_arc_credits")
51 | val storyArcCredits: List? = null,
52 |
53 | @SerialName("team_credits")
54 | val teamCredits: List? = null,
55 | val volume: ComicVineVolume? = null,
56 | )
57 |
58 | @Serializable
59 | data class ComicVineIssueSlim(
60 | val id: Int,
61 | val name: String? = null,
62 |
63 | @SerialName("api_detail_url")
64 | val apiDetailUrl: String,
65 | @SerialName("site_detail_url")
66 | val siteDetailUrl: String? = null,
67 |
68 | @SerialName("issue_number")
69 | val issueNumber: String? = null,
70 | )
71 |
72 | @Serializable
73 | data class ComicVinePersonCredit(
74 | val id: Int,
75 | val name: String,
76 | @SerialName("api_detail_url")
77 | val apiDetailUrl: String,
78 | @SerialName("site_detail_url")
79 | val siteDetailUrl: String,
80 | val role: String,
81 | )
82 |
83 | @Serializable
84 | data class ComicVineAltImage(
85 | val id: Int,
86 | @SerialName("original_url")
87 | val originalUrl: String? = null,
88 | val caption: String? = null,
89 | @SerialName("image_tags")
90 | val imageTags: String? = null,
91 | )
92 |
93 |
--------------------------------------------------------------------------------
/komf-core/src/commonMain/kotlin/snd/komf/providers/comicvine/model/ComicVineSearchResult.kt:
--------------------------------------------------------------------------------
1 | package snd.komf.providers.comicvine.model
2 |
3 | import kotlinx.serialization.SerialName
4 | import kotlinx.serialization.Serializable
5 |
6 | @Serializable()
7 | class ComicVineSearchResult(
8 | val error: String,
9 | val limit: Int,
10 | val offset: Int,
11 | @SerialName("number_of_page_results")
12 | val numberOfPageResults: Int,
13 | @SerialName("number_of_total_results")
14 | val numberOfTotalResults: Int,
15 | @SerialName("status_code")
16 | val statusCode: Int,
17 | val results: T,
18 | val version: String,
19 | )
20 |
21 |
--------------------------------------------------------------------------------
/komf-core/src/commonMain/kotlin/snd/komf/providers/comicvine/model/ComicVineStoryArc.kt:
--------------------------------------------------------------------------------
1 | package snd.komf.providers.comicvine.model
2 |
3 | import kotlinx.serialization.SerialName
4 | import kotlinx.serialization.Serializable
5 | import kotlin.jvm.JvmInline
6 |
7 | @JvmInline
8 | @Serializable
9 | value class ComicVineStoryArcId(val value: Int) {
10 | override fun toString() = value.toString()
11 | }
12 |
13 | @Serializable
14 | data class ComicVineStoryArc(
15 | val id: ComicVineStoryArcId,
16 | val name: String,
17 | val issues: List,
18 |
19 | val aliases: String? = null,
20 | @SerialName("count_of_isssue_appearances")
21 | val countOfIssueAppearances: Int? = null,
22 | val deck: String? = null,
23 | val description: String? = null,
24 | val publisher: ComicVinePublisher? = null,
25 | )
26 |
27 | @Serializable
28 | data class ComicVineStoryArchIssue(
29 | val id: ComicVineIssueId,
30 | val name: String? = null,
31 | @SerialName("api_detail_url")
32 | val apiDetailUrl: String,
33 | )
--------------------------------------------------------------------------------
/komf-core/src/commonMain/kotlin/snd/komf/providers/comicvine/model/ComicVineVolume.kt:
--------------------------------------------------------------------------------
1 | package snd.komf.providers.comicvine.model
2 |
3 | import kotlinx.serialization.SerialName
4 | import kotlinx.serialization.Serializable
5 | import snd.komf.model.ProviderSeriesId
6 | import kotlin.jvm.JvmInline
7 |
8 | @JvmInline
9 | @Serializable
10 | value class ComicVineVolumeId(val value: Int) {
11 | override fun toString() = value.toString()
12 | }
13 |
14 | fun ProviderSeriesId.toComicVineVolumeId() = ComicVineVolumeId(value.toInt())
15 |
16 | @Serializable
17 | data class ComicVineVolume(
18 | val id: ComicVineVolumeId,
19 | val name: String,
20 | @SerialName("api_detail_url")
21 | val apiDetailUrl: String,
22 | @SerialName("site_detail_url")
23 | val siteDetailUrl: String,
24 |
25 | val aliases: String? = null,
26 | @SerialName("count_of_issues")
27 | val countOfIssues: Int? = null,
28 | val description: String? = null,
29 | val image: ComicVineImage? = null,
30 | val publisher: ComicVinePublisher? = null,
31 | @SerialName("start_year")
32 | val startYear: String? = null,
33 | @SerialName("resource_type")
34 | val resourceType: String? = null,
35 | val characters: List? = null,
36 | val locations: List? = null,
37 | val issues: List? = null,
38 | val concepts: List? = null,
39 | )
40 |
41 | @Serializable
42 | data class ComicVineVolumeSearch(
43 | val id: Int,
44 | val name: String,
45 | @SerialName("api_detail_url")
46 | val apiDetailUrl: String,
47 | @SerialName("site_detail_url")
48 | val siteDetailUrl: String,
49 |
50 | val aliases: String? = null,
51 | @SerialName("count_of_issues")
52 | val countOfIssues: Int? = null,
53 | val description: String? = null,
54 | @SerialName("first_issue")
55 | val firstIssue: ComicVineIssueSlim? = null,
56 | @SerialName("last_issue")
57 | val lastIssue: ComicVineIssueSlim? = null,
58 | val image: ComicVineImage? = null,
59 | val publisher: ComicVinePublisher? = null,
60 | @SerialName("start_year")
61 | val startYear: String? = null,
62 | @SerialName("resource_type")
63 | val resourceType: String? = null,
64 | )
65 |
66 | @Serializable
67 | data class ComicVineConcept(
68 | val id: Int,
69 | val name: String,
70 | @SerialName("api_detail_url")
71 | val apiDetailUrl: String,
72 | @SerialName("site_detail_url")
73 | val siteDetailUrl: String,
74 | val count: String,
75 | )
76 |
77 | @Serializable
78 | data class ComicVinePublisher(
79 | val id: Int,
80 | val name: String,
81 | @SerialName("api_detail_url")
82 | val apiDetailUrl: String,
83 | )
84 |
--------------------------------------------------------------------------------
/komf-core/src/commonMain/kotlin/snd/komf/providers/hentag/HentagBook.kt:
--------------------------------------------------------------------------------
1 | package snd.komf.providers.hentag
2 |
3 | import kotlinx.datetime.Instant
4 | import kotlinx.serialization.KSerializer
5 | import kotlinx.serialization.Serializable
6 | import kotlinx.serialization.descriptors.PrimitiveKind
7 | import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
8 | import kotlinx.serialization.descriptors.SerialDescriptor
9 | import kotlinx.serialization.encoding.Decoder
10 | import kotlinx.serialization.encoding.Encoder
11 |
12 | @Serializable
13 | data class HentagBook(
14 | val title: String,
15 | val coverImageUrl: String? = null,
16 | val parodies: List? = null,
17 | val circles: List? = null,
18 | val artists: List? = null,
19 | val characters: List? = null,
20 | val maleTags: List? = null,
21 | val femaleTags: List? = null,
22 | val otherTags: List? = null,
23 | val language: String,
24 | val category: String,
25 | @Serializable(with = InstantEpochMillisSerializer::class)
26 | val createdAt: Instant,
27 | @Serializable(with = InstantEpochMillisSerializer::class)
28 | val lastModified: Instant,
29 | @Serializable(with = InstantEpochMillisSerializer::class)
30 | val publishedOn: Instant? = null,
31 | val locations: List? = null,
32 | val favorite: Boolean,
33 | )
34 |
35 | object InstantEpochMillisSerializer : KSerializer {
36 |
37 | override val descriptor: SerialDescriptor =
38 | PrimitiveSerialDescriptor("kotlinx.datetime.Instant", PrimitiveKind.LONG)
39 |
40 | override fun deserialize(decoder: Decoder): Instant =
41 | Instant.fromEpochMilliseconds(decoder.decodeLong())
42 |
43 | override fun serialize(encoder: Encoder, value: Instant) =
44 | encoder.encodeLong(value.toEpochMilliseconds())
45 | }
46 |
--------------------------------------------------------------------------------
/komf-core/src/commonMain/kotlin/snd/komf/providers/hentag/HentagClient.kt:
--------------------------------------------------------------------------------
1 | package snd.komf.providers.hentag
2 |
3 | import io.ktor.client.*
4 | import io.ktor.client.call.*
5 | import io.ktor.client.request.*
6 | import io.ktor.http.*
7 | import kotlinx.serialization.json.add
8 | import kotlinx.serialization.json.buildJsonObject
9 | import kotlinx.serialization.json.put
10 | import kotlinx.serialization.json.putJsonArray
11 | import snd.komf.model.Image
12 |
13 | class HentagClient(private val ktor: HttpClient) {
14 | private val baseUrl = "https://hentag.com/api/v1"
15 |
16 | suspend fun searchByTitle(
17 | title: String,
18 | language: String? = null
19 | ): List {
20 | return ktor.post("$baseUrl/search/vault/title") {
21 | contentType(ContentType.Application.Json)
22 | setBody(buildJsonObject {
23 | put("title", title)
24 | language?.let { put("language", it) }
25 | })
26 | }.body()
27 | }
28 |
29 | suspend fun searchByUrls(
30 | urls: List,
31 | language: String? = null
32 | ): List {
33 | return ktor.post("$baseUrl/search/vault/url") {
34 | contentType(ContentType.Application.Json)
35 | setBody(buildJsonObject {
36 | putJsonArray("urls") { urls.forEach { add(it) } }
37 | language?.let { put("language", it) }
38 | })
39 | }.body()
40 | }
41 |
42 | suspend fun searchByIds(
43 | ids: List,
44 | language: String? = null
45 | ): List {
46 | return ktor.post("$baseUrl/search/vault/id") {
47 | contentType(ContentType.Application.Json)
48 | setBody(buildJsonObject {
49 | putJsonArray("ids") { ids.forEach { add(it) } }
50 | language?.let { put("language", it) }
51 | })
52 | }.body()
53 | }
54 |
55 | suspend fun getCover(series: HentagBook): Image? {
56 | return series.coverImageUrl?.let {
57 | val bytes = ktor.get(it).body()
58 | Image(bytes)
59 | }
60 | }
61 | }
--------------------------------------------------------------------------------
/komf-core/src/commonMain/kotlin/snd/komf/providers/kodansha/KodanshaClient.kt:
--------------------------------------------------------------------------------
1 | package snd.komf.providers.kodansha
2 |
3 | import io.ktor.client.*
4 | import io.ktor.client.call.*
5 | import io.ktor.client.request.*
6 | import snd.komf.model.Image
7 | import snd.komf.providers.kodansha.model.KodanshaBook
8 | import snd.komf.providers.kodansha.model.KodanshaBookId
9 | import snd.komf.providers.kodansha.model.KodanshaResponse
10 | import snd.komf.providers.kodansha.model.KodanshaSearchResult
11 | import snd.komf.providers.kodansha.model.KodanshaSeries
12 | import snd.komf.providers.kodansha.model.KodanshaSeriesId
13 |
14 | class KodanshaClient(private val ktor: HttpClient) {
15 | private val apiUrl = "https://api.kodansha.us"
16 |
17 | suspend fun search(name: String): KodanshaResponse> {
18 | return ktor.get("$apiUrl/search/V3") {
19 | parameter("query", name)
20 | }.body()
21 | }
22 |
23 | suspend fun getSeries(seriesId: KodanshaSeriesId): KodanshaResponse {
24 | return ktor.get("$apiUrl/series/V2/${seriesId.id}").body()
25 | }
26 |
27 | suspend fun getAllSeriesBooks(seriesId: KodanshaSeriesId): List {
28 | return ktor.get("$apiUrl/product/forSeries/${seriesId.id}").body()
29 | }
30 |
31 | suspend fun getBook(bookId: KodanshaBookId): KodanshaResponse {
32 | return ktor.get("$apiUrl/product/${bookId.id}").body()
33 | }
34 |
35 | suspend fun getThumbnail(url: String): Image {
36 | val bytes: ByteArray = ktor.get(url).body()
37 | return Image(bytes)
38 | }
39 | }
40 |
41 |
--------------------------------------------------------------------------------
/komf-core/src/commonMain/kotlin/snd/komf/providers/kodansha/model/KodanshaBook.kt:
--------------------------------------------------------------------------------
1 | package snd.komf.providers.kodansha.model
2 |
3 | import kotlinx.datetime.LocalDateTime
4 | import kotlinx.serialization.Serializable
5 | import kotlin.jvm.JvmInline
6 |
7 | @Serializable
8 | data class KodanshaBook(
9 | val id: Int,
10 | val name: String,
11 | val volumeNumber: Int? = null,
12 | val chapterNumber: Int? = null,
13 | val description: String? = null,
14 | val readable: KodanshaBookReadable,
15 | val variants: List = emptyList(),
16 |
17 | val ageRating: String? = null,
18 | val publishDate: LocalDateTime? = null,
19 | val categoryId: Int,
20 | val category: String,
21 | val subCategoryId: Int,
22 | val subCategory: String,
23 | val thumbnails: List = emptyList(),
24 |
25 | val creators: List? = null,
26 |
27 | val readableUrl: String?,
28 | )
29 |
30 | @JvmInline
31 | value class KodanshaBookId(val id: Int)
32 |
33 | @Serializable
34 | data class KodanshaBookVariant(
35 | val type: String,
36 | val price: Double? = null,
37 | val fullPrice: Double? = null,
38 | val isComingSoon: Boolean? = null,
39 | val isPreorder: Boolean? = null,
40 | val priceType: String? = null,
41 | val id: Int,
42 | val description: String? = null,
43 | val isOnSale: Boolean? = null,
44 | val userDefaultProductImage: Boolean? = null,
45 | val thumbnails: List,
46 | )
47 |
48 | @Serializable
49 | data class KodanshaBookReadable(
50 | val seriesId: String,
51 | val genres: List? = null,
52 | val isbn: String? = null,
53 | val eisbn: String? = null,
54 | val pageCount: Int? = null,
55 | val coverType: String? = null,
56 | val colorType: String? = null,
57 | val printReleaseDate: LocalDateTime? = null,
58 | val digitalReleaseDate: LocalDateTime? = null,
59 | val releaseDate: LocalDateTime? = null
60 | )
61 |
--------------------------------------------------------------------------------
/komf-core/src/commonMain/kotlin/snd/komf/providers/kodansha/model/KodanshaCreator.kt:
--------------------------------------------------------------------------------
1 | package snd.komf.providers.kodansha.model
2 |
3 | import kotlinx.serialization.Serializable
4 |
5 | @Serializable
6 | data class KodanshaCreator(
7 | val name: String,
8 | )
9 |
--------------------------------------------------------------------------------
/komf-core/src/commonMain/kotlin/snd/komf/providers/kodansha/model/KodanshaResponse.kt:
--------------------------------------------------------------------------------
1 | package snd.komf.providers.kodansha.model
2 |
3 | import kotlinx.serialization.Serializable
4 |
5 | @Serializable
6 | data class KodanshaResponse(
7 | val response: T,
8 | val status: KodanshaResponseStatus
9 | )
10 |
11 | @Serializable
12 | data class KodanshaResponseStatus(
13 | val type: String
14 | )
--------------------------------------------------------------------------------
/komf-core/src/commonMain/kotlin/snd/komf/providers/kodansha/model/KodanshaSearchResult.kt:
--------------------------------------------------------------------------------
1 | package snd.komf.providers.kodansha.model
2 |
3 | import kotlinx.serialization.Serializable
4 |
5 | @Serializable
6 | data class KodanshaSearchResult(
7 | val type: String,
8 | val displayType: String,
9 | val content: KodanshaSearchResultContent,
10 | )
11 |
12 | @Serializable
13 | data class KodanshaSearchResultContent(
14 | val id: Int,
15 | val title: String,
16 | val seriesName: String? = null,
17 | val description: String? = null,
18 | val thumbnails: List = emptyList(),
19 | val readableUrl: String? = null,
20 | )
21 |
--------------------------------------------------------------------------------
/komf-core/src/commonMain/kotlin/snd/komf/providers/kodansha/model/KodanshaSeries.kt:
--------------------------------------------------------------------------------
1 | package snd.komf.providers.kodansha.model
2 |
3 | import kotlinx.serialization.Serializable
4 | import kotlin.jvm.JvmInline
5 |
6 | @Serializable
7 | data class KodanshaSeries(
8 | val id: Int,
9 | val title: String,
10 | val genres: List? = null,
11 | val creators: List? = null,
12 | val completionStatus: String? = null,
13 | val description: String? = null,
14 | val ageRating: String? = null,
15 | val thumbnails: List? = null,
16 | val publisher: String? = null,
17 | val readableUrl: String? = null,
18 | )
19 |
20 | @JvmInline
21 | value class KodanshaSeriesId(val id: Int)
22 |
23 | @Serializable
24 | data class KodanshaGenre(
25 | val name: String,
26 | val id: Int
27 | )
28 |
--------------------------------------------------------------------------------
/komf-core/src/commonMain/kotlin/snd/komf/providers/kodansha/model/KodanshaThumbnail.kt:
--------------------------------------------------------------------------------
1 | package snd.komf.providers.kodansha.model
2 |
3 | import kotlinx.serialization.Serializable
4 |
5 | @Serializable
6 | data class KodanshaThumbnail(
7 | val width: Int? = null,
8 | val height: Int? = null,
9 | val fileSize: Long? = null,
10 | val url: String,
11 | val color: String? = null,
12 | )
13 |
--------------------------------------------------------------------------------
/komf-core/src/commonMain/kotlin/snd/komf/providers/mal/MalClient.kt:
--------------------------------------------------------------------------------
1 | package snd.komf.providers.mal
2 |
3 | import io.ktor.client.*
4 | import io.ktor.client.call.*
5 | import io.ktor.client.request.*
6 | import snd.komf.model.Image
7 | import snd.komf.providers.mal.model.MalSearchResults
8 | import snd.komf.providers.mal.model.MalSeries
9 |
10 |
11 | class MalClient(private val ktor: HttpClient) {
12 | private val baseUrl = "https://api.myanimelist.net"
13 | private val includeFields: Set = setOf(
14 | "id",
15 | "title",
16 | "main_picture",
17 | "alternative_titles",
18 | "start_date",
19 | "end_date",
20 | "synopsis",
21 | "mean",
22 | "rank",
23 | "popularity",
24 | "num_list_users",
25 | "num_scoring_users",
26 | "nsfw",
27 | "genres",
28 | "created_at",
29 | "updated_at",
30 | "media_type",
31 | "status",
32 | "num_volumes",
33 | "num_chapters",
34 | "authors{first_name,last_name}",
35 | "pictures",
36 | "background",
37 | "serialization{name}",
38 | "main_picture",
39 | "pictures"
40 | )
41 |
42 | suspend fun searchSeries(name: String): MalSearchResults {
43 | return ktor.get("$baseUrl/v2/manga") {
44 | parameter("fields", "alternative_titles,media_type")
45 | parameter("q", name)
46 | parameter("nsfw", "true")
47 | }.body()
48 | }
49 |
50 | suspend fun getSeries(id: Int): MalSeries {
51 | return ktor.get("$baseUrl/v2/manga/$id") {
52 | parameter("fields", includeFields.joinToString())
53 | }.body()
54 | }
55 |
56 | suspend fun getThumbnail(series: MalSeries): Image? {
57 | return series.mainPicture?.medium?.let {
58 | val bytes: ByteArray = ktor.get(it).body()
59 | Image(bytes)
60 | }
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/komf-core/src/commonMain/kotlin/snd/komf/providers/mal/model/MalSearchResults.kt:
--------------------------------------------------------------------------------
1 | package snd.komf.providers.mal.model
2 |
3 | import kotlinx.serialization.SerialName
4 | import kotlinx.serialization.Serializable
5 |
6 | @Serializable
7 | data class MalSearchResults(
8 | val data: List,
9 | val paging: MalPaging
10 | )
11 |
12 | @Serializable
13 | data class MalSearchNode(
14 | val node: MalSearchResult,
15 | )
16 |
17 | @Serializable
18 | data class MalSearchResult(
19 | val id: Int,
20 | val title: String,
21 | @SerialName("alternative_titles")
22 | val alternativeTitles: MalAlternativeTiltle,
23 | @SerialName("media_type")
24 | val mediaType: MalMediaType,
25 | @SerialName("main_picture")
26 | val mainPicture: MalPicture? = null,
27 | )
28 |
29 | @Serializable
30 | data class MalPaging(
31 | val next: String?
32 | )
33 |
--------------------------------------------------------------------------------
/komf-core/src/commonMain/kotlin/snd/komf/providers/mangabaka/MangaBakaClient.kt:
--------------------------------------------------------------------------------
1 | package snd.komf.providers.mangabaka
2 |
3 | import io.ktor.client.*
4 | import io.ktor.client.call.*
5 | import io.ktor.client.request.*
6 | import io.ktor.http.*
7 | import snd.komf.model.Image
8 | import snd.komf.providers.mangabaka.model.MangaBakaSearchResponse
9 | import snd.komf.providers.mangabaka.model.MangaBakaSeries
10 | import snd.komf.providers.mangabaka.model.MangaBakaSeriesId
11 | import snd.komf.providers.mangabaka.model.MangaBakaType
12 |
13 | class MangaBakaClient(private val ktor: HttpClient) {
14 | private val baseUrl = "https://mangabaka.dev"
15 |
16 | suspend fun searchSeries(
17 | title: String,
18 | types: List? = null,
19 | page: Int = 1,
20 | ): MangaBakaSearchResponse {
21 | return ktor.get("${baseUrl}/api/v1/series/search") {
22 | parameter("q", title)
23 | parameter("page", page.toString())
24 | types?.forEach { parameter("type", it.name.lowercase()) }
25 | }.body()
26 | }
27 |
28 | suspend fun getSeries(id: MangaBakaSeriesId): MangaBakaSeries {
29 | return ktor.get("${baseUrl}/api/v1/series/${id}.json").body()
30 | }
31 |
32 | suspend fun getCoverBytes(url: String): Image? {
33 | val response = ktor.get(url)
34 | return Image(
35 | response.body(),
36 | response.contentType()?.let { "${it.contentType}/${it.contentSubtype}" }
37 | )
38 | }
39 | }
--------------------------------------------------------------------------------
/komf-core/src/commonMain/kotlin/snd/komf/providers/mangabaka/model/MangaBakaSearchResponse.kt:
--------------------------------------------------------------------------------
1 | package snd.komf.providers.mangabaka.model
2 |
3 | import kotlinx.serialization.Serializable
4 |
5 | @Serializable
6 | data class MangaBakaSearchResponse(
7 | val status: Int,
8 | val results: List
9 | )
10 |
--------------------------------------------------------------------------------
/komf-core/src/commonMain/kotlin/snd/komf/providers/mangadex/MangaDexClient.kt:
--------------------------------------------------------------------------------
1 | package snd.komf.providers.mangadex
2 |
3 | import io.ktor.client.*
4 | import io.ktor.client.call.*
5 | import io.ktor.client.request.*
6 | import snd.komf.model.Image
7 | import snd.komf.providers.mangadex.model.MangaDexCoverArt
8 | import snd.komf.providers.mangadex.model.MangaDexManga
9 | import snd.komf.providers.mangadex.model.MangaDexMangaId
10 | import snd.komf.providers.mangadex.model.MangaDexPagedResponse
11 | import snd.komf.providers.mangadex.model.MangaDexResponse
12 |
13 | const val filesUrl: String = "https://uploads.mangadex.org"
14 | private const val apiUrl: String = "https://api.mangadex.org"
15 |
16 | class MangaDexClient(private val ktor: HttpClient) {
17 |
18 | suspend fun searchSeries(
19 | title: String,
20 | limit: Int = 5,
21 | offset: Int = 0,
22 | ): MangaDexPagedResponse> {
23 | return ktor.get("$apiUrl/manga") {
24 | parameter("limit", limit.toString())
25 | parameter("offset", offset.toString())
26 | parameter("includes[]", "artist")
27 | parameter("includes[]", "author")
28 | parameter("includes[]", "cover_art")
29 | parameter("order[relevance]", "desc")
30 | parameter("contentRating[]", "safe")
31 | parameter("contentRating[]", "suggestive")
32 | parameter("contentRating[]", "erotica")
33 | parameter("contentRating[]", "pornographic")
34 | parameter("title", title)
35 | }.body()
36 | }
37 |
38 | suspend fun getSeries(mangaId: MangaDexMangaId): MangaDexManga {
39 | val response: MangaDexResponse = ktor.get("$apiUrl/manga/${mangaId.value}") {
40 | parameter("includes[]", "artist")
41 | parameter("includes[]", "author")
42 | parameter("includes[]", "cover_art")
43 | }.body()
44 |
45 | return response.data
46 | }
47 |
48 | suspend fun getSeriesCovers(
49 | mangaId: MangaDexMangaId,
50 | limit: Int = 100,
51 | offset: Int = 0
52 | ): MangaDexPagedResponse> {
53 | return ktor.get("$apiUrl/cover") {
54 | parameter("limit", limit.toString())
55 | parameter("offset", offset.toString())
56 | parameter("manga[]", mangaId.value)
57 | }.body()
58 | }
59 |
60 | suspend fun getCover(coverId: String): MangaDexCoverArt {
61 | val response: MangaDexPagedResponse = ktor.get("$apiUrl/cover/$coverId").body()
62 |
63 | return response.data
64 | }
65 |
66 | suspend fun getCover(mangaId: MangaDexMangaId, fileName: String): Image {
67 | val bytes = ktor.get("$filesUrl/covers/${mangaId.value}/$fileName.512.jpg")
68 | .body()
69 | return Image(bytes)
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/komf-core/src/commonMain/kotlin/snd/komf/providers/mangadex/model/MangaDexLink.kt:
--------------------------------------------------------------------------------
1 | package snd.komf.providers.mangadex.model
2 |
3 | enum class MangaDexLink {
4 | MANGA_DEX,
5 | ANILIST,
6 | ANIME_PLANET,
7 | BOOKWALKER_JP,
8 | MANGA_UPDATES,
9 | NOVEL_UPDATES,
10 | KITSU,
11 | AMAZON,
12 | EBOOK_JAPAN,
13 | MY_ANIME_LIST,
14 | CD_JAPAN,
15 | RAW,
16 | ENGLISH_TL,
17 | }
--------------------------------------------------------------------------------
/komf-core/src/commonMain/kotlin/snd/komf/providers/mangadex/model/MangaDexManga.kt:
--------------------------------------------------------------------------------
1 | package snd.komf.providers.mangadex.model
2 |
3 | import kotlinx.datetime.Instant
4 | import kotlinx.serialization.SerialName
5 | import kotlinx.serialization.Serializable
6 | import kotlin.jvm.JvmInline
7 |
8 | @Serializable
9 | data class MangaDexManga(
10 | val id: MangaDexMangaId,
11 | val type: String,
12 | val attributes: MangaDexAttributes,
13 | val relationships: List
14 | ) {
15 | fun getCoverArt(): MangaDexCoverArt? = relationships.filterIsInstance().firstOrNull()
16 |
17 | }
18 |
19 | @JvmInline
20 | @Serializable
21 | value class MangaDexMangaId(val value: String)
22 |
23 | @Serializable
24 | data class MangaDexAttributes(
25 | val title: Map,
26 | val altTitles: List