From 59380ed6f4e4f107be505f2fb26161eed3d2e32c Mon Sep 17 00:00:00 2001 From: ThePromidius Date: Mon, 24 Jan 2022 11:23:34 +0100 Subject: [PATCH] Add Kavita extension (#10241) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Trying out git Sorry in advance if this messes up. Trying to work on my extensions fork to develop a new extension So far Mangas are listed Successfully. * Added Kavita Icons * Added Kavita ParseDetails. All data is retrieved the first time SManga is created, but reimplemented since it retrieves info from backup * Added Chapter List "chapters": [ { "id": 750, "range": "1.1", "number": "11", "pages": 16, "isSpecial": false, }, { "id": 767, "range": "001.jpg", "number": "0", "pages": 5, "isSpecial": true, }, { "id": 818, "range": "1.3", "number": "13", "pages": 8, "isSpecial": false, }, { "id": 817, "range": "1.4", "number": "14", "pages": 12, "isSpecial": false, }, { "id": 768, "range": "002.jpg", "number": "0", "pages": 5, "isSpecial": true, }, <- extracted json see how when isSpecial, number = 0 Needs to be fixed * Delete .github/ISSUE_TEMPLATE directory * Checklist: ✓ It Lists the library ✓ It Lists the chapters of a serie ✓ Clicking on a chapter shows the image * New Icons & login finished Added token isvalid else relogins Login should be confirmed of not having problems * Changed Defaults to Kavita Wiki's * Added Search Manga Feature * Removed User Authentication. Leaving only token Behind * Refactored out login credentials and rewrote the authentication for joining. user now just puts in base path and api key * Cleanup some of the code and re-arange to keep related code together. * Cleanup some of the code and re-arange to keep related code together. * Still WIP, stream lined creation of Series and Series detail. Chapters are a bit broken. * Series generation with metadata is done. Working on chapter code. * Code to properly label chapters is done * Revert "Delete .github/ISSUE_TEMPLATE directory" This reverts commit 8d5740c4 * Reset editor Config * Removed Input token * Cleaned Unused code and files * Readded Search Removed token again * Modified all imports to avoid lintcheck errors -> deleted all wildcards * Integrated format filter with latest backend code * Refactored authentication to now grab it from solely the address field. * Refactored authentication to now grab it from solely the address field. * Refactored authentication to now grab it from solely the address field. Auth token is fetched when needed without requiring user to restart their session. * - Fixed duped Pages on main - Fixed Chapters going backwards - Fixed last chapter getting duped * - Fixed login with new opds url setting. * Cleaned commented code * Cleaned commented code * Added isLogin. IOException if invalid apikey * Fixes Volume Naming Previously All Volume Number was displayed as '0', It fixes that bug. * Removed Gson usages * Updated the filter Dto for all methods * Merged Http err Handling * Added debug option to see 400 error * 1.2.3. Commented debugRequest * Crappy code warning: Added first filtering support * Manga details fixed: Artist,Author,Summary, ¿genres? should be displayed properly now * Pushing nothing changed * - Fixed 500 error when reset filter - Added reading status filter - Cleaned some spaghetti code that was redundant * Code cleanup * Format Filter is almost readt. Majora finish it. * Added library filter * More code cleanup. Authentication request only sent once. * People filter Added * Added collection,character and translator filters Remaining filters: - Rating - Format * Fixed displayed chapter number. Cleaned chapter title. * - Fixed #12 - Reworked Login - Added Logs - Added more errors handlers * Added format filter Added check for url (must start with http/s) Fixed filters getting disabled when scrolling further (request for new pages did not have filters) Code cleanup * updated extVersionCode ->5 - ready for release * updated extVersionCode ->6 - pull request to tachiyomiorg/ext * removed release folder from git and removed unwanted modifications in the project * removed release folder from git and removed unwanted modifications in the project * unwanted modification * Update src/all/kavita/src/eu/kanade/tachiyomi/extension/all/kavita/Kavita.kt Co-authored-by: Alessandro Jean * Update src/all/kavita/build.gradle Co-authored-by: Alessandro Jean * removed unused import * Changed icons to be more consistent with other extensions. * QuickFix: format filter wrong default. * Added webview support (Needs to be tested thoroughly) Refactored baseUrl to apiUrl baseURl is now only "Server:Port" (without trailing slashes.) Closes #14 * Fixed Json Decode error when genres,people or artists are empty. Solution to #15 is to use tachiyomi `sort by chapter number` feature. * Added publication status filter * Added sort filter * Bump extVersionCode * Updated cover artist serial name in SeriesMetadataDto * Added user rating filter * Removed other people filter * Removed other people filter Fixed filtering suddenly not applying * Filters that are not populated won't show * Added new preferences options to customize filters showing in filter list * Added more configurable sources to have multipe kavita instances * Code Cleanup * Fixed ReadStatus * Changed order of preference setting * Added more checks and log exceptions v0.5+ is required message added * Added more checks and log exceptions v0.5+ is required message added * Fixed Cover Artist filter preference not working * Changed extVersionCode to 1 * Fixed Tags filter not populating * Added toast to restart app when source name is changed * Forgot to stage this file. Fixes tags filter not populated * Added one more error message. v0.5+ required * Fixed having to restart twice the app after preference changes Changed setupLogin order Fixed some log messages. * Changed more log messages Some cleanup. * Fixed Unexpected JSON token in manga details * Fixed search was creating a Smanga with url = series ID instead of complete url * Fixed search was creating a Smanga with url = series ID instead of complete url Co-authored-by: Joseph Milazzo Co-authored-by: Shashank Pujari Co-authored-by: Alessandro Jean --- src/all/kavita/AndroidManifest.xml | 2 + src/all/kavita/build.gradle | 16 + .../kavita/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 3175 bytes .../kavita/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 1883 bytes .../kavita/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 3949 bytes .../kavita/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 6506 bytes .../kavita/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 9189 bytes src/all/kavita/res/web_hi_res_512.png | Bin 0 -> 33547 bytes .../tachiyomi/extension/all/kavita/Kavita.kt | 1278 +++++++++++++++++ .../extension/all/kavita/KavitaConstants.kt | 77 + .../extension/all/kavita/KavitaFactory.kt | 13 + .../extension/all/kavita/KavitaHelper.kt | 48 + .../extension/all/kavita/dto/MangaDto.kt | 110 ++ .../extension/all/kavita/dto/MetadataDto.kt | 76 + .../extension/all/kavita/dto/Responses.kt | 18 + 15 files changed, 1638 insertions(+) create mode 100644 src/all/kavita/AndroidManifest.xml create mode 100644 src/all/kavita/build.gradle create mode 100644 src/all/kavita/res/mipmap-hdpi/ic_launcher.png create mode 100644 src/all/kavita/res/mipmap-mdpi/ic_launcher.png create mode 100644 src/all/kavita/res/mipmap-xhdpi/ic_launcher.png create mode 100644 src/all/kavita/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 src/all/kavita/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 src/all/kavita/res/web_hi_res_512.png create mode 100644 src/all/kavita/src/eu/kanade/tachiyomi/extension/all/kavita/Kavita.kt create mode 100644 src/all/kavita/src/eu/kanade/tachiyomi/extension/all/kavita/KavitaConstants.kt create mode 100644 src/all/kavita/src/eu/kanade/tachiyomi/extension/all/kavita/KavitaFactory.kt create mode 100644 src/all/kavita/src/eu/kanade/tachiyomi/extension/all/kavita/KavitaHelper.kt create mode 100644 src/all/kavita/src/eu/kanade/tachiyomi/extension/all/kavita/dto/MangaDto.kt create mode 100644 src/all/kavita/src/eu/kanade/tachiyomi/extension/all/kavita/dto/MetadataDto.kt create mode 100644 src/all/kavita/src/eu/kanade/tachiyomi/extension/all/kavita/dto/Responses.kt diff --git a/src/all/kavita/AndroidManifest.xml b/src/all/kavita/AndroidManifest.xml new file mode 100644 index 000000000..30deb7f79 --- /dev/null +++ b/src/all/kavita/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/src/all/kavita/build.gradle b/src/all/kavita/build.gradle new file mode 100644 index 000000000..0c767f5f6 --- /dev/null +++ b/src/all/kavita/build.gradle @@ -0,0 +1,16 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply plugin: 'kotlinx-serialization' + +ext { + extName = 'Kavita' + pkgNameSuffix = 'all.kavita' + extClass = '.KavitaFactory' + extVersionCode = 1 +} + +dependencies { + implementation 'info.debatty:java-string-similarity:2.0.0' +} + +apply from: "$rootDir/common.gradle" diff --git a/src/all/kavita/res/mipmap-hdpi/ic_launcher.png b/src/all/kavita/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..16cee6c3e562ab2df5129ed30737a02362d999de GIT binary patch literal 3175 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 Ver@jQlf?i4002ovPDHLkV1h5CE(M{uV9T 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 Hu0mjfERi0s literal 0 HcmV?d00001 diff --git a/src/all/kavita/res/mipmap-xxhdpi/ic_launcher.png b/src/all/kavita/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..98f3ea1ff226dc3810f6663eb95098365d4ea1fb GIT binary patch literal 6506 zcmV-w8I|UVP)=NklHZC9)+u|^os#FmFfL%$5 zQ)O^2hZt~T2XJgBp<+w6g{{bmg9NyOu?q=ZxP*{|F0}XV+jr*oo0;C%Gp~EPduHCb zGt}d0cW1hLxniQC}NpV{wDZW>XlV=c2#uG zf(u0F>y?II)!Jp`Ajm79xNPdBre{I}jZ~_Y-&Ur|FO3{M{=NOL{pP@HkAHg%ic*G8 zV32|UMBzz5K@y+a^3;mVX|umL6|a1Bs$A?XRLi7XwG^|e36FV=;Cbf)5fVIlY4Zyx z1Y8jw(X*zPt}`xM7k3DN$JGc-~RJW_rGF8gdq7=BfkK#@EG_0*RwZ~ z*|{f%OZn4F)v_bahQs4Qn~wVED_iFPL`#ZoVZ;*E0V8`PJ~;WnH*eVRBO9WUUx@qy zguV_ck8$UVPp-@?n)lpLDSrm9N_7F!Ql60Dv62zW7?%|7aw;m2NUVb>5DO6f_nc(z z#MHi{>%aHuM}7srD>5ccp#A(``vC~AG2yxF?wjXa@!4CyIUXxrU*R->EpP!V%7e#@#4PkZhP>TU;o#W3`FXV7F^^9Ak*i8PwKXBeQ9lO$)f)Ne<$PS z(+@nI0#KvW$Gm?|;yPe@)}bqL@QEh#yN|5jeD{}MhU*Hp3ZYM=eCPs1)h7u#r0?4L z)Dx4v#$7FgCnN>(Y#?rf^6OWpp}EnQW-uUXWg*}kI&;H?zyAKmA9@VZn)0JUx&Wc? zW1#xLC-a%@&+aHBs^_ye61MVKjOh9&0`nlKasBk`6Vic4bWdTJFVQn4YL`qg_N#xt z`rhl|x=CAw(4nRsj#4G^1CXgc@X3Dam1o|Dn5&28;ujuI+4bt{(tKjIO-~cm$E*X* z`_WM#QQH{R_|Si@y=RFHP#(S!AZ0VA0}!5z3WNZm&y)MZSD!g--ME(DRd?Uo4eLRi zPU!AW-5R4lVF9FLzA#U0)J<32cP4yIzy|?BUx;=vhN?iU|C2?4{@|6T-<9SdIl&7( zebb`fhpUr^FVH#nFN8b<1=0lwi?{iz`xe31I0F#vT$DDit^%3Sm#sj(0MI+q9NGXN z?S|KLxx_x2;NF`Jkjz2=K<7YFad?9&5FO@_Hm_fRa<&5b2FRE4c$h9PbJPg{h2j7B zQ6F1@7TW+(N4g;Z(cWkDfe@e`P@n@+M1@bG_)6Wd+6x{p=5)-LuXH-Vd@%r_2}3`K z_6I3NVmb;G8bEEWJ|3j-${j}dZf4n_4n sh-2g#AJ7sX($=t>wz236D(q+yc~W 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 literal 0 HcmV?d00001 diff --git a/src/all/kavita/res/mipmap-xxxhdpi/ic_launcher.png b/src/all/kavita/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..dc7d8341f3c7731c7f291bce231453c019cf53c3 GIT binary patch literal 9189 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 D^5d?$ literal 0 HcmV?d00001 diff --git a/src/all/kavita/res/web_hi_res_512.png b/src/all/kavita/res/web_hi_res_512.png new file mode 100644 index 0000000000000000000000000000000000000000..0168166c3363a50c51cac0c655eeddf92cbcd868 GIT binary patch literal 33547 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%0D<* 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 literal 0 HcmV?d00001 diff --git a/src/all/kavita/src/eu/kanade/tachiyomi/extension/all/kavita/Kavita.kt b/src/all/kavita/src/eu/kanade/tachiyomi/extension/all/kavita/Kavita.kt new file mode 100644 index 000000000..bf4be2f84 --- /dev/null +++ b/src/all/kavita/src/eu/kanade/tachiyomi/extension/all/kavita/Kavita.kt @@ -0,0 +1,1278 @@ +package eu.kanade.tachiyomi.extension.all.kavita + +import android.app.Application +import android.content.SharedPreferences +import android.text.InputType +import android.util.Log +import android.widget.Toast +import androidx.preference.EditTextPreference +import androidx.preference.MultiSelectListPreference +import eu.kanade.tachiyomi.BuildConfig +import eu.kanade.tachiyomi.extension.all.kavita.dto.AuthenticationDto +import eu.kanade.tachiyomi.extension.all.kavita.dto.ChapterDto +import eu.kanade.tachiyomi.extension.all.kavita.dto.KavitaComicsSearch +import eu.kanade.tachiyomi.extension.all.kavita.dto.MangaFormat +import eu.kanade.tachiyomi.extension.all.kavita.dto.MetadataAgeRatings +import eu.kanade.tachiyomi.extension.all.kavita.dto.MetadataCollections +import eu.kanade.tachiyomi.extension.all.kavita.dto.MetadataGenres +import eu.kanade.tachiyomi.extension.all.kavita.dto.MetadataLanguages +import eu.kanade.tachiyomi.extension.all.kavita.dto.MetadataLibrary +import eu.kanade.tachiyomi.extension.all.kavita.dto.MetadataPayload +import eu.kanade.tachiyomi.extension.all.kavita.dto.MetadataPeople +import eu.kanade.tachiyomi.extension.all.kavita.dto.MetadataPubStatus +import eu.kanade.tachiyomi.extension.all.kavita.dto.MetadataTag +import eu.kanade.tachiyomi.extension.all.kavita.dto.PersonRole +import eu.kanade.tachiyomi.extension.all.kavita.dto.SeriesDto +import eu.kanade.tachiyomi.extension.all.kavita.dto.SeriesMetadataDto +import eu.kanade.tachiyomi.extension.all.kavita.dto.VolumeDto +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.network.POST +import eu.kanade.tachiyomi.network.asObservableSuccess +import eu.kanade.tachiyomi.source.ConfigurableSource +import eu.kanade.tachiyomi.source.model.Filter +import eu.kanade.tachiyomi.source.model.FilterList +import eu.kanade.tachiyomi.source.model.MangasPage +import eu.kanade.tachiyomi.source.model.Page +import eu.kanade.tachiyomi.source.model.SChapter +import eu.kanade.tachiyomi.source.model.SManga +import eu.kanade.tachiyomi.source.online.HttpSource +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.add +import kotlinx.serialization.json.buildJsonArray +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put +import okhttp3.Headers +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.Request +import okhttp3.RequestBody +import okhttp3.RequestBody.Companion.toRequestBody +import okhttp3.Response +import rx.Observable +import rx.Single +import rx.schedulers.Schedulers +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get +import uy.kohesive.injekt.injectLazy +import java.io.IOException +import java.security.MessageDigest + +class Kavita(suffix: String = "") : ConfigurableSource, HttpSource() { + + override val id by lazy { + val key = "${"kavita_$suffix"}/all/$versionId" + val bytes = MessageDigest.getInstance("MD5").digest(key.toByteArray()) + (0..7).map { bytes[it].toLong() and 0xff shl 8 * (7 - it) }.reduce(Long::or) and Long.MAX_VALUE + } + private val preferences: SharedPreferences by lazy { + Injekt.get().getSharedPreferences("source_$id", 0x0000) + } + override val name = "Kavita (${preferences.getString(KavitaConstants.customSourceNamePref,suffix)})" + override val lang = "all" + override val supportsLatest = true + private val apiUrl by lazy { getPrefApiUrl() } + override val baseUrl by lazy { getPrefBaseUrl() } + private val address by lazy { getPrefAddress() } // Address for the Kavita OPDS url. Should be http(s)://host:(port)/api/opds/api-key + private var jwtToken = "" // * JWT Token for authentication with the server. Stored in memory. + private val LOG_TAG = "extension.all.kavita_${preferences.getString(KavitaConstants.customSourceNamePref,suffix)!!.replace(' ','_')}" + private var isLoged = false // Used to know if login was correct and not send login requests anymore + + private val json: Json by injectLazy() + private val helper = KavitaHelper() + private inline fun Response.parseAs(): T = + use { + json.decodeFromString(it.body?.string().orEmpty()) + } + private inline fun > safeValueOf(type: String): T { + return java.lang.Enum.valueOf(T::class.java, type) + } + + private var series = emptyList() // Acts as a cache + + override fun popularMangaRequest(page: Int): Request { + if (!isLoged) { + doLogin() + } + return POST( + "$apiUrl/series/all?pageNumber=$page&libraryId=0&pageSize=20", + headersBuilder().build(), + buildFilterBody() + ) + } + + override fun popularMangaParse(response: Response): MangasPage { + try { + val result = response.parseAs>() + series = result + val mangaList = result.map { item -> helper.createSeriesDto(item, apiUrl) } + return MangasPage(mangaList, helper.hasNextPage(response)) + } catch (e: Exception) { + Log.e(LOG_TAG, "Possible outdated kavita", e) + throw IOException("Please check your kavita version.\nv0.5+ is required for the extension to work properly") + } + } + + override fun latestUpdatesRequest(page: Int): Request { + return POST( + "$apiUrl/series/recently-added?pageNumber=$page&libraryId=0&pageSize=20", + headersBuilder().build(), + buildFilterBody() + ) + } + + override fun latestUpdatesParse(response: Response): MangasPage { + val result = response.parseAs>() + series = result + val mangaList = result.map { item -> helper.createSeriesDto(item, apiUrl) } + return MangasPage(mangaList, helper.hasNextPage(response)) + } + + /** + * SEARCH MANGA + * **/ + private var isFilterOn = false // If any filter option is enabled this is true + private var toFilter = MetadataPayload() + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + toFilter = MetadataPayload() // need to reset it or will double + isFilterOn = false + filters.forEach { filter -> + when (filter) { + + is SortFilter -> { + if (filter.state != null) { + toFilter.sorting = filter.state!!.index + 1 + toFilter.sorting_asc = filter.state!!.ascending + // disabled till the search api is stable + // isFilterOn = true + } + } + is StatusFilterGroup -> { + filter.state.forEach { content -> + if (content.state) { + toFilter.readStatus.add(content.name) + isFilterOn = true + } + } + } + is GenreFilterGroup -> { + filter.state.forEach { content -> + if (content.state) { + toFilter.genres.add(genresListMeta.find { it.title == content.name }!!.id) + isFilterOn = true + } + } + } + is UserRating -> { + toFilter.userRating = filter.state + } + is TagFilterGroup -> { + filter.state.forEach { content -> + if (content.state) { + toFilter.tags.add(tagsListMeta.find { it.title == content.name }!!.id) + isFilterOn = true + } + } + } + is AgeRatingFilterGroup -> { + filter.state.forEach { content -> + if (content.state) { + toFilter.ageRating.add(ageRatingsListMeta.find { it.title == content.name }!!.value) + isFilterOn = true + } + } + } + is FormatsFilterGroup -> { + filter.state.forEach { content -> + if (content.state) { + toFilter.formats.add(MangaFormat.valueOf(content.name).ordinal) + isFilterOn = true + } + } + } + is CollectionFilterGroup -> { + filter.state.forEach { content -> + if (content.state) { + toFilter.collections.add(collectionsListMeta.find { it.title == content.name }!!.id) + isFilterOn = true + } + } + } + + is LanguageFilterGroup -> { + filter.state.forEach { content -> + if (content.state) { + toFilter.language.add(languagesListMeta.find { it.title == content.name }!!.isoCode) + isFilterOn = true + } + } + } + is LibrariesFilterGroup -> { + filter.state.forEach { content -> + if (content.state) { + toFilter.libraries.add(libraryListMeta.find { it.name == content.name }!!.id) + isFilterOn = true + } + } + } + + is PubStatusFilterGroup -> { + filter.state.forEach { content -> + if (content.state) { + toFilter.pubStatus.add(pubStatusListMeta.find { it.title == content.name }!!.value) + isFilterOn = true + } + } + } + + is WriterPeopleFilterGroup -> { + filter.state.forEach { content -> + if (content.state) { + toFilter.peopleWriters.add(peopleListMeta.find { it.name == content.name }!!.id) + isFilterOn = true + } + } + } + is PencillerPeopleFilterGroup -> { + filter.state.forEach { content -> + if (content.state) { + toFilter.peoplePenciller.add(peopleListMeta.find { it.name == content.name }!!.id) + isFilterOn = true + } + } + } + is InkerPeopleFilterGroup -> { + filter.state.forEach { content -> + if (content.state) { + toFilter.peopleInker.add(peopleListMeta.find { it.name == content.name }!!.id) + isFilterOn = true + } + } + } + is ColoristPeopleFilterGroup -> { + filter.state.forEach { content -> + if (content.state) { + toFilter.peoplePeoplecolorist.add(peopleListMeta.find { it.name == content.name }!!.id) + isFilterOn = true + } + } + } + is LettererPeopleFilterGroup -> { + filter.state.forEach { content -> + if (content.state) { + toFilter.peopleLetterer.add(peopleListMeta.find { it.name == content.name }!!.id) + isFilterOn = true + } + } + } + is CoverArtistPeopleFilterGroup -> { + filter.state.forEach { content -> + if (content.state) { + toFilter.peopleCoverArtist.add(peopleListMeta.find { it.name == content.name }!!.id) + isFilterOn = true + } + } + } + is EditorPeopleFilterGroup -> { + filter.state.forEach { content -> + if (content.state) { + toFilter.peopleEditor.add(peopleListMeta.find { it.name == content.name }!!.id) + isFilterOn = true + } + } + } + is PublisherPeopleFilterGroup -> { + filter.state.forEach { content -> + if (content.state) { + toFilter.peoplePublisher.add(peopleListMeta.find { it.name == content.name }!!.id) + isFilterOn = true + } + } + } + is CharacterPeopleFilterGroup -> { + filter.state.forEach { content -> + if (content.state) { + toFilter.peopleCharacter.add(peopleListMeta.find { it.name == content.name }!!.id) + isFilterOn = true + } + } + } + is TranslatorPeopleFilterGroup -> { + filter.state.forEach { content -> + if (content.state) { + toFilter.peopleTranslator.add(peopleListMeta.find { it.name == content.name }!!.id) + isFilterOn = true + } + } + } + } + } + + if (isFilterOn || query.isEmpty()) { + return popularMangaRequest(page) + } else { + return GET("$apiUrl/Library/search?queryString=$query", headers) + } + } + + override fun searchMangaParse(response: Response): MangasPage { + if (isFilterOn) { + return popularMangaParse(response) + } else { + if (response.request.url.toString().contains("api/series/all")) + return popularMangaParse(response) + + val result = response.parseAs>() + val mangaList = result.map(::searchMangaFromObject) + return MangasPage(mangaList, false) + } + } + + private fun searchMangaFromObject(obj: KavitaComicsSearch): SManga = SManga.create().apply { + title = obj.name + thumbnail_url = "$apiUrl/Image/series-cover?seriesId=${obj.seriesId}" + description = "None" + url = "$apiUrl/Series/${obj.seriesId}" + } + + /** + * MANGA DETAILS (metadata about series) + * **/ + + override fun fetchMangaDetails(manga: SManga): Observable { + val serieId = helper.getIdFromUrl(manga.url) + return client.newCall(GET("$apiUrl/series/metadata?seriesId=$serieId", headersBuilder().build())) + .asObservableSuccess() + .map { response -> + Log.d(LOG_TAG, "fetchMangaDetails response body: ```${response.peekBody(Long.MAX_VALUE).string()}```") + mangaDetailsParse(response).apply { initialized = true } + } + } + + override fun mangaDetailsRequest(manga: SManga): Request { + val serieId = helper.getIdFromUrl(manga.url) + val foundSerie = series.find { dto -> dto.id == serieId } + return GET( + "$baseUrl/library/${foundSerie!!.libraryId}/series/$serieId", + headersBuilder().build() + ) + } + + override fun mangaDetailsParse(response: Response): SManga { + + val result = response.parseAs() + + val existingSeries = series.find { dto -> dto.id == result.seriesId } + + if (existingSeries != null) { + val manga = helper.createSeriesDto(existingSeries, apiUrl) + manga.artist = result.coverArtists.joinToString { it.name } + manga.description = result.summary + manga.author = result.writers.joinToString { it.name } + manga.genre = result.genres.joinToString { it.title } + + return manga + } + + return SManga.create().apply { + url = "$apiUrl/Series/${result.seriesId}" + artist = result.coverArtists.joinToString { ", " } + author = result.writers.joinToString { ", " } + genre = result.genres.joinToString { ", " } + thumbnail_url = "$apiUrl/image/series-cover?seriesId=${result.seriesId}" + } + } + + /** + * CHAPTER LIST + * **/ + override fun chapterListRequest(manga: SManga): Request { + val url = "$apiUrl/Series/volumes?seriesId=${helper.getIdFromUrl(manga.url)}" + return GET(url, headersBuilder().build()) + } + + private fun chapterFromObject(obj: ChapterDto): SChapter = SChapter.create().apply { + url = obj.id.toString() + if (obj.number == "0" && obj.isSpecial) { + name = obj.range + } else { + val cleanedName = obj.title.replaceFirst("^0+(?!$)".toRegex(), "") + name = "Chapter $cleanedName" + } + date_upload = helper.parseDate(obj.created) + chapter_number = obj.number.toFloat() + scanlator = obj.pages.toString() + } + + private fun chapterFromVolume(obj: ChapterDto, volume: VolumeDto): SChapter = + SChapter.create().apply { + // If there are multiple chapters to this volume, then prefix with Volume number + if (volume.chapters.isNotEmpty() && obj.number != "0") { + name = "Volume ${volume.number} Chapter ${obj.number}" + } else if (obj.number == "0") { + // This chapter is solely on volume + if (volume.number == 0) { + // Treat as special + if (obj.range == "") { + name = "Chapter 0" + } else { + name = obj.range + } + } else { + name = "Volume ${volume.number}" + } + } else { + name = "Unhandled Else Volume ${volume.number}" + } + url = obj.id.toString() + date_upload = helper.parseDate(obj.created) + chapter_number = obj.number.toFloat() + scanlator = "${obj.pages}" + } + override fun chapterListParse(response: Response): List { + try { + val volumes = response.parseAs>() + val allChapterList = mutableListOf() + volumes.forEach { volume -> + run { + if (volume.number == 0) { + // Regular chapters + volume.chapters.map { + allChapterList.add(chapterFromObject(it)) + } + } else { + // Volume chapter + volume.chapters.map { + allChapterList.add(chapterFromVolume(it, volume)) + } + } + } + } + allChapterList.reverse() + return allChapterList + } catch (e: Exception) { + Log.e(LOG_TAG, "Unhandled exception parsing chapters. Send logs to kavita devs", e) + throw IOException("Unhandled exception parsing chapters. Send logs to kavita devs") + } + } + + /** + * Fetches the "url" of each page from the chapter + * **/ + override fun pageListRequest(chapter: SChapter): Request { + return GET("${chapter.url}/Reader/chapter-info") + } + + override fun fetchPageList(chapter: SChapter): Observable> { + val chapterId = chapter.url + val numPages = chapter.scanlator?.toInt() + val numPages2 = "$numPages".toInt() - 1 + val pages = mutableListOf() + for (i in 0..numPages2) { + pages.add( + Page( + index = i, + imageUrl = "$apiUrl/Reader/image?chapterId=$chapterId&page=$i" + ) + ) + } + return Observable.just(pages) + } + + override fun pageListParse(response: Response): List = + throw UnsupportedOperationException("Not used") + + override fun imageUrlParse(response: Response): String = "" + + /** + * FILTERING + **/ + + /** Some variable names already exist. im not good at naming add Meta suffix */ + private var genresListMeta = emptyList() + private var tagsListMeta = emptyList() + private var ageRatingsListMeta = emptyList() + private var peopleListMeta = emptyList() + private var pubStatusListMeta = emptyList() + private var languagesListMeta = emptyList() + private var libraryListMeta = emptyList() + private var collectionsListMeta = emptyList() + private val personRoles = listOf( + "Writer", + "Penciller", + "Inker", + "Colorist", + "Letterer", + "CoverArtist", + "Editor", + "Publisher", + "Character", + "Translator" + ) + + private class UserRating() : + Filter.Select( + "Minimum Rating", + arrayOf( + "Any", + "1 star", + "2 stars", + "3 stars", + "4 stars", + "5 stars" + ) + ) + + private class SortFilter(sortables: Array) : Filter.Sort("Sort by", sortables, Selection(0, true)) + + val sortableList = listOf( + Pair("Sort name", 1), + Pair("Created", 2), + Pair("Last modified", 3), + ) + private class StatusFilter(name: String) : Filter.CheckBox(name, false) + private class StatusFilterGroup(filters: List) : + Filter.Group("Status", filters) + + private class GenreFilter(name: String) : Filter.CheckBox(name, false) + private class GenreFilterGroup(genres: List) : + Filter.Group("Genres", genres) + + private class TagFilter(name: String) : Filter.CheckBox(name, false) + private class TagFilterGroup(tags: List) : Filter.Group("Tags", tags) + + private class AgeRatingFilter(name: String) : Filter.CheckBox(name, false) + private class AgeRatingFilterGroup(ageRatings: List) : + Filter.Group("Age Rating", ageRatings) + + private class FormatFilter(name: String) : Filter.CheckBox(name, false) + private class FormatsFilterGroup(formats: List) : + Filter.Group("Formats", formats) + + private class CollectionFilter(name: String) : Filter.CheckBox(name, false) + private class CollectionFilterGroup(collections: List) : + Filter.Group("Collection", collections) + + private class LanguageFilter(name: String) : Filter.CheckBox(name, false) + private class LanguageFilterGroup(languages: List) : + Filter.Group("Language", languages) + + private class LibraryFilter(library: String) : Filter.CheckBox(library, false) + private class LibrariesFilterGroup(libraries: List) : + Filter.Group("Libraries", libraries) + + private class PubStatusFilter(name: String) : Filter.CheckBox(name, false) + private class PubStatusFilterGroup(status: List) : + Filter.Group("Publication Status", status) + + private class PeopleHeaderFilter(name: String) : + Filter.Header(name) + private class PeopleSeparatorFilter() : + Filter.Separator() + + private class WriterPeopleFilter(name: String) : Filter.CheckBox(name, false) + private class WriterPeopleFilterGroup(peoples: List) : + Filter.Group("Writer", peoples) + + private class PencillerPeopleFilter(name: String) : Filter.CheckBox(name, false) + private class PencillerPeopleFilterGroup(peoples: List) : + Filter.Group("Penciller", peoples) + + private class InkerPeopleFilter(name: String) : Filter.CheckBox(name, false) + private class InkerPeopleFilterGroup(peoples: List) : + Filter.Group("Inker", peoples) + + private class ColoristPeopleFilter(name: String) : Filter.CheckBox(name, false) + private class ColoristPeopleFilterGroup(peoples: List) : + Filter.Group("Colorist", peoples) + + private class LettererPeopleFilter(name: String) : Filter.CheckBox(name, false) + private class LettererPeopleFilterGroup(peoples: List) : + Filter.Group("Letterer", peoples) + + private class CoverArtistPeopleFilter(name: String) : Filter.CheckBox(name, false) + private class CoverArtistPeopleFilterGroup(peoples: List) : + Filter.Group("Cover Artist", peoples) + + private class EditorPeopleFilter(name: String) : Filter.CheckBox(name, false) + private class EditorPeopleFilterGroup(peoples: List) : + Filter.Group("Editor", peoples) + + private class PublisherPeopleFilter(name: String) : Filter.CheckBox(name, false) + private class PublisherPeopleFilterGroup(peoples: List) : + Filter.Group("Publisher", peoples) + + private class CharacterPeopleFilter(name: String) : Filter.CheckBox(name, false) + private class CharacterPeopleFilterGroup(peoples: List) : + Filter.Group("Character", peoples) + + private class TranslatorPeopleFilter(name: String) : Filter.CheckBox(name, false) + private class TranslatorPeopleFilterGroup(peoples: List) : + Filter.Group("Translator", peoples) + + override fun getFilterList(): FilterList { + val toggledFilters = getToggledFilters() + + val filters = try { + val peopleInRoles = mutableListOf>() + personRoles.map { role -> + val peoplesWithRole = mutableListOf() + peopleListMeta.map { + if (it.role == safeValueOf(role).role) { + peoplesWithRole.add(it) + } + } + peopleInRoles.add(peoplesWithRole) + } + + val filtersLoaded = mutableListOf>() + + if (sortableList.isNotEmpty() and toggledFilters.contains("Sort Options")) { + filtersLoaded.add( + SortFilter(sortableList.map { it.first }.toTypedArray()) + ) + } + if (toggledFilters.contains("Read Status")) { + filtersLoaded.add( + StatusFilterGroup( + listOf( + "notRead", + "inProgress", + "read" + ).map { StatusFilter(it) } + ) + ) + } + + if (genresListMeta.isNotEmpty() and toggledFilters.contains("Genres")) { + filtersLoaded.add( + GenreFilterGroup(genresListMeta.map { GenreFilter(it.title) }) + ) + } + if (tagsListMeta.isNotEmpty() and toggledFilters.contains("Tags")) { + filtersLoaded.add( + TagFilterGroup(tagsListMeta.map { TagFilter(it.title) }) + ) + } + if (ageRatingsListMeta.isNotEmpty() and toggledFilters.contains("Age Rating")) { + filtersLoaded.add( + AgeRatingFilterGroup(ageRatingsListMeta.map { AgeRatingFilter(it.title) }) + ) + } + if (toggledFilters.contains("Format")) { + filtersLoaded.add( + FormatsFilterGroup( + listOf( + "Image", + "Archive", + "Unknown", + "Epub", + "Pdf" + ).map { FormatFilter(it) } + ) + ) + } + if (collectionsListMeta.isNotEmpty() and toggledFilters.contains("Collections")) { + filtersLoaded.add( + CollectionFilterGroup(collectionsListMeta.map { CollectionFilter(it.title) }) + ) + } + if (languagesListMeta.isNotEmpty() and toggledFilters.contains("Languages")) { + filtersLoaded.add( + LanguageFilterGroup(languagesListMeta.map { LanguageFilter(it.title) }) + ) + } + if (libraryListMeta.isNotEmpty() and toggledFilters.contains("Libraries")) { + filtersLoaded.add( + LibrariesFilterGroup(libraryListMeta.map { LibraryFilter(it.name) }) + ) + } + if (pubStatusListMeta.isNotEmpty() and toggledFilters.contains("Publication Status")) { + filtersLoaded.add( + PubStatusFilterGroup(pubStatusListMeta.map { PubStatusFilter(it.title) }) + ) + } + if (pubStatusListMeta.isNotEmpty() and toggledFilters.contains("Rating")) { + filtersLoaded.add( + UserRating() + ) + } + + // People Metadata: + if (personRoles.isNotEmpty() and toggledFilters.any { personRoles.contains(it) }) { + filtersLoaded.addAll( + listOf>( + PeopleHeaderFilter(""), + PeopleSeparatorFilter(), + PeopleHeaderFilter("PEOPLE") + ) + ) + if (peopleInRoles[0].isNotEmpty() and toggledFilters.contains("Writer")) { + filtersLoaded.add( + WriterPeopleFilterGroup( + peopleInRoles[0].map { WriterPeopleFilter(it.name) } + ) + ) + } + if (peopleInRoles[1].isNotEmpty() and toggledFilters.contains("Penciller")) { + filtersLoaded.add( + PencillerPeopleFilterGroup( + peopleInRoles[1].map { PencillerPeopleFilter(it.name) } + ) + ) + } + if (peopleInRoles[2].isNotEmpty() and toggledFilters.contains("Inker")) { + filtersLoaded.add( + InkerPeopleFilterGroup( + peopleInRoles[2].map { InkerPeopleFilter(it.name) } + ) + ) + } + if (peopleInRoles[3].isNotEmpty() and toggledFilters.contains("Colorist")) { + filtersLoaded.add( + ColoristPeopleFilterGroup( + peopleInRoles[3].map { ColoristPeopleFilter(it.name) } + ) + ) + } + if (peopleInRoles[4].isNotEmpty() and toggledFilters.contains("Letterer")) { + filtersLoaded.add( + LettererPeopleFilterGroup( + peopleInRoles[4].map { LettererPeopleFilter(it.name) } + ) + ) + } + if (peopleInRoles[5].isNotEmpty() and toggledFilters.contains("CoverArtist")) { + filtersLoaded.add( + CoverArtistPeopleFilterGroup( + peopleInRoles[5].map { CoverArtistPeopleFilter(it.name) } + ) + ) + } + if (peopleInRoles[6].isNotEmpty() and toggledFilters.contains("Editor")) { + filtersLoaded.add( + EditorPeopleFilterGroup( + peopleInRoles[6].map { EditorPeopleFilter(it.name) } + ) + ) + } + + if (peopleInRoles[7].isNotEmpty() and toggledFilters.contains("Publisher")) { + filtersLoaded.add( + PublisherPeopleFilterGroup( + peopleInRoles[7].map { PublisherPeopleFilter(it.name) } + ) + ) + } + if (peopleInRoles[8].isNotEmpty() and toggledFilters.contains("Character")) { + filtersLoaded.add( + CharacterPeopleFilterGroup( + peopleInRoles[8].map { CharacterPeopleFilter(it.name) } + ) + ) + } + if (peopleInRoles[9].isNotEmpty() and toggledFilters.contains("Translator")) { + filtersLoaded.add( + TranslatorPeopleFilterGroup( + peopleInRoles[9].map { TranslatorPeopleFilter(it.name) } + ) + ) + filtersLoaded + } else { + filtersLoaded + } + } else { filtersLoaded } + } catch (e: Exception) { + Log.e(LOG_TAG, "[FILTERS] Error while creating filter list", e) + emptyList() + } + return FilterList(filters) + } + + /** + * + * Finished filtering + * + * */ + class LoginErrorException(message: String? = null, cause: Throwable? = null) : Exception(message, cause) { + constructor(cause: Throwable) : this(null, cause) + } + override fun headersBuilder(): Headers.Builder { + if (jwtToken.isEmpty()) throw LoginErrorException("403 Error\nOPDS address got modified or is incorrect") + return Headers.Builder() + .add("User-Agent", "Tachiyomi Kavita v${BuildConfig.VERSION_NAME}") + .add("Content-Type", "application/json") + .add("Authorization", "Bearer $jwtToken") + } + private fun setupLoginHeaders(): Headers.Builder { + return Headers.Builder() + .add("User-Agent", "Tachiyomi Kavita v${BuildConfig.VERSION_NAME}") + .add("Content-Type", "application/json") + .add("Authorization", "Bearer $jwtToken") + } + private fun buildFilterBody(filter: MetadataPayload = toFilter): RequestBody { + var filter = filter + if (!isFilterOn) { + filter = MetadataPayload() + } + + val formats = if (filter.formats.isEmpty()) { + buildJsonArray { + add(MangaFormat.Archive.ordinal) + add(MangaFormat.Image.ordinal) + add(MangaFormat.Pdf.ordinal) + } + } else { + buildJsonArray { filter.formats.map { add(it) } } + } + + val payload = buildJsonObject { + put("formats", formats) + put("libraries", buildJsonArray { filter.libraries.map { add(it) } }) + put( + "readStatus", + buildJsonObject { + if (filter.readStatus.isNotEmpty()) { + filter.readStatus.forEach { status -> + if (status in listOf("notRead", "inProgress", "read")) { + put(status, JsonPrimitive(true)) + } else { + put(status, JsonPrimitive(false)) + } + } + } else { + put("notRead", JsonPrimitive(true)) + put("inProgress", JsonPrimitive(true)) + put("read", JsonPrimitive(true)) + } + } + ) + put("genres", buildJsonArray { filter.genres.map { add(it) } }) + put("writers", buildJsonArray { filter.peopleWriters.map { add(it) } }) + put("penciller", buildJsonArray { filter.peoplePenciller.map { add(it) } }) + put("inker", buildJsonArray { filter.peopleInker.map { add(it) } }) + put("colorist", buildJsonArray { filter.peoplePeoplecolorist.map { add(it) } }) + put("letterer", buildJsonArray { filter.peopleLetterer.map { add(it) } }) + put("coverArtist", buildJsonArray { filter.peopleCoverArtist.map { add(it) } }) + put("editor", buildJsonArray { filter.peopleEditor.map { add(it) } }) + put("publisher", buildJsonArray { filter.peoplePublisher.map { add(it) } }) + put("character", buildJsonArray { filter.peopleCharacter.map { add(it) } }) + put("translators", buildJsonArray { filter.peopleTranslator.map { add(it) } }) + put("collectionTags", buildJsonArray { filter.collections.map { add(it) } }) + put("languages", buildJsonArray { filter.language.map { add(it) } }) + put("publicationStatus", buildJsonArray { filter.pubStatus.map { add(it) } }) + put("tags", buildJsonArray { filter.tags.map { add(it) } }) + put("rating", filter.userRating) + put("ageRating", buildJsonArray { filter.ageRating.map { add(it) } }) + put( + "sortOptions", + buildJsonObject { + put("sortField", filter.sorting) + put("isAscending", JsonPrimitive(filter.sorting_asc)) + } + ) + } + return payload.toString().toRequestBody(JSON_MEDIA_TYPE) + } + + override fun setupPreferenceScreen(screen: androidx.preference.PreferenceScreen) { + val opdsAddressPref = screen.editTextPreference( + ADDRESS_TITLE, + "OPDS url", + "", + "The OPDS url copied from User Settings. This should include address and the api key on end." + ) + + val enabledFiltersPref = MultiSelectListPreference(screen.context).apply { + key = KavitaConstants.toggledFiltersPref + title = "Default filters shown" + summary = "Show these filters in the filter list" + entries = KavitaConstants.filterPrefEntries + entryValues = KavitaConstants.filterPrefEntriesValue + setDefaultValue(KavitaConstants.defaultFilterPrefEntries) + setOnPreferenceChangeListener { _, newValue -> + val checkValue = newValue as Set + preferences.edit() + .putStringSet(KavitaConstants.toggledFiltersPref, checkValue) + .commit() + } + } + val customSourceNamePref = EditTextPreference(screen.context).apply { + key = KavitaConstants.customSourceNamePref + title = "Displayed name for source" + summary = "Here you can change this source name.\n" + + "You can write a descriptive name to identify this opds URL" + setOnPreferenceChangeListener { _, newValue -> + val res = preferences.edit() + .putString(KavitaConstants.customSourceNamePref, newValue.toString()) + .commit() + Toast.makeText( + screen.context, + "Restart Tachiyomi to apply new setting.", + Toast.LENGTH_LONG + ).show() + Log.v(LOG_TAG, "[Preferences] Successfully modified custom source name: $newValue") + res + } + } + + screen.addPreference(customSourceNamePref) + screen.addPreference(opdsAddressPref) + screen.addPreference(enabledFiltersPref) + } + + private fun androidx.preference.PreferenceScreen.editTextPreference( + preKey: String, + title: String, + default: String, + summary: String, + isPassword: Boolean = false + ): EditTextPreference { + return EditTextPreference(context).apply { + key = preKey + this.title = title + val input = preferences.getString(title, null) + this.summary = if (input == null || input.isEmpty()) summary else input + this.setDefaultValue(default) + dialogTitle = title + + if (isPassword) { + setOnBindEditTextListener { + it.inputType = + InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD + } + } + setOnPreferenceChangeListener { _, newValue -> + try { + val res = preferences.edit().putString(title, newValue as String).commit() + Toast.makeText( + context, + "Restart Tachiyomi to apply new setting.", + Toast.LENGTH_LONG + ).show() + setupLogin(newValue) + Log.v(LOG_TAG, "[Preferences] Successfully modified OPDS URL") + res + } catch (e: Exception) { + e.printStackTrace() + false + } + } + } + } + + // private fun getPrefapiKey(): String = preferences.getString("APIKEY", "")!! + private fun getPrefBaseUrl(): String = preferences.getString("BASEURL", "")!! + private fun getPrefApiUrl(): String = preferences.getString("APIURL", "")!! + private fun getPrefKey(key: String): String = preferences.getString(key, "")!! + private fun getToggledFilters() = preferences.getStringSet(KavitaConstants.toggledFiltersPref, KavitaConstants.defaultFilterPrefEntries)!! + + // We strip the last slash since we will append it above + private fun getPrefAddress(): String { + var path = preferences.getString(ADDRESS_TITLE, "")!! + if (path.isNotEmpty() && path.last() == '/') { + path = path.substring(0, path.length - 1) + } + return path + } + + companion object { + private const val ADDRESS_TITLE = "Address" + private val JSON_MEDIA_TYPE = "application/json; charset=utf-8".toMediaTypeOrNull() + } + + /** + * LOGIN + **/ + private fun setupLogin(addressFromPreference: String = "") { + Log.v(LOG_TAG, "[Setup Login] Starting setup") + val validaddress = if (address.isEmpty()) addressFromPreference else address + val tokens = validaddress.split("/api/opds/") + val apiKey = tokens[1] + val baseUrlSetup = tokens[0].replace("\n", "\\n") + + if (!baseUrlSetup.startsWith("http")) { + try { + throw Exception("""Url does not start with "http/s" but with ${baseUrlSetup.split("://")[0]} """) + } catch (e: Exception) { + throw Exception("""Malformed Url: $baseUrlSetup""") + } + } + preferences.edit().putString("BASEURL", baseUrlSetup).commit() + preferences.edit().putString("APIKEY", apiKey).commit() + preferences.edit().putString("APIURL", "$baseUrlSetup/api").commit() + Log.v(LOG_TAG, "[Setup Login] Setup successful") + } + + private fun doLogin() { + + if (address.isEmpty()) { + Log.e(LOG_TAG, "OPDS URL is empty or null") + throw IOException("You must setup the Address to communicate with Kavita") + } + if (address.split("/opds/").size != 2) { + throw IOException("Address is not correct. Please copy from User settings -> OPDS Url") + } + if (jwtToken.isEmpty()) setupLogin() + Log.v(LOG_TAG, "[Login] Starting login") + val request = POST( + "$apiUrl/Plugin/authenticate?apiKey=${getPrefKey("APIKEY")}&pluginName=Tachiyomi-Kavita", + setupLoginHeaders().build(), "{}".toRequestBody("application/json; charset=utf-8".toMediaTypeOrNull()) + ) + client.newCall(request).execute().use { + if (it.code == 200) { + try { + jwtToken = it.parseAs().token + isLoged = true + } catch (e: Exception) { + Log.e(LOG_TAG, "Possible outdated kavita", e) + throw IOException("Please check your kavita version.\nv0.5+ is required for the extension to work properly") + } + } else { + Log.e(LOG_TAG, "[LOGIN] login failed. Authentication was not successful -> Code: ${it.code}.Response message: ${it.message} Response body: ${it.body!!}.") + throw LoginErrorException("[LOGIN] login failed. Authentication was not successful") + } + } + Log.v(LOG_TAG, "[Login] Login successful") + } + + init { + if (apiUrl.isNotBlank()) { + Single.fromCallable { + // Login + var loginSuccesful = false + try { + doLogin() + loginSuccesful = true + } catch (e: LoginErrorException) { + Log.e(LOG_TAG, "Init login failed: $e") + } + if (loginSuccesful) { // doing this check to not clutter LOGS + // Genres + Log.v(LOG_TAG, "[Filter] Fetching filters ") + try { + client.newCall(GET("$apiUrl/Metadata/genres", headersBuilder().build())) + .execute().use { response -> + genresListMeta = try { + val responseBody = response.body + if (responseBody != null) { + responseBody.use { json.decodeFromString(it.string()) } + } else { + Log.e( + LOG_TAG, + "[Filter] Error decoding JSON for genres filter: response body is null. Response code: ${response.code}" + ) + emptyList() + } + } catch (e: Exception) { + Log.e(LOG_TAG, "[Filter] Error decoding JSON for genres filter -> ${response.body!!}", e) + emptyList() + } + } + } catch (e: Exception) { + Log.e(LOG_TAG, "[Filter] Error loading genres for filters", e) + } + // tagsListMeta + try { + client.newCall(GET("$apiUrl/Metadata/tags", headersBuilder().build())) + .execute().use { response -> + tagsListMeta = try { + val responseBody = response.body + if (responseBody != null) { + responseBody.use { json.decodeFromString(it.string()) } + } else { + Log.e( + LOG_TAG, + "[Filter] Error decoding JSON for tagsList filter: response body is null. Response code: ${response.code}" + ) + emptyList() + } + } catch (e: Exception) { + Log.e(LOG_TAG, "[Filter] Error decoding JSON for tagsList filter", e) + emptyList() + } + } + } catch (e: Exception) { + Log.e(LOG_TAG, "[Filter] Error loading tagsList for filters", e) + } + // age-ratings + try { + client.newCall( + GET( + "$apiUrl/Metadata/age-ratings", + headersBuilder().build() + ) + ).execute().use { response -> + ageRatingsListMeta = try { + val responseBody = response.body + if (responseBody != null) { + responseBody.use { json.decodeFromString(it.string()) } + } else { + Log.e( + LOG_TAG, + "[Filter] Error decoding JSON for age-ratings filter: response body is null. Response code: ${response.code}" + ) + emptyList() + } + } catch (e: Exception) { + Log.e( + LOG_TAG, + "[Filter] Error decoding JSON for age-ratings filter", + e + ) + emptyList() + } + } + } catch (e: Exception) { + Log.e(LOG_TAG, "[Filter] Error loading age-ratings for age-ratings", e) + } + // collectionsListMeta + try { + client.newCall(GET("$apiUrl/Collection", headersBuilder().build())) + .execute().use { response -> + collectionsListMeta = try { + val responseBody = response.body + if (responseBody != null) { + responseBody.use { json.decodeFromString(it.string()) } + } else { + Log.e( + LOG_TAG, + "[Filter] Error decoding JSON for collectionsListMeta filter: response body is null. Response code: ${response.code}" + ) + emptyList() + } + } catch (e: Exception) { + Log.e( + LOG_TAG, + "[Filter] Error decoding JSON for collectionsListMeta filter", + e + ) + emptyList() + } + } + } catch (e: Exception) { + Log.e(LOG_TAG, "[Filter] Error loading collectionsListMeta for collectionsListMeta", e) + } + // languagesListMeta + try { + client.newCall(GET("$apiUrl/Metadata/languages", headersBuilder().build())) + .execute().use { response -> + languagesListMeta = try { + val responseBody = response.body + if (responseBody != null) { + responseBody.use { json.decodeFromString(it.string()) } + } else { + Log.e( + LOG_TAG, + "[Filter] Error decoding JSON for languagesListMeta filter: response body is null. Response code: ${response.code}" + ) + emptyList() + } + } catch (e: Exception) { + Log.e( + LOG_TAG, + "[Filter] Error decoding JSON for languagesListMeta filter", + e + ) + emptyList() + } + } + } catch (e: Exception) { + Log.e(LOG_TAG, "[Filter] Error loading languagesListMeta for languagesListMeta", e) + } + // libraries + try { + client.newCall(GET("$apiUrl/Library", headersBuilder().build())).execute() + .use { response -> + libraryListMeta = try { + val responseBody = response.body + if (responseBody != null) { + responseBody.use { json.decodeFromString(it.string()) } + } else { + Log.e( + LOG_TAG, + "[Filter] Error decoding JSON for libraries filter: response body is null. Response code: ${response.code}" + ) + emptyList() + } + } catch (e: Exception) { + Log.e( + LOG_TAG, + "[Filter] Error decoding JSON for libraries filter", + e + ) + emptyList() + } + } + } catch (e: Exception) { + Log.e(LOG_TAG, "[Filter] Error loading libraries for languagesListMeta", e) + } + // peopleListMeta + try { + client.newCall(GET("$apiUrl/Metadata/people", headersBuilder().build())) + .execute().use { response -> + peopleListMeta = try { + val responseBody = response.body + if (responseBody != null) { + responseBody.use { json.decodeFromString(it.string()) } + } else { + Log.e( + LOG_TAG, + "error while decoding JSON for peopleListMeta filter: response body is null. Response code: ${response.code}" + ) + emptyList() + } + } catch (e: Exception) { + Log.e( + LOG_TAG, + "error while decoding JSON for peopleListMeta filter", + e + ) + emptyList() + } + } + } catch (e: Exception) { + Log.e(LOG_TAG, "[Filter] Error loading tagsList for peopleListMeta", e) + } + try { + client.newCall(GET("$apiUrl/Metadata/publication-status", headersBuilder().build())) + .execute().use { response -> + pubStatusListMeta = try { + val responseBody = response.body + if (responseBody != null) { + responseBody.use { json.decodeFromString(it.string()) } + } else { + Log.e( + LOG_TAG, + "error while decoding JSON for publicationStatusListMeta filter: response body is null. Response code: ${response.code}" + ) + emptyList() + } + } catch (e: Exception) { + Log.e( + LOG_TAG, + "error while decoding JSON for publicationStatusListMeta filter", + e + ) + emptyList() + } + } + } catch (e: Exception) { + Log.e(LOG_TAG, "[Filter] Error loading tagsList for peopleListMeta", e) + } + + Log.v(LOG_TAG, "[Filter] Successfully loaded metadata tags from server") + } + Log.v(LOG_TAG, "Successfully ended init") + } + .subscribeOn(Schedulers.io()) + .observeOn(Schedulers.io()) + .subscribe( + {}, + { tr -> + Log.e(LOG_TAG, "error while doing initial calls", tr) + } + ) + } + } +} diff --git a/src/all/kavita/src/eu/kanade/tachiyomi/extension/all/kavita/KavitaConstants.kt b/src/all/kavita/src/eu/kanade/tachiyomi/extension/all/kavita/KavitaConstants.kt new file mode 100644 index 000000000..1fa7ddeb8 --- /dev/null +++ b/src/all/kavita/src/eu/kanade/tachiyomi/extension/all/kavita/KavitaConstants.kt @@ -0,0 +1,77 @@ +package eu.kanade.tachiyomi.extension.all.kavita + +object KavitaConstants { + // togle filters + const val toggledFiltersPref = "toggledFilters" + val filterPrefEntries = arrayOf( + "Sort Options", + "Format", + "Libraries", + "Read Status", + "Genres", + "Tags", + "Collections", + "Languages", + "Publication Status", + "Rating", + "Age Rating", + "Writers", + "Penciller", + "Inker", + "Colorist", + "Letterer", + "Cover Artist", + "Editor", + "Publisher", + "Character", + "Translators" + ) + val filterPrefEntriesValue = arrayOf( + "Sort Options", + "Format", + "Libraries", + "Read Status", + "Genres", + "Tags", + "Collections", + "Languages", + "Publication Status", + "Rating", + "Age Rating", + "Writers", + "Penciller", + "Inker", + "Colorist", + "Letterer", + "CoverArtist", + "Editor", + "Publisher", + "Character", + "Translators" + ) + val defaultFilterPrefEntries = setOf( + "Sort Options", + "Format", + "Libraries", + "Read Status", + "Genres", + "Tags", + "Collections", + "Languages", + "Publication Status", + "Rating", + "Age Rating", + "Writers", + "Penciller", + "Inker", + "Colorist", + "Letterer", + "CoverArtist", + "Editor", + "Publisher", + "Character", + "Translators" + ) + + const val customSourceNamePref = "customSourceName" +} diff --git a/src/all/kavita/src/eu/kanade/tachiyomi/extension/all/kavita/KavitaFactory.kt b/src/all/kavita/src/eu/kanade/tachiyomi/extension/all/kavita/KavitaFactory.kt new file mode 100644 index 000000000..c709140d7 --- /dev/null +++ b/src/all/kavita/src/eu/kanade/tachiyomi/extension/all/kavita/KavitaFactory.kt @@ -0,0 +1,13 @@ +package eu.kanade.tachiyomi.extension.all.kavita + +import eu.kanade.tachiyomi.source.Source +import eu.kanade.tachiyomi.source.SourceFactory + +class KavitaFactory : SourceFactory { + override fun createSources(): List = + listOf( + Kavita("1"), + Kavita("2"), + Kavita("3") + ) +} diff --git a/src/all/kavita/src/eu/kanade/tachiyomi/extension/all/kavita/KavitaHelper.kt b/src/all/kavita/src/eu/kanade/tachiyomi/extension/all/kavita/KavitaHelper.kt new file mode 100644 index 000000000..afe2caee0 --- /dev/null +++ b/src/all/kavita/src/eu/kanade/tachiyomi/extension/all/kavita/KavitaHelper.kt @@ -0,0 +1,48 @@ +package eu.kanade.tachiyomi.extension.all.kavita + +import eu.kanade.tachiyomi.extension.all.kavita.dto.PaginationInfo +import eu.kanade.tachiyomi.extension.all.kavita.dto.SeriesDto +import eu.kanade.tachiyomi.source.model.SManga +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.json.Json +import okhttp3.Response +import java.text.SimpleDateFormat +import java.util.Locale +import java.util.TimeZone + +class KavitaHelper { + val json = Json { + isLenient = true + ignoreUnknownKeys = true + allowSpecialFloatingPointValues = true + useArrayPolymorphism = true + prettyPrint = true + } + + val dateFormatter = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSSSS", Locale.US) + .apply { timeZone = TimeZone.getTimeZone("UTC") } + fun parseDate(dateAsString: String): Long = + dateFormatter.parse(dateAsString)?.time ?: 0 + + fun hasNextPage(response: Response): Boolean { + val paginationHeader = response.header("Pagination") + var hasNextPage = false + if (!paginationHeader.isNullOrEmpty()) { + val paginationInfo = json.decodeFromString(paginationHeader) + hasNextPage = paginationInfo.currentPage + 1 > paginationInfo.totalPages + } + return !hasNextPage + } + + fun getIdFromUrl(url: String): Int { + return url.split("/").last().toInt() + } + + fun createSeriesDto(obj: SeriesDto, baseUrl: String): SManga = + SManga.create().apply { + url = "$baseUrl/Series/${obj.id}" + title = obj.name + // Deprecated: description = obj.summary + thumbnail_url = "$baseUrl/image/series-cover?seriesId=${obj.id}" + } +} diff --git a/src/all/kavita/src/eu/kanade/tachiyomi/extension/all/kavita/dto/MangaDto.kt b/src/all/kavita/src/eu/kanade/tachiyomi/extension/all/kavita/dto/MangaDto.kt new file mode 100644 index 000000000..e10d2087a --- /dev/null +++ b/src/all/kavita/src/eu/kanade/tachiyomi/extension/all/kavita/dto/MangaDto.kt @@ -0,0 +1,110 @@ +package eu.kanade.tachiyomi.extension.all.kavita.dto + +import kotlinx.serialization.Serializable + +@Serializable +enum class MangaFormat(val format: Int) { + Image(0), + Archive(1), + Unknown(2), + Epub(3), + Pdf(4); + companion object { + private val map = PersonRole.values().associateBy(PersonRole::role) + fun fromInt(type: Int) = map[type] + } +} +enum class PersonRole(val role: Int) { + Other(1), + Writer(3), + Penciller(4), + Inker(5), + Colorist(6), + Letterer(7), + CoverArtist(8), + Editor(9), + Publisher(10), + Character(11), + Translator(12); + companion object { + private val map = PersonRole.values().associateBy(PersonRole::role) + fun fromInt(type: Int) = map[type] + } +} +@Serializable +data class SeriesDto( + val id: Int, + val name: String, + val originalName: String = "", + val thumbnail_url: String? = "", + val localizedName: String? = "", + val sortName: String? = "", + val pages: Int, + val coverImageLocked: Boolean = true, + val pagesRead: Int, + val userRating: Int, + val userReview: String? = "", + val format: Int, + val created: String? = "", + val libraryId: Int, + val libraryName: String? = "" +) + +@Serializable +data class SeriesMetadataDto( + val id: Int, + val summary: String? = "", + val writers: List = emptyList(), + val coverArtists: List = emptyList(), + val genres: List = emptyList(), + val seriesId: Int, + val ageRating: Int + +) +@Serializable +data class Genres( + val title: String +) +@Serializable +data class Person( + val name: String +) + +@Serializable +data class VolumeDto( + val id: Int, + val number: Int, + val name: String, + val pages: Int, + val pagesRead: Int, + val lastModified: String, + val created: String, + val seriesId: Int, + val chapters: List = emptyList() +) + +@Serializable +data class ChapterDto( + val id: Int, + val range: String, + val number: String, + val pages: Int, + val isSpecial: Boolean, + val title: String, + val pagesRead: Int, + val coverImageLocked: Boolean, + val volumeId: Int, + val created: String +) + +@Serializable +data class KavitaComicsSearch( + val seriesId: Int, + val name: String, + val originalName: String, + val sortName: String, + val localizedName: String, + val format: Int, + val libraryName: String, + val libraryId: Int +) diff --git a/src/all/kavita/src/eu/kanade/tachiyomi/extension/all/kavita/dto/MetadataDto.kt b/src/all/kavita/src/eu/kanade/tachiyomi/extension/all/kavita/dto/MetadataDto.kt new file mode 100644 index 000000000..b2b7c79fa --- /dev/null +++ b/src/all/kavita/src/eu/kanade/tachiyomi/extension/all/kavita/dto/MetadataDto.kt @@ -0,0 +1,76 @@ +package eu.kanade.tachiyomi.extension.all.kavita.dto + +import kotlinx.serialization.Serializable +/** +* This file contains all class for filtering +* */ +@Serializable +data class MetadataGenres( + val id: Int, + val title: String, +) +@Serializable +data class MetadataPeople( + val id: Int, + val name: String, + val role: Int +) +@Serializable +data class MetadataPubStatus( + val value: Int, + val title: String +) +@Serializable +data class MetadataTag( + val id: Int, + val title: String, +) +@Serializable +data class MetadataAgeRatings( + val value: Int, + val title: String +) +@Serializable +data class MetadataLanguages( + val isoCode: String, + val title: String +) +@Serializable +data class MetadataLibrary( + val id: Int, + val name: String, + val type: Int +) +@Serializable +data class MetadataCollections( + val id: Int, + val title: String, +) + +data class MetadataPayload( + var sorting: Int = 1, + var sorting_asc: Boolean = true, + var readStatus: ArrayList = arrayListOf< String>(), + var genres: ArrayList = arrayListOf(), + var tags: ArrayList = arrayListOf(), + var ageRating: ArrayList = arrayListOf(), + var formats: ArrayList = arrayListOf(), + var collections: ArrayList = arrayListOf(), + var userRating: Int = 0, + var people: ArrayList = arrayListOf(), + var language: ArrayList = arrayListOf(), + var libraries: ArrayList = arrayListOf(), + var pubStatus: ArrayList = arrayListOf(), + + var peopleWriters: ArrayList = arrayListOf(), + var peoplePenciller: ArrayList = arrayListOf(), + var peopleInker: ArrayList = arrayListOf(), + var peoplePeoplecolorist: ArrayList = arrayListOf(), + var peopleLetterer: ArrayList = arrayListOf(), + var peopleCoverArtist: ArrayList = arrayListOf(), + var peopleEditor: ArrayList = arrayListOf(), + var peoplePublisher: ArrayList = arrayListOf(), + var peopleCharacter: ArrayList = arrayListOf(), + var peopleTranslator: ArrayList = arrayListOf(), + +) diff --git a/src/all/kavita/src/eu/kanade/tachiyomi/extension/all/kavita/dto/Responses.kt b/src/all/kavita/src/eu/kanade/tachiyomi/extension/all/kavita/dto/Responses.kt new file mode 100644 index 000000000..a2120c9a7 --- /dev/null +++ b/src/all/kavita/src/eu/kanade/tachiyomi/extension/all/kavita/dto/Responses.kt @@ -0,0 +1,18 @@ +package eu.kanade.tachiyomi.extension.all.kavita.dto + +import kotlinx.serialization.Serializable + +@Serializable // Used to process login +data class AuthenticationDto( + val username: String, + val token: String, + val apiKey: String +) + +@Serializable +data class PaginationInfo( + val currentPage: Int, + val itemsPerPage: Int, + val totalItems: Int, + val totalPages: Int +)